<?php

if ( ! class_exists( 'LoginPress_Attempts' ) ) :

	/**
	 * LoginPress_Attempts
	 */
	class LoginPress_Attempts {

		/**
		 * Variable for LoginPress Limit Login Attempts table name.
		 *
		 * @var string
		 * @since 3.0.0
		 */
		protected $llla_table;

		/**
		 * Variable that Check for LoginPress Key.
		 *
		 * @var string
		 * @since 3.0.0
		 */
		public $attempts_settings;
		/**
		 * Variable to store the time of the attempt.
		 *
		 * @var string
		 * @since 5.0.0
		 */
		public $attempt_time;
		/**
		 * Variable that Check for LoginPress hidelogin settings.
		 *
		 * @var string
		 * @since 5.0.0
		 */
		public $loginpress_hidelogin;
		/**
		 * Variable that Check for edd block status.
		 *
		 * @var string
		 * @since 5.0.0
		 */
		public $edd_block_error;
		/**
		 * Variable that stores the ip of the user.
		 *
		 * @var string
		 * @since 5.0.0
		 */
		public $ip;
		/**
		 * Variable that stores the username of the user.
		 *
		 * @var string
		 * @since 6.0.0
		 */
		public $username;
		/**
		 * Class constructor.
		 */
		public function __construct() {
			$this->edd_block_error = true;
				global $wpdb;
				$this->llla_table           = $wpdb->prefix . 'loginpress_limit_login_details';
				$this->attempts_settings    = get_option( 'loginpress_limit_login_attempts' );
				if ( ! $this->attempts_settings ) {
					update_option(
						'loginpress_limit_login_attempts',
						array(
							'attempts_allowed' => 4,
							'minutes_lockout'  => 20,
						)
					);
					$this->attempts_settings = array(
						'attempts_allowed' => 4,
						'minutes_lockout'  => 20,
					);
				}
				$this->loginpress_hidelogin = get_option( 'loginpress_hidelogin' );
				$this->ip                   = $this->get_address();
				$is_llla_active             = get_option( 'loginpress_pro_addons' );
			if ( isset( $is_llla_active['limit-login-attempts']['is_active'] ) && $is_llla_active['limit-login-attempts']['is_active'] ) {
				$this->hooks();
			}
		}

		/** * * * * * *
		 * Action hooks.
		 *
		 * @version 6.0.0
		 * * * * * * * */
		public function hooks() {

			// add_action( 'wp_login_failed', array( $this, 'llla_login_failed' ), 999,1  );
			add_action( 'wp_loaded', array( $this, 'llla_wp_loaded' ) );
			add_action( 'init', array( $this, 'llla_check_xml_request' ) );
			add_action( 'init', array( $this, 'hide_login_integrate' ) );
			add_action( 'init', array( $this, 'loginpress_login_widget_integrate' ) ); // Integrate Widget Login Add-on.
			add_filter( 'authenticate', array( $this, 'llla_login_attempts_auth' ), 98, 3 );
			add_action( 'wp_login', array( $this, 'llla_login_attempts_wp_login' ), 99, 2 );
			// add_action( 'wp_login_failed', array( $this, 'llla_login_attempts_wp_login_failed' ), 10, 2 );
			$disable_xml_rpc = isset( $this->attempts_settings['disable_xml_rpc_request'] ) ? $this->attempts_settings['disable_xml_rpc_request'] : '';

			if ( 'on' === $disable_xml_rpc ) {
				$this->disable_xml_rpc();
			}

			$disable_user_rest_endpoint = isset( $this->attempts_settings['disable_user_rest_endpoint'] ) ? $this->attempts_settings['disable_user_rest_endpoint'] : '';
			if ( 'on' === $disable_user_rest_endpoint ) {
				add_filter( 'rest_endpoints', array( $this, 'llla_restrict_user_rest_endpoints' ) );
			}

			$disable_app_rest_endpoint = isset( $this->attempts_settings['disable_app_rest_endpoint'] ) ? $this->attempts_settings['disable_app_rest_endpoint'] : '';
			if ( 'on' === $disable_app_rest_endpoint && ! is_admin() ) {
				add_filter( 'rest_authentication_errors', array( $this, 'llla_restrict_app_rest_endpoints' ) );
			}

			$disable_app_pass_rest_endpoint = isset( $this->attempts_settings['disable_app_pass_rest_endpoint'] ) ? $this->attempts_settings['disable_app_pass_rest_endpoint'] : '';
			if ( 'on' === $disable_app_pass_rest_endpoint ) {
				add_filter( 'wp_is_application_passwords_available', array( $this, 'llla_restrict_app_passwords' ) );
			}
		}

		/**
		 * LoginPress Hide login Integration with TranslatePress and LoginPress Limit Login Attempts.
		 *
		 * @return void
		 * @since  3.0.0
		 */
		public function hide_login_integrate() {

			global $pagenow, $wpdb;
			$loginpress_hidelogin = $this->loginpress_hidelogin;
			if ( 'index.php' === $pagenow && $this->llla_time() && isset( $loginpress_hidelogin['rename_login_slug'] ) ) {

				$last_attempt_time = $wpdb->get_var( $wpdb->prepare( "SELECT `datentime` FROM `{$this->llla_table}` WHERE `ip` = %s ORDER BY `datentime` DESC", $this->ip ) ); // @codingStandardsIgnoreLine.

				$slug                 = isset( $loginpress_hidelogin['rename_login_slug'] ) ? $loginpress_hidelogin['rename_login_slug'] : '';
				$admin_url            = get_admin_url( null, '', 'admin' );
				$current_login_url    = home_url() . $slug . '/';
				$additional_login_url = home_url() . $slug;

				if ( isset( $_SERVER['HTTPS'] ) && 'on' === $_SERVER['HTTPS'] ) {
					$url = 'https';
				} else {
					$url = 'http';
				}
				// Here append the common URL characters.
				$url .= '://';
				// Append the host(domain name, ip) to the URL.
				$url .= isset( $_SERVER['HTTP_HOST'] ) ? wp_unslash( sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) ) : '';
				// Append the requested resource location to the URL.
				$url .= isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) ) : '';

				if ( ( $current_login_url === $url || $admin_url === $url || $additional_login_url === $url ) && $this->llla_time() ) {
					wp_die( __( $this->loginpress_lockout_error( $last_attempt_time ) ), 403 ); // @codingStandardsIgnoreLine.
				}
			}
			$this->llla_wp_loaded();
		}

		/**
		 * Compatibility with LoginPress - Login Widget Add-On.
		 *
		 * @return void
		 * @since 3.0.0
		 * @version 6.0.1
		 */
		public function loginpress_login_widget_integrate() {
			if ( is_user_logged_in() ) {
				return null;
			}
			global $wpdb;

			$attempts_allowed  = isset( $this->attempts_settings['attempts_allowed'] ) && intval( $this->attempts_settings['attempts_allowed'] ) !== 0 ? intval( $this->attempts_settings['attempts_allowed'] ) : intval( 4 );
			$last_attempt_time = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(ip) FROM `{$this->llla_table}` WHERE `ip` = %s ORDER BY `datentime` DESC", $this->ip ) ); // @codingStandardsIgnoreLine.

			if ( $this->llla_time() && ( $last_attempt_time >= $attempts_allowed ) ) {
				add_filter( 'dynamic_sidebar_params', array( $this, 'loginpress_widget_params' ), 10 );
			}
		}

		/**
		 * Remove LoginPress - Login widgets if LoginPress - Limit Login Attempts applied.
		 *
		 * @param array $params widget parameter.
		 * @since 3.0.0
		 * @return Widgets
		 */
		public function loginpress_widget_params( $params ) {

			foreach ( $params as $param_index => $param_val ) {

				if ( isset( $param_val['widget_id'] ) && strpos( $param_val['widget_id'], 'loginpress-login-widget' ) !== false ) {
					unset( $params[ $param_index ] );
				}
			}

			return $params;
		}

		/**
		 * Check Auth if request coming from xmlrpc.
		 *
		 * @since 3.0.0
		 * @return void
		 */
		public function llla_check_xml_request() {

			global $pagenow;
			if ( 'xmlrpc.php' === $pagenow ) {
				$this->llla_wp_loaded();
			}
		}

		/**
		 * Disable xml rpc request
		 *
		 * @since 3.0.0
		 * @return void
		 */
		public function disable_xml_rpc() {

			add_filter( 'xmlrpc_enabled', '__return_false' );
		}

		/**
		 * Restrict access to user REST API endpoints.
		 *
		 * This function removes the '/wp/v2/users' and '/wp/v2/users/(?P<id>[\d]+)'
		 * endpoints from the list of available endpoints, effectively disabling
		 * access to user data through the REST API.
		 *
		 * @param array $endpoints An array of available REST API endpoints.
		 * @return array The modified array of REST API endpoints.
		 * @since 6.0.0
		 */
		public function llla_restrict_user_rest_endpoints( $endpoints ) {

			// Endpoints to restrict
			$user_endpoints = array(
				'/wp/v2/users',
				'/wp/v2/users/(?P<id>[\d]+)', // Matches /wp/v2/users/<id>
			);

			foreach ( $endpoints as $route => $endpoint ) {
				// Check if the route matches any user-related endpoints
				foreach ( $user_endpoints as $user_endpoint ) {
					if ( preg_match( '#^' . $user_endpoint . '$#', $route ) ) {
						// Modify the endpoint to require authentication
						$endpoints[ $route ][0]['callback'] = function ( $request ) {
							if ( ! is_user_logged_in() ) {
								return new WP_Error(
									'rest_user_access_denied',
									__( 'You must be logged in to access user data.', 'loginpress-pro' ),
									array( 'status' => 401 )
								);
							}
							// Call the original callback if authenticated
							return rest_do_request( $request );
						};
					}
				}
			}

			return $endpoints;
		}

		/**
		 * Restrict app password usage for unauthenticated users.
		 *
		 * @since 6.0.0
		 * @return false
		 */
		public function llla_restrict_app_passwords() {
			return false;
		}

		/**
		 * Restrict REST API authentication for unauthenticated users.
		 *
		 * @param WP_Error|null|true $errors Existing authentication errors.
		 * @return WP_Error|null|true Modified errors.
		 * @since 6.0.0
		 */
		public function llla_restrict_app_rest_endpoints( $errors ) {

			if ( is_user_logged_in() ) {
				return $errors;
			}

			// Block authentication attempts for unauthenticated users.
			return new WP_Error(
				'rest_authentication_disabled',
				__( 'REST API authentication is disabled for remote access.', 'loginpress-pro' ),
				array( 'status' => 401 )
			);
		}

		/**
		 * Attempts Login Authentication.
		 *
		 * @param object $user Object of the user.
		 * @param string $username username.
		 * @param string $password password.
		 * @since 3.0.0
		 * @version 6.0.0
		 */
		public function llla_login_attempts_auth( $user, $username, $password ) {
			$this->username = $username;
			if ( isset( $_POST['g-recaptcha-response'] ) && empty( $_POST['g-recaptcha-response'] ) ) {
				return;
			}
			if ( false === $this->edd_block_error ) {
				$this->edd_block_error = true;
				return;
			}

			if ( $user instanceof WP_User ) {
				return $user;
			}

			// Is username or password field empty?
			if ( empty( $username ) || empty( $password ) ) {

				if ( is_wp_error( $user ) ) {
					return $user;
				}

				$error = new WP_Error();

				if ( empty( $username ) ) {
					$error->add( 'empty_username', $this->limit_query( $username ) );
				}

				if ( empty( $password ) ) {
					$error->add( 'empty_password', $this->limit_query( $username ) );
				}

				return $error;
			}

			if ( ! empty( $username ) && ! empty( $password ) ) {

				$error = new WP_Error();
				global $pagenow, $wpdb;

				$whitelisted_ip = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$this->llla_table} WHERE ip = %s AND whitelist = %d LIMIT 1", $this->ip, 1 ) );
				if ( $whitelisted_ip >= 1 ) {
					return null;
				} else {
					// Check if user exists
					$error->add( 'llla_error', $this->limit_query( $username ) );
				}
				if ( class_exists( 'LifterLMS' ) && 'index.php' === $pagenow ) {
					/**
					 * Filter to show the Limit Login Attempts error on the LifterLMS login form.
					 *
					 * @since 5.0.0
					 */
					apply_filters( 'loginpress_llla_error_filter', false );
				}

				return $error;
			}
		}

		/**
		 * Die WordPress login on blacklist or lockout.
		 *
		 * @since  3.0.0
		 */
		public function llla_wp_loaded() {
			if ( is_user_logged_in() ) {
				return;
			}
			if ( class_exists( 'Easy_Digital_Downloads' ) ) {
				// Unset the 'edd_invalid_login' error to prevent the default Easy Digital Downloads login error
				// from displaying, allowing custom error handling via LoginPress.
				edd_unset_error( 'edd_invalid_login' );
			}
			global $pagenow, $wpdb;

			$last_attempt_time = $wpdb->get_var( $wpdb->prepare( "SELECT `datentime` FROM `{$this->llla_table}` WHERE `ip` = %s ORDER BY `datentime` DESC", $this->ip ) ); // @codingStandardsIgnoreLine.

			$blacklist_check = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `{$this->llla_table}` WHERE `ip` = %s AND `blacklist` = 1", $this->ip ) ); // @codingStandardsIgnoreLine.

			if ( 'xmlrpc.php' === $pagenow && ( $this->llla_time() || $blacklist_check >= 1 ) ) {
				echo $this->loginpress_lockout_error( $last_attempt_time ); // @codingStandardsIgnoreLine.
				wp_die( '', 403 );
			}
			$blacklist_message = isset( $this->attempts_settings['blacklist_message'] ) && ! empty( $this->attempts_settings['blacklist_message'] ) ? $this->attempts_settings['blacklist_message'] : __( 'You are not allowed to access admin panel', 'loginpress-pro' );
			// limit wp-admin access.
			if ( is_admin() && $blacklist_check >= 1 ) {
				wp_die( __( $blacklist_message, 'loginpress-pro' ), 403 ); // @codingStandardsIgnoreLine.
			}

			// limit wp-login.php access if blacklisted.
			if ( 'wp-login.php' === $pagenow && $blacklist_check >= 1 ) {
				wp_die( __( $blacklist_message, 'loginpress-pro' ), 403 ); // @codingStandardsIgnoreLine.
			}

			// limit wp-login.php access if time remains.
			if ( 'wp-login.php' === $pagenow && $this->llla_time() && $this->loginpress_lockout_error( $last_attempt_time ) ) {
				wp_die( $this->loginpress_lockout_error( $last_attempt_time ), 403 ); // @codingStandardsIgnoreLine.
			}

			// limit WooCommerce Account access if blacklisted.
			if ( 'index.php' === $pagenow && $blacklist_check >= 1 && class_exists( 'WooCommerce' ) ) {

				remove_shortcode( 'woocommerce_my_account' );
				add_shortcode( 'woocommerce_my_account', array( $this, 'woo_blacklisted_error' ) );
			}

			// limit WooCommerce Account access if time remains.
			if ( $this->llla_time() && class_exists( 'WooCommerce' ) && 'index.php' === $pagenow ) {
				remove_shortcode( 'woocommerce_my_account' );
				remove_action( 'woocommerce_before_checkout_form', 'woocommerce_checkout_login_form' );
				add_shortcode( 'woocommerce_my_account', array( $this, 'woo_attempt_error' ) );
			}

			// Handle EDD login forms when blacklisted.
			if ( 'index.php' === $pagenow && $blacklist_check >= 1 && class_exists( 'Easy_Digital_Downloads' ) ) {
				remove_shortcode( 'edd_login' );
				add_shortcode( 'edd_login', array( $this, 'loginpress_edd_blacklisted_error' ) );
				add_filter( 'render_block', array( $this, 'lp_render_edd_blacklisted_error_block' ), 10, 2 );
			}

			// Handle EDD login forms when lockout time remains.
			if ( $this->llla_time() && class_exists( 'Easy_Digital_Downloads' ) && 'index.php' === $pagenow ) {

				remove_shortcode( 'edd_login' );
				add_shortcode( 'edd_login', array( $this, 'loginpress_edd_attempt_error' ) );
				if ( $this->edd_block_error == true ) {
					$this->edd_block_error = false;
					add_filter( 'render_block', array( $this, 'lp_render_edd_attempt_error_block' ), 10, 2 );
				}
			}
		}

		/**
		 * Replaces the EDD login block with the blacklisted error content.
		 *
		 * @param string $block_content The original block content.
		 * @param array  $block         The block being rendered.
		 * @return string Modified block content.
		 */
		public function lp_render_edd_blacklisted_error_block( $block_content, $block ) {
			if ( isset( $block['blockName'] ) && $block['blockName'] === 'edd/login' ) {
				return $this->loginpress_edd_blacklisted_error( array() );
			}
			return $block_content;
		}

		/**
		 * Replaces the EDD login block with the login attempt error content.
		 *
		 * @param string $block_content The original block content.
		 * @param array  $block         The block being rendered.
		 * @return string Modified block content.
		 */
		public function lp_render_edd_attempt_error_block( $block_content, $block ) {
			if ( isset( $block['blockName'] ) && $block['blockName'] === 'edd/login' ) {
				return $this->loginpress_edd_attempt_error();
			}
			return $block_content;
		}

		/**
		 * Callback for error message 'EDD login blacklisted'
		 *
		 * @since 5.0.0
		 */
		public function loginpress_edd_blacklisted_error() {
			$blacklist_message = isset( $this->attempts_settings['blacklist_message'] ) && ! empty( $this->attempts_settings['blacklist_message'] ) ? $this->attempts_settings['blacklist_message'] : __( 'You are not allowed to access admin panel', 'loginpress-pro' );
			echo '<div class="edd-alert-error">';
			echo esc_html__( $blacklist_message, 'loginpress-pro' );// @codingStandardsIgnoreLine.
			echo '</div>';
		}

		/**
		 * Callback for error message 'EDD login attempt error'
		 *
		 * @since 5.0.0
		 */
		public function loginpress_edd_attempt_error() {
			echo '<div class="edd-alert-error">';

			global $wpdb;
			$last_attempt_time = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM `{$this->llla_table}` WHERE `ip` = %s ORDER BY `datentime` DESC", $this->ip ) ); // @codingStandardsIgnoreLine.
			if ( $last_attempt_time ) {
				$last_attempt_time = $last_attempt_time->datentime;
			}
			echo wp_kses_post( $this->loginpress_lockout_error( $last_attempt_time ) );
			echo '</div>';
		}

		/**
		 * Callback for error message 'woocommerce my-account login blacklisted'
		 *
		 * @since 2.1.0
		 */
		public function woo_blacklisted_error() {
			$blacklist_message = isset( $this->attempts_settings['blacklist_message'] ) && ! empty( $this->attempts_settings['blacklist_message'] ) ? $this->attempts_settings['blacklist_message'] : __( 'You are not allowed to access admin panel', 'loginpress-pro' );
			?>
			<div class="woocommerce-error">
				<?php
				echo esc_html__( $blacklist_message, 'loginpress-pro' );// @codingStandardsIgnoreLine.
				?>
			</div>
			<?php
		}

		/**
		 * Callback for error message 'woocommerce my-account login attempt'
		 *
		 * @since 2.1.0
		 */
		public function woo_attempt_error() {

			echo '<div class="woocommerce-error">';

			global $wpdb;
			$last_attempt_time = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM `{$this->llla_table}` WHERE `ip` = %s ORDER BY `datentime` DESC", $this->ip ) ); // @codingStandardsIgnoreLine.
			if ( $last_attempt_time ) {
				$last_attempt_time = $last_attempt_time->datentime;
			}
			echo wp_kses_post( $this->loginpress_lockout_error( $last_attempt_time ) );
			echo '</div>';
		}

		/**
		 * Check the limit
		 */
		public function user_limit_check() {

			global $wpdb;
			$current_time = current_time( 'timestamp' ); // @codingStandardsIgnoreLine.
			$gate         = $this->gateway();

			$attempts_allowed  = isset( $this->attempts_settings['attempts_allowed'] ) ? $this->attempts_settings['attempts_allowed'] : '';
			$lockout_increase  = isset( $this->attempts_settings['lockout_increase'] ) ? $this->attempts_settings['lockout_increase'] : '';
			$minutes_lockout   = isset( $this->attempts_settings['minutes_lockout'] ) ? intval( $this->attempts_settings['minutes_lockout'] ) : '';
			$last_attempt_time = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM `{$this->llla_table}` WHERE `ip` = %s ORDER BY `datentime` DESC", $this->ip ) ); // @codingStandardsIgnoreLine.

			if ( $last_attempt_time ) {
				$last_attempt_time = $last_attempt_time->datentime;
			}

			$lockout_time = $current_time - ( $minutes_lockout * 60 );
			$attempt_time = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `{$this->llla_table}` WHERE `ip` = %s AND `datentime` > %s", $this->ip, $lockout_time ) ); // @codingStandardsIgnoreLine.

			return array(
				'attempts_allowed'  => $attempts_allowed,
				'lockout_increase'  => $lockout_increase,
				'minutes_lockout'   => $minutes_lockout,
				'last_attempt_time' => $last_attempt_time,
				'lockout_time'      => $lockout_time,
				'attempt_time'      => $attempt_time,
			);
		}

		/**
		 * Callback for error message 'llla_error'
		 *
		 * @param string $username username.
		 * @return string $error.
		 * @since  3.0.0
		 * @version 6.0.1
		 */
		public function limit_query( $username, $attempt_type = 0 ) {

			global $wpdb;
			$current_time = current_time( 'timestamp' ); // Returns floating point with microsecond precision // @codingStandardsIgnoreLine.
			$gate         = $this->gateway();
			if ( empty( $gate ) ) {
				return null;
			}

			$attempts_allowed  = isset( $this->attempts_settings['attempts_allowed'] ) && intval( $this->attempts_settings['attempts_allowed'] ) !== 0 ? intval( $this->attempts_settings['attempts_allowed'] ) : intval( 4 );
			$lockout_increase  = isset( $this->attempts_settings['lockout_increase'] ) ? $this->attempts_settings['lockout_increase'] : '';
			$minutes_lockout   = isset( $this->attempts_settings['minutes_lockout'] ) && intval( $this->attempts_settings['minutes_lockout'] ) !== 0 ? intval( $this->attempts_settings['minutes_lockout'] ) : intval(20 );
			$last_attempt_time = $wpdb->get_var( $wpdb->prepare( "SELECT `datentime` FROM `{$this->llla_table}` WHERE `ip` = %s ORDER BY `datentime` DESC", $this->ip ) ); // @codingStandardsIgnoreLine.

			if ( 1 === $attempt_type ) {
				if ( ! empty( $username ) ) {
					$wpdb->query( $wpdb->prepare( "INSERT INTO {$this->llla_table} (ip, username, datentime, gateway, login_status) values (%s, %s, %s, %s, %s)", $this->ip, $username, $current_time, $gate, 'Success' ) ); // @codingStandardsIgnoreLine.
				}
			} else {

				if ( $last_attempt_time ) {
					$last_attempt_time = is_object( $last_attempt_time ) || is_array( $last_attempt_time ) ? $last_attempt_time->datentime : $last_attempt_time;
				}

				$lockout_time = $current_time - ( $minutes_lockout * 60 );

				$attempt_time = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `{$this->llla_table}` WHERE `ip` = %s AND `datentime` > %s AND `login_status` != %s", $this->ip, $lockout_time, 'Success' ) ); // @codingStandardsIgnoreLine.
				if ( $attempt_time + 1 < $attempts_allowed ) {
					// 0 Attempts overhead solution.
					$wpdb->query( $wpdb->prepare( "INSERT INTO {$this->llla_table} (ip, username, datentime, gateway, login_status) values (%s, %s, %s, %s, %s)", $this->ip, $username, $current_time, $gate, 'Failed' ) ); // @codingStandardsIgnoreLine.

					return $this->loginpress_attempts_error( $attempt_time );

				} else {
					wp_die( $this->loginpress_lockout_error( $last_attempt_time ), 403 );

				}
			}
		}

		/**
		 * Lockout error message.
		 *
		 * @param string $last_attempt_time time of the last attempt.
		 * @since  3.0.0
		 * @version 6.0.1
		 * @return string $lockout_message Custom error message
		 */
		public function loginpress_lockout_error( $last_attempt_time ) {
			global $wpdb;
			$current_time     = current_time( 'timestamp' );
			$minutes_set      = isset( $this->attempts_settings['minutes_lockout'] ) && intval( $this->attempts_settings['minutes_lockout'] ) !== 0 ? intval( $this->attempts_settings['minutes_lockout'] ) : intval(20 );
			$lockout_window   = $current_time - ( $minutes_set * 60 );
			$attempts_allowed = isset( $this->attempts_settings['attempts_allowed'] ) && intval( $this->attempts_settings['attempts_allowed'] ) !== 0 ? intval( $this->attempts_settings['attempts_allowed'] ) : intval( 4 );;

			// Check if a 'Locked' entry already exists within the lockout window
			$existing_lock = $wpdb->get_var(
				$wpdb->prepare(
					"SELECT COUNT(*) FROM {$this->llla_table} WHERE ip = %s AND login_status = %s AND datentime >= %d",
					$this->ip,
					'Locked',
					$lockout_window
				)
			);

			if ( ! $existing_lock ) {
				// No recent locked entry, insert a new one
				$wpdb->query(
					$wpdb->prepare(
						"INSERT INTO {$this->llla_table} (ip, username, datentime, gateway, login_status) VALUES (%s, %s, %s, %s, %s)",
						$this->ip,
						$this->username,
						$current_time,
						$this->gateway(),
						'Locked'
					)
				);

				$this->lp_send_lockout_notification( $this->username, $this->ip, $this->gateway() );
			}

			$time            = intval( $current_time - $last_attempt_time );
			$count           = (int) ( $time / 60 ) % 60; // Minutes since last attempt
			$lockout_message = isset( $this->attempts_settings['lockout_message'] ) ? sanitize_text_field( $this->attempts_settings['lockout_message'] ) : '';
			$message         = __( 'You have exceeded the amount of login attempts.', 'loginpress-pro' );

			if ( $count < $minutes_set || $attempts_allowed == 1 ) {

				$remain = $minutes_set - $count;
				$remain = $remain == 0 ? 1 : $remain;
				$remain = $attempts_allowed == 1 ? 1 : $remain;
				$minute = ( $remain === 1 ) ? 'minute' : 'Minutes';

				if ( empty( $lockout_message ) ) {
					$message = sprintf( // translators: Default lockout message
						__( '%1$sError:%2$s Too many failed attempts. You are locked out for %3$s %4$s.', 'loginpress-pro' ),
						'<strong>',
						'</strong>',
						$remain,
						$minute
					);
				} else {
					$lockout_message = str_replace( '%TIME%', $remain . ' ' . $minute, $lockout_message );
					$message         = sprintf( // translators: User's lockout message
						__( '%1$sError:%2$s %3$s', 'loginpress-pro' ),
						'<strong>',
						'</strong>',
						$lockout_message
					);
				}
			}

			return $message;
		}


		/**
		 * LoginPress Limit Login Attempts Time Checker.
		 *
		 * @return boolean
		 * @since  3.0.0
		 * @version 6.0.1
		 */
		public function llla_time() {
			if ( is_user_logged_in() ) {
				return;
			}
			global $wpdb;
			$current_time = current_time( 'timestamp' ); // @codingStandardsIgnoreLine.

			$attempts_allowed = isset( $this->attempts_settings['attempts_allowed'] ) && intval( $this->attempts_settings['attempts_allowed'] ) !== 0 ? intval( $this->attempts_settings['attempts_allowed'] ) : intval( 4 );;
			$lockout_increase = isset( $this->attempts_settings['lockout_increase'] ) ? $this->attempts_settings['lockout_increase'] : '';
			$minutes_lockout  = isset( $this->attempts_settings['minutes_lockout'] ) && intval( $this->attempts_settings['minutes_lockout'] ) !== 0 ? intval( $this->attempts_settings['minutes_lockout'] ) : intval(20 );

			$lockout_time = $current_time - ( $minutes_lockout * 60 );
			if ( ! isset( $this->attempt_time ) || $this->attempt_time === null ) {
				$this->attempt_time = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `{$this->llla_table}` WHERE `ip` = %s AND `datentime` > %s AND `whitelist` = 0 AND `login_status` != %s", $this->ip, $lockout_time, 'Success' ) ); // @codingStandardsIgnoreLine.
			}
			// 0 Attempts overhead solution.
			if ( $this->attempt_time < $attempts_allowed ) {
				return false;
			} else {
				return true;
			}
		}

		/**
		 * Attempts error message
		 *
		 * @param int $count counter.
		 * @return string [Custom error message]
		 * @since 6.0.0
		 * @version 6.0.1
		 */
		public function loginpress_attempts_error( $count ) {
			global $pagenow;
			$attempts_allowed = isset( $this->attempts_settings['attempts_allowed'] ) && intval( $this->attempts_settings['attempts_allowed'] ) !== 0 ? intval( $this->attempts_settings['attempts_allowed'] ) : intval( 4 );

			$remains = $attempts_allowed - $count - 1;

			if ( isset( $this->attempts_settings['attempts_left_message'] ) && ! empty( $this->attempts_settings['attempts_left_message'] ) ) {
				$attempts_message = $this->attempts_settings['attempts_left_message'];
				$attempts_message = str_replace( '%count%', $remains, $attempts_message );
				// Check if the EDD class exists.
				if ( class_exists( 'Easy_Digital_Downloads' ) && isset( $_POST['edd_login_nonce'] ) ) {
					// translators: Modify the message without "ERROR" for EDD.
					$attempts_left_message = sprintf( __( ' %1$s', 'loginpress-pro' ),  $attempts_message ); // @codingStandardsIgnoreLine.
				} else {    // translators: Error msg for default forms.
					$attempts_left_message = sprintf( __( '%1$sERROR:%2$s %3$s', 'loginpress-pro' ), '<strong>', '</strong>', $attempts_message );
				}
			} else {
				/* Translators: The attempts. */
				$attempts_left_message = sprintf( __( '%1$sERROR:%2$s You have only %3$s attempts', 'loginpress-pro' ), '<strong>', '</strong>', $remains );

				// Check if the EDD class exists.
				if ( class_exists( 'Easy_Digital_Downloads' ) && isset( $_POST['edd_login_nonce'] ) ) {
					// translators: Modify the message without "ERROR" for EDD.
					$attempts_left_message = sprintf( __( 'You have only %3$s attempts remaining.', 'loginpress-pro' ), '<strong>', '</strong>', $remains );
				}
			}

			/**
			 * LoginPress limit Login Attempts Custom Error Message for the specific Attempt
			 *
			 * @param string $attempt_message The default Limit Login Attempts Error message.
			 * @param int    $count           The number of attempt from the user.
			 * @param int    $remaining       The remaining attempts of the users.
			 *
			 * @version 3.0.0
			 * @return array $llla_attempt_args the modified arguments.
			 */

			$llla_attempt_message = apply_filters( 'loginpress_attempt_error', $attempts_left_message, $count, $remains );
			if ( class_exists( 'Easy_Digital_Downloads' ) ) {
				if ( $remains >= 1 ) {
					edd_set_error( 'loginpress-pro', $llla_attempt_message );
				}
			}

			$allowed_html = array(
				'a'      => array(),
				'br'     => array(),
				'em'     => array(),
				'strong' => array(),
				'i'      => array(),
			);

			return wp_kses( $llla_attempt_message, $allowed_html );
		}

		/**
		 * Check the gateway.
		 *
		 * @return string
		 * @since  3.0.0
		 * @version 6.0.0
		 */
		public function gateway() {
			/**
			* Apply a filter to allow passing different login gateways (e.g., LifterLMS, LearnDash, etc.).
			*
			* @param string|bool $gateway Default is false. Should be replaced with custom gateway string.
			* @since 5.0.0
			*/
			$gateway_passed = apply_filters( 'loginpress_gateway_passed', false );
			$gateway        = '';
			if ( isset( $_POST['woocommerce-login-nonce'] ) ) {
				wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['woocommerce-login-nonce'] ) ), 'woocommerce-login' );
			}
			if ( isset( $_POST['woocommerce-login-nonce'] ) ) {
				$gateway = esc_html__( 'WooCommerce', 'loginpress-pro' );
			} elseif ( isset( $GLOBALS['wp_xmlrpc_server'] ) && is_object( $GLOBALS['wp_xmlrpc_server'] ) ) {
				$gateway = esc_html__( 'XMLRPC', 'loginpress-pro' );
			} elseif ( isset( $_POST['edd_login_nonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['edd_login_nonce'] ) ), 'edd-login' ) ) { // Check for EDD login form nonce
				$gateway = esc_html__( 'EDD Login', 'loginpress-pro' );
			} elseif ( $gateway_passed ) {
				$gateway = $gateway_passed;
			} elseif ( isset( $_GET['lpsl_login_id'] ) ) {
				$social_id = $_GET['lpsl_login_id'];
				if ( 'success_facebook' === $social_id ) {
					$gateway = esc_html__( 'Facebook Login', 'loginpress-pro' );
				} elseif ( 'success_twitter' === $social_id ) {
					$gateway = esc_html__( 'X(twitter) Login', 'loginpress-pro' );
				} elseif ( 'success_gplus' === $social_id ) {
					$gateway = esc_html__( 'Google Login', 'loginpress-pro' );
				} elseif ( 'success_linkedin' === $social_id ) {
					$gateway = esc_html__( 'Linkedin Login', 'loginpress-pro' );
				} elseif ( 'success_microsoft' === $social_id ) {
					$gateway = esc_html__( 'Microsoft Login', 'loginpress-pro' );
				} elseif ( 'success_apple' === $social_id ) {
					$gateway = esc_html__( 'Apple Login', 'loginpress-pro' );
				} elseif ( 'success_github' === $social_id ) {
					$gateway = esc_html__( 'Github Login', 'loginpress-pro' );
				} elseif ( 'success_discord' === $social_id ) {
					$gateway = esc_html__( 'Discord Login', 'loginpress-pro' );
				} elseif ( 'success_wordpress' === $social_id ) {
					$gateway = esc_html__( 'WordPress Login', 'loginpress-pro' );
				}
			} else {
				$gateway = esc_html__( 'WP Login', 'loginpress-pro' );
			}

			return $gateway;
		}

		/**
		 * Get correct remote address
		 *
		 * @param string $type_name The address type.
		 *
		 * @return string
		 * @since  3.1.1
		 */
		public function get_address( $type_name = '' ) {

			$ip_address = '';
			if ( isset( $_SERVER['HTTP_CLIENT_IP'] ) && ! empty( $_SERVER['HTTP_CLIENT_IP'] ) ) {
				$ip_address = sanitize_text_field( wp_unslash( $_SERVER['HTTP_CLIENT_IP'] ) );
			} elseif ( isset( $_SERVER['HTTP_X_FORWARDED_FOR'] ) && ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
				$ip_address = sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_FORWARDED_FOR'] ) );
			} elseif ( isset( $_SERVER['HTTP_X_FORWARDED'] ) && ! empty( $_SERVER['HTTP_X_FORWARDED'] ) ) {
				$ip_address = sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_FORWARDED'] ) );
			} elseif ( isset( $_SERVER['HTTP_FORWARDED_FOR'] ) && ! empty( $_SERVER['HTTP_FORWARDED_FOR'] ) ) {
				$ip_address = sanitize_text_field( wp_unslash( $_SERVER['HTTP_FORWARDED_FOR'] ) );
			} elseif ( isset( $_SERVER['HTTP_FORWARDED'] ) && ! empty( $_SERVER['HTTP_FORWARDED'] ) ) {
				$ip_address = sanitize_text_field( wp_unslash( $_SERVER['HTTP_FORWARDED'] ) );
			} elseif ( isset( $_SERVER['REMOTE_ADDR'] ) && ! empty( $_SERVER['REMOTE_ADDR'] ) ) {
				$ip_address = sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) );
			} else {
				$ip_address = 'UNKNOWN';
			}

			return $ip_address;
		}

		/**
		 * Send lockout notification email
		 *
		 * @param string $username The username that was locked out
		 * @param string $ip The IP address that was locked out
		 * @param string $gateway The gateway where the lockout occurred
		 * @since 6.0.0
		 */
		public function lp_send_lockout_notification( $username, $ip, $gateway ) {
			if ( ! isset( $this->attempts_settings['enable_lockout_notification'] ) ||
				$this->attempts_settings['enable_lockout_notification'] !== 'on' ) {
				return;
			}

			$email_addresses = isset( $this->attempts_settings['notification_email'] ) ?
				$this->attempts_settings['notification_email'] : '';

			if ( empty( $email_addresses ) ) {
				return;
			}

			// Validate and clean email addresses
			$emails = array_filter(
				array_map(
					function ( $email ) {
						$email = trim( $email );
						return is_email( $email ) ? $email : false;
					},
					explode( ',', $email_addresses )
				)
			);

			// Bail if no valid emails remain
			if ( empty( $emails ) ) {
				return;
			}
			$subject = isset( $this->attempts_settings['notification_subject'] ) ?
				$this->attempts_settings['notification_subject'] : '%username% is locked out at %sitename%';
			$body    = isset( $this->attempts_settings['notification_body'] ) ?
				$this->attempts_settings['notification_body'] : $this->lp_get_default_email_body();

			// Replace variables
			$replacements = array(
				'%sitename%' => get_bloginfo( 'name' ),
				'%username%' => $username,
				'%date%'     => date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ) ),
				'%ip%'       => $ip,
				'%gateway%'  => $gateway,
			);

			$subject = str_replace( array_keys( $replacements ), array_values( $replacements ), $subject );
			$body    = str_replace( array_keys( $replacements ), array_values( $replacements ), $body );

			// Add attempt details to the email body
			$body .= "\n\n" . $this->lp_get_attempt_details( $ip, $username );

			$headers = array( 'Content-Type: text/plain; charset=UTF-8' );

			wp_mail( implode( ',', $emails ), $subject, $body, $headers );
		}

		/**
		 * Get default email body
		 *
		 * @since 6.0.0
		 */
		private function lp_get_default_email_body() {
			return "Hello,\n\n" .
				"At your site %sitename%, a user: %username% was recently locked out from the IP: %ip%\n" .
				"on %date% while trying to log in through %gateway%.\n\n" .
				'Please visit your dashboard to unlock this user or add them to the blacklist.';
		}

		/**
		 * Get attempt details for the email
		 *
		 * @since 6.0.0
		 */
		private function lp_get_attempt_details( $ip, $username ) {
			global $wpdb;

			$attempts = $wpdb->get_results(
				$wpdb->prepare(
					"SELECT * FROM `{$this->llla_table}` WHERE `ip` = %s AND `username` = %s ORDER BY `datentime` DESC LIMIT 5",
					$ip,
					$username
				)
			);

			if ( empty( $attempts ) ) {
				return '';
			}

			$output  = "Attempt Details:\n";
			$output .= "----------------\n";

			foreach ( $attempts as $attempt ) {
				$output .= sprintf(
					"Date: %s\nGateway: %s\nIP: %s\nUsername: %s\n\n",
					date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $attempt->datentime ),
					$attempt->gateway,
					$attempt->ip,
					$attempt->username
				);
			}

			return $output;
		}

		/** Callback for `wp_login` action hook.
		 *
		 * @param string  $user_login The user login.
		 * @param WP_User $user The user object.
		 *
		 * @since 6.0.0
		 */
		public function llla_login_attempts_wp_login( $user_login, $user ) {
			if ( isset( $_GET['lpsl_login_id'] ) ) { // @codingStandardsIgnoreLine.

				$success = explode( '_', $_GET['lpsl_login_id'] ); // @codingStandardsIgnoreLine.
				if ( $success[0] == 'success' ) {

					$this->limit_query( $user_login, 1 );
					return '';
				}
			}
			$this->limit_query( $user_login, 1 );
		}


		/**
		 * Called on wp_login_failed action hook.
		 *
		 * @param string $username The username that was attempted to be logged in.
		 * @param object $error The error object from the login attempt.
		 * @since 6.0.0
		 */
		/**
		 * Detects bot behavior based on login failures.
		 *
		 * @param string   $username Attempted username.
		 * @param WP_Error $error    WP_Error object containing failure reason.
		 * @since 6.0.0
		 */
		public function llla_login_attempts_wp_login_failed( $username, $error ) {
			if ( ! isset( $this->attempts_settings['ip_intelligence'] ) || $this->attempts_settings['ip_intelligence'] === 'off' ) {
				return '';
			}
			global $wpdb;

			$ip         = $this->ip;
			$table_name = $wpdb->prefix . 'loginpress_limit_login_details';
			$user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : '';
			$headers    = $this->get_request_headers();

			$is_bot = false;

			// --------------------------------------------
			// 1. Analyze recent failed login timestamps
			// --------------------------------------------
			$recent_attempts = $wpdb->get_results(
				$wpdb->prepare(
					"SELECT datentime FROM $table_name 
					WHERE ip = %s AND login_status = 'Failed' 
					ORDER BY datentime DESC LIMIT 10",
					$ip
				)
			);

			if ( count( $recent_attempts ) >= 2 ) {
				$timestamps = array_reverse( array_map( 'floatval', wp_list_pluck( $recent_attempts, 'datentime' ) ) );
				$intervals  = array();

				for ( $i = 1; $i < count( $timestamps ); $i++ ) {
					$intervals[] = $timestamps[ $i ] - $timestamps[ $i - 1 ];
				}

				$fast       = array_filter( $intervals, fn( $i ) => $i < 0.3 );
				$consistent = false;

				if ( count( $intervals ) >= 3 ) {
					$avg        = array_sum( $intervals ) / count( $intervals );
					$consistent = array_reduce(
						$intervals,
						fn( $carry, $val ) => $carry && ( abs( $val - $avg ) / $avg < 0.1 ),
						true
					);
				}

				$duration = end( $timestamps ) - reset( $timestamps );

				if ( count( $fast ) >= 2 || $consistent || $duration < 10 ) {
					$is_bot = true;
				}
			}

			// --------------------------------------------
			// 2. Check User-Agent for headless bots
			// --------------------------------------------
			if ( empty( $user_agent ) || preg_match( '/(Headless|PhantomJS|python-requests|curl|bot|crawler|scrapy|Go-http-client)/i', $user_agent ) ) {
				$is_bot = true;
			}

			// --------------------------------------------
			// 3. Check for missing key headers (non-browser requests)
			// --------------------------------------------
			// $required_headers = [ 'accept', 'accept-language', 'referer' ];
			// foreach ( $required_headers as $header ) {
			// if ( empty( $headers[ $header ] ) ) {
			// $is_bot = true;
			// break;
			// }
			// }

			// --------------------------------------------
			// Block or blacklist bot
			// --------------------------------------------
			if ( $is_bot ) {
				$wpdb->query(
					$wpdb->prepare(
						"UPDATE $table_name SET blacklist = 1 WHERE ip = %s",
						$ip
					)
				);

				error_log( "Bot login attempt blocked and blacklisted: $ip | UA: $user_agent" );
			}
		}

		/**
		 * Get all request headers in lowercase keys.
		 *
		 * @return array
		 * @since 6.0.0
		 */
		private function get_request_headers() {
			$headers = array();

			foreach ( $_SERVER as $key => $value ) {
				if ( str_starts_with( $key, 'HTTP_' ) ) {
					$header_key             = strtolower( str_replace( '_', '-', substr( $key, 5 ) ) );
					$headers[ $header_key ] = $value;
				}
			}

			return $headers;
		}
	}

endif;

new LoginPress_Attempts();
