<?php
/**
 * Login protection for Security Pack.
 *
 * @package ArkHost_Security_Pack
 */

// Prevent direct access.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Login protection class.
 */
class ARKSP_Login_Protection {

	/**
	 * Custom login URL slug.
	 *
	 * @var string|null
	 */
	private $custom_login_slug = null;

	/**
	 * Cookie name for custom login access.
	 *
	 * @var string
	 */
	private $cookie_name = 'arksp_login_access';

	/**
	 * Whether the current request came through the custom login slug.
	 *
	 * @var bool
	 */
	private $is_custom_slug_request = false;

	/**
	 * Constructor.
	 */
	public function __construct() {
		// Failed login tracking.
		add_action( 'wp_login_failed', array( $this, 'handle_failed_login' ) );
		// Priority 10 runs BEFORE WordPress's authenticate (priority 20) to block locked-out IPs early.
		add_filter( 'authenticate', array( $this, 'check_lockout' ), 10, 3 );

		// Successful login tracking.
		add_action( 'wp_login', array( $this, 'handle_successful_login' ), 10, 2 );

		// Block locked out IPs from seeing the login form at all.
		// This runs on login_init which fires before the login form is displayed.
		add_action( 'login_init', array( $this, 'block_locked_out_on_login_page' ), 1 );

		// Honeypot field.
		// NOTE: Must be registered BEFORE handle_custom_login_slug_early() which
		// may load wp-login.php and exit, preventing any later hook registrations.
		if ( ARKSP_Plugin::get_setting( 'honeypot_enabled', true ) ) {
			add_action( 'login_form', array( $this, 'add_honeypot_field' ) );
			add_action( 'register_form', array( $this, 'add_honeypot_field' ) );
			add_filter( 'authenticate', array( $this, 'check_honeypot' ), 1, 3 );
			add_filter( 'registration_errors', array( $this, 'check_honeypot_registration' ), 10, 3 );
		}

		// Hide login error messages (prevents username enumeration).
		// NOTE: Must be registered BEFORE handle_custom_login_slug_early() which
		// may load wp-login.php and exit, preventing any later hook registrations.
		if ( ARKSP_Plugin::get_setting( 'hide_login_errors', true ) ) {
			// Replace specific auth errors at the source (runs after WP's authenticate at priority 20).
			add_filter( 'authenticate', array( $this, 'genericize_auth_error' ), PHP_INT_MAX, 3 );
			// Also filter the rendered error string as a safety net.
			add_filter( 'login_errors', array( $this, 'hide_login_errors' ), PHP_INT_MAX );
		}

		// Custom login URL - must run very early before WordPress processes the request.
		// WARNING: handle_custom_login_slug_early() may require wp-login.php and exit.
		// All login-related hooks MUST be registered above this point.
		if ( ARKSP_Plugin::get_setting( 'login_rename_enabled', false ) ) {
			$this->custom_login_slug = ARKSP_Plugin::get_setting( 'login_custom_url', '' );
			if ( ! empty( $this->custom_login_slug ) ) {
				// Register URL filters BEFORE handle_custom_login_slug_early() because
				// it loads wp-login.php and exits. Without these filters active, the
				// login form action URL points to wp-login.php instead of the custom
				// slug, causing the form POST to be blocked by should_block_direct_wp_login().
				add_action( 'init', array( $this, 'restrict_wp_login' ), 1 );
				add_filter( 'site_url', array( $this, 'filter_login_url' ), 10, 4 );
				add_filter( 'wp_redirect', array( $this, 'filter_redirect_url' ), 10, 2 );
				add_filter( 'login_url', array( $this, 'filter_login_url_direct' ), 10, 3 );

				// Handle custom login slug immediately (constructor runs during plugins_loaded).
				$this->handle_custom_login_slug_early();
			}
		}

		// Hide wp-admin for non-logged-in users.
		if ( ARKSP_Plugin::get_setting( 'hide_wp_admin', false ) ) {
			// Must run early - WordPress redirects wp-admin to wp-login.php before init.
			$this->block_wp_admin_early();
			add_action( 'init', array( $this, 'hide_wp_admin' ), 1 );
		}
	}

	/**
	 * Block locked out IPs from seeing the login page.
	 * Runs on login_init before the form is displayed.
	 */
	public function block_locked_out_on_login_page() {
		// Check if custom login URL is enabled and block direct wp-login.php access.
		if ( $this->should_block_direct_wp_login() ) {
			$this->redirect_to_404();
		}

		// Check lockout first.
		if ( $this->is_ip_locked_out_early() ) {
			$this->show_lockout_message_early();
		}

		// Check admin access restriction.
		if ( $this->is_admin_access_restricted() ) {
			$this->show_admin_access_denied_message();
		}
	}

	/**
	 * Check if direct wp-login.php access should be blocked.
	 *
	 * @return bool True if access should be blocked.
	 */
	private function should_block_direct_wp_login() {
		// If wp-login.php was loaded via the custom slug handler, never block.
		if ( $this->is_custom_slug_request ) {
			return false;
		}

		// Only applies when custom login URL is enabled.
		if ( ! ARKSP_Plugin::get_setting( 'login_rename_enabled', false ) ) {
			return false;
		}

		$custom_slug = ARKSP_Plugin::get_setting( 'login_custom_url', '' );
		if ( empty( $custom_slug ) ) {
			return false;
		}

		// Allow logged-in users (for logout, profile actions, etc.).
		$logged_in = false;
		foreach ( $_COOKIE as $name => $value ) {
			if ( strpos( $name, 'wordpress_logged_in_' ) === 0 ) {
				$logged_in = true;
				break;
			}
		}
		if ( $logged_in ) {
			return false;
		}

		// Allow if user has the access cookie (visited custom login URL before).
		// This allows all WordPress flows (password reset, etc.) to work normally.
		$expected_value = wp_hash( 'arksp_login_' . $custom_slug );
		$cookie_value   = isset( $_COOKIE['arksp_login_access'] ) ? sanitize_text_field( wp_unslash( $_COOKIE['arksp_login_access'] ) ) : '';

		if ( $cookie_value === $expected_value ) {
			return false;
		}

		// No valid cookie - block direct wp-login.php access.
		return true;
	}

	/**
	 * Check if admin access is restricted for current IP/country.
	 *
	 * @return bool True if access should be denied.
	 */
	private function is_admin_access_restricted() {
		if ( ! ARKSP_Plugin::get_setting( 'admin_access_restriction', false ) ) {
			return false;
		}

		$ip = ARKSP_Helper::get_client_ip();
		if ( ! $ip ) {
			return false;
		}

		// Check if IP is whitelisted (always allow).
		$ip_control = arksp()->get_component( 'ip_control' );
		if ( ! $ip_control ) {
			$ip_control = new ARKSP_IP_Control();
		}
		if ( $ip_control->is_whitelisted( $ip ) ) {
			return false;
		}

		$allowed_countries = ARKSP_Plugin::get_setting( 'admin_allowed_countries', array() );
		$allowed_ips       = ARKSP_Plugin::get_setting( 'admin_allowed_ips', '' );
		$allowed_ip_list   = ARKSP_Helper::parse_ip_list( $allowed_ips );

		$has_country_restriction = ! empty( $allowed_countries );
		$has_ip_restriction      = ! empty( $allowed_ip_list );

		// If no restrictions configured, allow access.
		if ( ! $has_country_restriction && ! $has_ip_restriction ) {
			return false;
		}

		$access_granted = false;

		// Check IP restriction.
		if ( $has_ip_restriction && ARKSP_Helper::ip_matches_rules( $ip, $allowed_ip_list ) ) {
			$access_granted = true;
		}

		// Check country restriction.
		if ( ! $access_granted && $has_country_restriction ) {
			$geo_blocking = arksp()->get_component( 'geo_blocking' );
			if ( ! $geo_blocking ) {
				$geo_blocking = new ARKSP_Geo_Blocking();
			}

			$country_code = $geo_blocking->get_country_code( $ip );
			if ( $country_code && in_array( $country_code, $allowed_countries, true ) ) {
				$access_granted = true;
			}
		}

		// If access not granted, it should be restricted.
		return ! $access_granted;
	}

	/**
	 * Show admin access denied message and exit.
	 */
	private function show_admin_access_denied_message() {
		$ip = ARKSP_Helper::get_client_ip();

		// Log the blocked access attempt.
		ARKSP_Activity_Log::log(
			ARKSP_Activity_Log::EVENT_IP_BLOCKED,
			$ip,
			null,
			__( 'Admin access denied: country/IP not allowed', 'arkhost-security-pack' )
		);

		status_header( 403 );
		nocache_headers();

		wp_die(
			esc_html__( 'Admin access is not permitted from your location.', 'arkhost-security-pack' ),
			esc_html__( 'Access Denied', 'arkhost-security-pack' ),
			array(
				'response'  => 403,
				'back_link' => false,
			)
		);
	}

	/**
	 * Handle failed login attempt.
	 *
	 * @param string $username Username attempted.
	 */
	public function handle_failed_login( $username ) {
		if ( ! ARKSP_Plugin::get_setting( 'login_limit_enabled', true ) ) {
			return;
		}

		$ip = ARKSP_Helper::get_client_ip();

		if ( ! $ip ) {
			return;
		}

		// Check if IP is whitelisted.
		$ip_control = arksp()->get_component( 'ip_control' );
		if ( $ip_control && $ip_control->is_whitelisted( $ip ) ) {
			ARKSP_Activity_Log::log( ARKSP_Activity_Log::EVENT_LOGIN_FAILED, $ip, $username, __( 'Failed login (whitelisted IP)', 'arkhost-security-pack' ) );
			return;
		}

		// Log the failed attempt.
		ARKSP_Activity_Log::log( ARKSP_Activity_Log::EVENT_LOGIN_FAILED, $ip, $username );

		// Increment failed attempts counter.
		$attempts = $this->increment_failed_attempts( $ip );

		// Check if lockout threshold reached.
		$max_attempts = ARKSP_Plugin::get_setting( 'login_max_attempts', 5 );

		if ( $attempts >= $max_attempts ) {
			$this->lockout_ip( $ip );
		}

		// Send email alert if enabled.
		$this->maybe_send_alert( 'failed_login', $ip, $username, $attempts );
	}

	/**
	 * Handle successful login.
	 *
	 * @param string  $user_login Username.
	 * @param WP_User $user       User object.
	 */
	public function handle_successful_login( $user_login, $user ) {
		$ip = ARKSP_Helper::get_client_ip();

		// Log the successful login.
		ARKSP_Activity_Log::log( ARKSP_Activity_Log::EVENT_LOGIN_SUCCESS, $ip, $user_login );

		// Clear failed attempts for this IP.
		$this->clear_failed_attempts( $ip );

		// Send admin login notification if enabled.
		if ( ARKSP_Plugin::get_setting( 'admin_login_notify', false ) && user_can( $user, 'manage_options' ) ) {
			$this->send_admin_login_notification( $user, $ip );
		}
	}

	/**
	 * Send notification when an admin logs in.
	 *
	 * @param WP_User $user User object.
	 * @param string  $ip   IP address.
	 */
	private function send_admin_login_notification( $user, $ip ) {
		$email = ARKSP_Plugin::get_setting( 'email_alerts_address', get_option( 'admin_email' ) );
		if ( empty( $email ) ) {
			return;
		}

		// Check if this is a new IP for this user.
		$known_ips = get_user_meta( $user->ID, '_arksp_known_ips', true );
		if ( ! is_array( $known_ips ) ) {
			$known_ips = array();
		}

		// Only notify for new IPs (or always if no IPs recorded yet).
		$is_new_ip = ! in_array( $ip, $known_ips, true );

		// Add IP to known list (keep last 10).
		if ( $is_new_ip ) {
			$known_ips[] = $ip;
			$known_ips   = array_slice( $known_ips, -10 );
			update_user_meta( $user->ID, '_arksp_known_ips', $known_ips );
		}

		// Only send notification for new IPs.
		if ( ! $is_new_ip ) {
			return;
		}

		$site_name = get_bloginfo( 'name' );
		$subject   = sprintf(
			/* translators: %s: Site name */
			__( '[%s] Admin Login from New IP', 'arkhost-security-pack' ),
			$site_name
		);

		$message = sprintf(
			/* translators: 1: Username, 2: Site name */
			__( 'An administrator account logged in to %2$s from a new IP address.', 'arkhost-security-pack' ),
			$user->user_login,
			$site_name
		) . "\n\n";

		/* translators: %s: Username */
		$message .= sprintf( __( 'Username: %s', 'arkhost-security-pack' ), $user->user_login ) . "\n";
		/* translators: %s: IP address */
		$message .= sprintf( __( 'IP Address: %s', 'arkhost-security-pack' ), $ip ) . "\n";
		/* translators: %s: Login time */
		$message .= sprintf( __( 'Time: %s', 'arkhost-security-pack' ), current_time( 'mysql' ) ) . "\n\n";
		$message .= __( 'If this was not you, please secure your account immediately.', 'arkhost-security-pack' ) . "\n";

		wp_mail( $email, $subject, $message );
	}

	/**
	 * Hide specific login error messages to prevent username enumeration.
	 *
	 * @param string $error Error message.
	 * @return string
	 */
	public function hide_login_errors( $error ) {
		// Return generic message instead of revealing if username or password was wrong.
		return __( 'Invalid username or password.', 'arkhost-security-pack' );
	}

	/**
	 * Replace specific authentication errors with a generic message.
	 *
	 * Runs at very late priority on the authenticate filter to catch WordPress's
	 * own specific error messages (invalid_username, incorrect_password, etc.)
	 * and replace them before they reach the login page rendering.
	 *
	 * @param WP_User|WP_Error|null $user     User object or error.
	 * @param string                $username Username.
	 * @param string                $password Password.
	 * @return WP_User|WP_Error|null
	 */
	public function genericize_auth_error( $user, $username, $password ) {
		if ( is_wp_error( $user ) ) {
			$specific_codes = array( 'invalid_username', 'invalid_email', 'incorrect_password', 'invalidcombo' );

			if ( in_array( $user->get_error_code(), $specific_codes, true ) ) {
				return new WP_Error(
					'authentication_failed',
					__( 'Invalid username or password.', 'arkhost-security-pack' )
				);
			}
		}

		return $user;
	}

	/**
	 * Check if IP is locked out before authentication.
	 *
	 * @param WP_User|WP_Error|null $user     User object or error.
	 * @param string                $username Username.
	 * @param string                $password Password.
	 * @return WP_User|WP_Error|null
	 */
	public function check_lockout( $user, $username, $password ) {
		if ( ! ARKSP_Plugin::get_setting( 'login_limit_enabled', true ) ) {
			return $user;
		}

		if ( empty( $username ) ) {
			return $user;
		}

		$ip = ARKSP_Helper::get_client_ip();

		if ( ! $ip ) {
			return $user;
		}

		// Check if IP is whitelisted.
		$ip_control = arksp()->get_component( 'ip_control' );
		if ( $ip_control && $ip_control->is_whitelisted( $ip ) ) {
			return $user;
		}

		// Check if IP is locked out.
		$lockout = $this->get_lockout( $ip );
		$max_attempts = (int) ARKSP_Plugin::get_setting( 'login_max_attempts', 5 );
		$lockout_duration = (int) ARKSP_Plugin::get_setting( 'login_lockout_duration', 15 );

		if ( $lockout && (int) $lockout->failed_attempts >= $max_attempts ) {
			// Calculate lockout expiry from updated_at + duration.
			// Use current_time('timestamp') for WordPress timezone consistency.
			$updated_time    = strtotime( $lockout->updated_at );
			$lockout_expires = $updated_time + ( $lockout_duration * 60 );
			$current_time    = current_time( 'timestamp' );

			if ( $current_time < $lockout_expires ) {
				// Still locked out.
				$remaining = human_time_diff( $current_time, $lockout_expires );

				return new WP_Error(
					'arksp_locked_out',
					sprintf(
						/* translators: %s: Time remaining */
						__( 'Too many failed login attempts. Please try again in %s.', 'arkhost-security-pack' ),
						$remaining
					)
				);
			} else {
				// Lockout expired - clear the record so they can try again.
				$this->clear_failed_attempts( $ip );
			}
		}

		return $user;
	}

	/**
	 * Increment failed attempts counter.
	 *
	 * @param string $ip IP address.
	 * @return int New count.
	 */
	private function increment_failed_attempts( $ip ) {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time.
		$existing = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT * FROM {$wpdb->prefix}arksp_lockouts WHERE ip_address = %s",
				$ip
			)
		);

		if ( $existing ) {
			$new_count = $existing->failed_attempts + 1;

			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time.
			$wpdb->update(
				$wpdb->prefix . 'arksp_lockouts',
				array(
					'failed_attempts' => $new_count,
					'updated_at'      => current_time( 'mysql' ),
				),
				array( 'ip_address' => $ip ),
				array( '%d', '%s' ),
				array( '%s' )
			);

			return $new_count;
		}

		// Create new record.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time.
		$wpdb->insert(
			$wpdb->prefix . 'arksp_lockouts',
			array(
				'ip_address'      => $ip,
				'failed_attempts' => 1,
				'created_at'      => current_time( 'mysql' ),
			),
			array( '%s', '%d', '%s' )
		);

		return 1;
	}

	/**
	 * Clear failed attempts for an IP.
	 *
	 * @param string $ip IP address.
	 */
	private function clear_failed_attempts( $ip ) {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time.
		$wpdb->delete(
			$wpdb->prefix . 'arksp_lockouts',
			array( 'ip_address' => $ip ),
			array( '%s' )
		);
	}

	/**
	 * Lockout an IP address.
	 *
	 * @param string $ip IP address.
	 */
	private function lockout_ip( $ip ) {
		global $wpdb;

		$duration = (int) ARKSP_Plugin::get_setting( 'login_lockout_duration', 15 );
		// Store Unix timestamp to avoid timezone issues.
		$lockout_until = time() + ( $duration * 60 );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time.
		$wpdb->update(
			$wpdb->prefix . 'arksp_lockouts',
			array(
				'lockout_until' => $lockout_until,
				'updated_at'    => current_time( 'mysql' ),
			),
			array( 'ip_address' => $ip ),
			array( '%d', '%s' ),
			array( '%s' )
		);

		// Log the lockout.
		ARKSP_Activity_Log::log(
			ARKSP_Activity_Log::EVENT_LOCKOUT,
			$ip,
			null,
			sprintf(
				/* translators: %d: Duration in minutes */
				__( 'Locked out for %d minutes', 'arkhost-security-pack' ),
				$duration
			)
		);

		// Send email alert.
		$this->maybe_send_alert( 'lockout', $ip );

		// Check for auto-blacklist of repeat offenders.
		$this->maybe_auto_blacklist( $ip );
	}

	/**
	 * Check if IP should be auto-blacklisted for repeat lockouts.
	 *
	 * @param string $ip IP address.
	 */
	private function maybe_auto_blacklist( $ip ) {
		if ( ! ARKSP_Plugin::get_setting( 'auto_blacklist_enabled', false ) ) {
			return;
		}

		$threshold = (int) ARKSP_Plugin::get_setting( 'auto_blacklist_threshold', 3 );
		if ( $threshold < 1 ) {
			return;
		}

		// Count lockouts for this IP in the activity log.
		$lockout_count = $this->count_ip_lockouts( $ip );

		if ( $lockout_count >= $threshold ) {
			$this->add_ip_to_blacklist( $ip );

			// Log the auto-blacklist.
			ARKSP_Activity_Log::log(
				ARKSP_Activity_Log::EVENT_IP_BLOCKED,
				$ip,
				null,
				sprintf(
					/* translators: %d: Number of lockouts */
					__( 'Auto-blacklisted after %d lockouts', 'arkhost-security-pack' ),
					$lockout_count
				)
			);

			// Send email alert.
			$this->maybe_send_alert( 'auto_blacklist', $ip, $lockout_count );
		}
	}

	/**
	 * Count the number of lockouts for an IP address.
	 *
	 * @param string $ip IP address.
	 * @return int
	 */
	private function count_ip_lockouts( $ip ) {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time.
		$count = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT COUNT(*) FROM {$wpdb->prefix}arksp_activity_log WHERE ip_address = %s AND event_type = %s",
				$ip,
				ARKSP_Activity_Log::EVENT_LOCKOUT
			)
		);

		return (int) $count;
	}

	/**
	 * Add an IP address to the blacklist.
	 *
	 * @param string $ip IP address.
	 */
	private function add_ip_to_blacklist( $ip ) {
		$current_blacklist = ARKSP_Plugin::get_setting( 'ip_blacklist', '' );

		// Check if IP is already in the blacklist.
		$blacklist_array = array_filter( array_map( 'trim', explode( "\n", $current_blacklist ) ) );
		if ( in_array( $ip, $blacklist_array, true ) ) {
			return;
		}

		// Add IP to blacklist.
		$blacklist_array[] = $ip;
		$new_blacklist     = implode( "\n", $blacklist_array );

		ARKSP_Plugin::update_setting( 'ip_blacklist', $new_blacklist );

		// Clear the IP control cache.
		$ip_control = arksp()->get_component( 'ip_control' );
		if ( $ip_control ) {
			$ip_control->clear_cache();
		}
	}

	/**
	 * Get lockout record for an IP.
	 *
	 * @param string $ip IP address.
	 * @return object|null
	 */
	private function get_lockout( $ip ) {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time.
		return $wpdb->get_row(
			$wpdb->prepare(
				"SELECT * FROM {$wpdb->prefix}arksp_lockouts WHERE ip_address = %s",
				$ip
			)
		);
	}

	/**
	 * Block wp-admin access early - called from constructor.
	 * Runs before WordPress can redirect to wp-login.php.
	 */
	private function block_wp_admin_early() {
		// Check if user is logged in - we can't use is_user_logged_in() this early.
		// Check for the logged_in cookie instead.
		$logged_in = false;
		foreach ( $_COOKIE as $name => $value ) {
			if ( strpos( $name, 'wordpress_logged_in_' ) === 0 ) {
				$logged_in = true;
				break;
			}
		}

		if ( $logged_in ) {
			return;
		}

		// Check if requesting wp-admin.
		$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
		$is_wp_admin = strpos( $request_uri, '/wp-admin' ) !== false;
		$is_ajax     = strpos( $request_uri, 'admin-ajax.php' ) !== false;

		if ( $is_wp_admin && ! $is_ajax ) {
			// Redirect to a non-existent URL to trigger WordPress's themed 404 page.
			$home = get_option( 'home', '' );
			$fake_url = rtrim( $home, '/' ) . '/arksp-404-' . wp_rand( 1000, 9999 );
			header( 'Location: ' . $fake_url, true, 302 );
			exit;
		}
	}

	/**
	 * Handle access to the custom login slug - called early from constructor.
	 * Directly loads wp-login.php when accessed via custom slug.
	 */
	public function handle_custom_login_slug_early() {
		if ( ! $this->is_custom_login_slug_request_early() ) {
			return;
		}

		// Check if IP is locked out before showing login form.
		if ( $this->is_ip_locked_out_early() ) {
			$this->show_lockout_message_early();
		}

		// Check admin access restriction before showing login form.
		if ( $this->is_admin_access_restricted() ) {
			$this->show_admin_access_denied_message();
		}

		// Set a flag that user accessed via custom login slug.
		// This will be checked by restrict_wp_login on subsequent requests.
		$cookie_value = wp_hash( 'arksp_login_' . $this->custom_login_slug );

		setcookie(
			$this->cookie_name,
			$cookie_value,
			time() + DAY_IN_SECONDS,
			defined( 'COOKIEPATH' ) ? COOKIEPATH : '/',
			defined( 'COOKIE_DOMAIN' ) ? COOKIE_DOMAIN : '',
			is_ssl(),
			true
		);

		// Also set it in the superglobal for immediate availability.
		$_COOKIE[ $this->cookie_name ] = $cookie_value;

		// Flag that this request came through the custom slug so login_init
		// won't block it in should_block_direct_wp_login().
		$this->is_custom_slug_request = true;

		// Directly load the login page (like WPS Hide Login does).
		require_once ABSPATH . 'wp-login.php';
		exit;
	}

	/**
	 * Check if IP is locked out - early version that works before init.
	 *
	 * @return bool
	 */
	private function is_ip_locked_out_early() {
		if ( ! ARKSP_Plugin::get_setting( 'login_limit_enabled', true ) ) {
			return false;
		}

		$ip = ARKSP_Helper::get_client_ip();
		if ( ! $ip ) {
			return false;
		}

		// Check whitelist first.
		$ip_control = arksp()->get_component( 'ip_control' );
		if ( $ip_control && $ip_control->is_whitelisted( $ip ) ) {
			return false;
		}

		// Check lockout status directly from database.
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time.
		$lockout = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT * FROM {$wpdb->prefix}arksp_lockouts WHERE ip_address = %s",
				$ip
			)
		);

		if ( ! $lockout ) {
			return false;
		}

		$max_attempts    = (int) ARKSP_Plugin::get_setting( 'login_max_attempts', 5 );
		$lockout_minutes = (int) ARKSP_Plugin::get_setting( 'login_lockout_duration', 15 );
		$current_time    = time();

		// Check if IP has reached max attempts and is still within lockout window.
		if ( (int) $lockout->failed_attempts >= $max_attempts && ! empty( $lockout->updated_at ) ) {
			$updated_time    = strtotime( $lockout->updated_at );
			$lockout_expires = $updated_time + ( $lockout_minutes * 60 );

			if ( $current_time < $lockout_expires ) {
				return true;
			}
		}

		// Also check lockout_until for honeypot/manual blocks.
		if ( ! empty( $lockout->lockout_until ) && is_numeric( $lockout->lockout_until ) ) {
			if ( $current_time < (int) $lockout->lockout_until ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Show lockout message and exit - early version.
	 */
	private function show_lockout_message_early() {
		status_header( 403 );
		nocache_headers();

		wp_die(
			sprintf(
				/* translators: %d: Lockout duration in minutes */
				esc_html__( 'Too many failed login attempts. Please try again in %d minutes.', 'arkhost-security-pack' ),
				absint( ARKSP_Plugin::get_setting( 'login_lockout_duration', 15 ) )
			),
			esc_html__( 'Access Denied', 'arkhost-security-pack' ),
			array(
				'response'  => 403,
				'back_link' => false,
			)
		);
	}

	/**
	 * Check if the current request is for the custom login slug - early version.
	 * Uses native PHP functions to avoid dependency on WordPress functions.
	 *
	 * @return bool
	 */
	private function is_custom_login_slug_request_early() {
		$request_uri  = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
		$request_path = trim( wp_parse_url( $request_uri, PHP_URL_PATH ), '/' );

		// Get the site path for subdirectory installations.
		// Use get_option directly since home_url() might have filters.
		$home         = get_option( 'home', '' );
		$home_path    = trim( wp_parse_url( $home, PHP_URL_PATH ), '/' );

		// Remove the home path prefix from the request path.
		if ( ! empty( $home_path ) && strpos( $request_path, $home_path ) === 0 ) {
			$request_path = trim( substr( $request_path, strlen( $home_path ) ), '/' );
		}

		return $request_path === $this->custom_login_slug;
	}

	/**
	 * Restrict direct access to wp-login.php.
	 * Block unless user has the access cookie from custom login slug.
	 */
	public function restrict_wp_login() {
		// If wp-login.php was loaded via the custom slug handler, never block.
		if ( $this->is_custom_slug_request ) {
			return;
		}

		// Check if we're on wp-login.php using the reliable $pagenow global.
		if ( ! isset( $GLOBALS['pagenow'] ) || 'wp-login.php' !== $GLOBALS['pagenow'] ) {
			return;
		}

		// Allow logged-in users.
		if ( is_user_logged_in() ) {
			return;
		}

		// Allow if user has the access cookie (came through custom login slug before).
		$expected_value = wp_hash( 'arksp_login_' . $this->custom_login_slug );
		$cookie_value   = isset( $_COOKIE[ $this->cookie_name ] ) ? sanitize_text_field( wp_unslash( $_COOKIE[ $this->cookie_name ] ) ) : '';

		if ( $cookie_value === $expected_value ) {
			return; // Cookie is valid, allow access.
		}

		// No valid cookie - block access with 404.
		$this->redirect_to_404();
	}

	/**
	 * Check if the current request is for the custom login slug.
	 *
	 * @return bool
	 */
	private function is_custom_login_slug_request() {
		$request_uri  = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
		$request_path = trim( wp_parse_url( $request_uri, PHP_URL_PATH ), '/' );

		// Get the site path for subdirectory installations.
		$home_path = trim( wp_parse_url( home_url(), PHP_URL_PATH ), '/' );

		// Remove the home path prefix from the request path.
		if ( ! empty( $home_path ) && strpos( $request_path, $home_path ) === 0 ) {
			$request_path = trim( substr( $request_path, strlen( $home_path ) ), '/' );
		}

		return $request_path === $this->custom_login_slug;
	}

	/**
	 * Redirect to 404 page.
	 */
	private function redirect_to_404() {
		// Try to get the site's 404 page URL.
		$home_url = home_url( '/' );

		// Use a non-existent URL to trigger WordPress 404.
		$fake_404_url = home_url( '/arksp-not-found-' . wp_rand( 1000, 9999 ) . '/' );

		status_header( 404 );
		nocache_headers();

		wp_safe_redirect( $fake_404_url, 302 );
		exit;
	}

	/**
	 * Hide wp-admin for non-logged-in users.
	 */
	public function hide_wp_admin() {
		if ( is_user_logged_in() ) {
			return;
		}

		$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';

		// Check if trying to access wp-admin (works for both root and subdirectory installs).
		// Allow admin-ajax.php for frontend AJAX functionality.
		$is_wp_admin = strpos( $request_uri, '/wp-admin' ) !== false;
		$is_ajax     = strpos( $request_uri, 'admin-ajax.php' ) !== false;

		if ( $is_wp_admin && ! $is_ajax ) {
			$this->redirect_to_404();
		}
	}

	/**
	 * Filter login URL in site_url.
	 *
	 * @param string      $url     The URL.
	 * @param string      $path    The path.
	 * @param string|null $scheme  The scheme.
	 * @param int|null    $blog_id Blog ID.
	 * @return string
	 */
	public function filter_login_url( $url, $path, $scheme, $blog_id ) {
		if ( strpos( $url, 'wp-login.php' ) !== false && ! empty( $this->custom_login_slug ) ) {
			$url = str_replace( 'wp-login.php', $this->custom_login_slug, $url );
		}
		return $url;
	}

	/**
	 * Filter login URL directly.
	 *
	 * @param string $login_url    The login URL.
	 * @param string $redirect     The redirect URL.
	 * @param bool   $force_reauth Force reauth.
	 * @return string
	 */
	public function filter_login_url_direct( $login_url, $redirect, $force_reauth ) {
		if ( ! empty( $this->custom_login_slug ) ) {
			$login_url = str_replace( 'wp-login.php', $this->custom_login_slug, $login_url );
		}
		return $login_url;
	}

	/**
	 * Filter redirect URLs.
	 *
	 * @param string $location Redirect location.
	 * @param int    $status   HTTP status.
	 * @return string
	 */
	public function filter_redirect_url( $location, $status ) {
		if ( strpos( $location, 'wp-login.php' ) !== false && ! empty( $this->custom_login_slug ) ) {
			// Check the redirect_to parameter in the location URL.
			$redirect_to = '';
			if ( preg_match( '/redirect_to=([^&]+)/', $location, $matches ) ) {
				$redirect_to = urldecode( $matches[1] );
			}

			// If redirect_to contains wp-admin and hide_wp_admin is enabled, block with 404.
			// This prevents /wp-admin from revealing the custom login URL.
			// Use get_option directly to avoid any potential issues with class method.
			$settings             = get_option( 'arksp_settings', array() );
			$hide_wp_admin        = ! empty( $settings['hide_wp_admin'] );
			$is_wp_admin_redirect = strpos( $redirect_to, 'wp-admin' ) !== false && strpos( $redirect_to, 'admin-ajax.php' ) === false;

			if ( $hide_wp_admin && $is_wp_admin_redirect ) {
				// Block with 404 - don't reveal the custom login URL.
				status_header( 404 );
				nocache_headers();
				// Exit immediately to prevent the redirect.
				exit;
			}

			$location = str_replace( 'wp-login.php', $this->custom_login_slug, $location );
		}
		return $location;
	}

	/**
	 * Show 404 page.
	 */
	private function show_404() {
		global $wp_query;

		status_header( 404 );
		nocache_headers();

		if ( $wp_query ) {
			$wp_query->set_404();
		}

		// Try to load theme 404 template.
		$template = get_404_template();
		if ( $template ) {
			include $template;
		} else {
			wp_die(
				esc_html__( 'Page not found.', 'arkhost-security-pack' ),
				esc_html__( '404 Not Found', 'arkhost-security-pack' ),
				array( 'response' => 404 )
			);
		}

		exit;
	}

	/**
	 * Add honeypot field to login form.
	 */
	public function add_honeypot_field() {
		// Honeypot field - hidden via CSS, bots will fill it.
		?>
		<p class="arksp-hp-field" style="position:absolute;left:-9999px;top:-9999px;">
			<label for="arksp_hp_email"><?php esc_html_e( 'Leave this field empty', 'arkhost-security-pack' ); ?></label>
			<input type="text" name="arksp_hp_email" id="arksp_hp_email" value="" tabindex="-1" autocomplete="off" />
		</p>
		<?php
	}

	/**
	 * Check honeypot field during authentication.
	 *
	 * @param WP_User|WP_Error|null $user     User object or error.
	 * @param string                $username Username.
	 * @param string                $password Password.
	 * @return WP_User|WP_Error|null
	 */
	public function check_honeypot( $user, $username, $password ) {
		// phpcs:ignore WordPress.Security.NonceVerification.Missing
		if ( ! empty( $_POST['arksp_hp_email'] ) ) {
			$ip = ARKSP_Helper::get_client_ip();

			ARKSP_Activity_Log::log(
				ARKSP_Activity_Log::EVENT_LOGIN_FAILED,
				$ip,
				$username,
				__( 'Honeypot triggered', 'arkhost-security-pack' )
			);

			// Auto-block this IP using configurable duration.
			$ip_control   = arksp()->get_component( 'ip_control' );
			$ban_duration = (int) ARKSP_Plugin::get_setting( 'honeypot_ban_duration', 60 );
			if ( $ip_control ) {
				$ip_control->auto_block_ip( $ip, $ban_duration, __( 'Honeypot triggered', 'arkhost-security-pack' ) );
			}

			return new WP_Error(
				'arksp_honeypot',
				__( 'Authentication failed.', 'arkhost-security-pack' )
			);
		}

		return $user;
	}

	/**
	 * Check honeypot field during registration.
	 *
	 * @param WP_Error $errors              Registration errors.
	 * @param string   $sanitized_user_login User login.
	 * @param string   $user_email          User email.
	 * @return WP_Error
	 */
	public function check_honeypot_registration( $errors, $sanitized_user_login, $user_email ) {
		// phpcs:ignore WordPress.Security.NonceVerification.Missing
		if ( ! empty( $_POST['arksp_hp_email'] ) ) {
			$errors->add(
				'arksp_honeypot',
				__( 'Registration failed.', 'arkhost-security-pack' )
			);

			// Auto-block this IP using configurable duration.
			$ip           = ARKSP_Helper::get_client_ip();
			$ip_control   = arksp()->get_component( 'ip_control' );
			$ban_duration = (int) ARKSP_Plugin::get_setting( 'honeypot_ban_duration', 60 );
			if ( $ip_control && $ip ) {
				$ip_control->auto_block_ip( $ip, $ban_duration, __( 'Honeypot triggered on registration', 'arkhost-security-pack' ) );
			}
		}

		return $errors;
	}

	/**
	 * Maybe send email alert.
	 *
	 * @param string $type     Alert type.
	 * @param string $ip       IP address.
	 * @param string $username Username (optional).
	 * @param int    $attempts Number of attempts (optional).
	 */
	private function maybe_send_alert( $type, $ip, $username = '', $attempts = 0 ) {
		if ( ! ARKSP_Plugin::get_setting( 'email_alerts_enabled', false ) ) {
			return;
		}

		$email = ARKSP_Plugin::get_setting( 'email_alerts_address', get_option( 'admin_email' ) );
		if ( empty( $email ) ) {
			return;
		}

		$site_name = get_bloginfo( 'name' );
		$site_url  = home_url();

		switch ( $type ) {
			case 'failed_login':
				$threshold = ARKSP_Plugin::get_setting( 'email_alert_threshold', 3 );
				if ( $attempts < $threshold ) {
					return;
				}

				$subject = sprintf(
					/* translators: %s: Site name */
					__( '[%s] Multiple Failed Login Attempts', 'arkhost-security-pack' ),
					$site_name
				);

				$message = sprintf(
					/* translators: 1: Number of attempts, 2: IP address, 3: Username, 4: Site URL */
					__( "There have been %1\$d failed login attempts on your site.\n\nIP Address: %2\$s\nUsername attempted: %3\$s\nSite: %4\$s\n\nIf this wasn't you, the IP will be automatically locked out after reaching the threshold.", 'arkhost-security-pack' ),
					$attempts,
					$ip,
					$username ? $username : __( '(empty)', 'arkhost-security-pack' ),
					$site_url
				);
				break;

			case 'lockout':
				$subject = sprintf(
					/* translators: %s: Site name */
					__( '[%s] IP Address Locked Out', 'arkhost-security-pack' ),
					$site_name
				);

				$message = sprintf(
					/* translators: 1: IP address, 2: Site URL */
					__( "An IP address has been locked out due to too many failed login attempts.\n\nIP Address: %1\$s\nSite: %2\$s", 'arkhost-security-pack' ),
					$ip,
					$site_url
				);
				break;

			case 'auto_blacklist':
				$subject = sprintf(
					/* translators: %s: Site name */
					__( '[%s] IP Permanently Blacklisted', 'arkhost-security-pack' ),
					$site_name
				);

				$message = sprintf(
					/* translators: 1: IP address, 2: Number of lockouts, 3: Site URL */
					__( "An IP address has been permanently added to your blacklist due to repeated lockouts.\n\nIP Address: %1\$s\nTotal Lockouts: %2\$d\nSite: %3\$s\n\nThis IP will no longer be able to access your website.", 'arkhost-security-pack' ),
					$ip,
					$attempts,
					$site_url
				);
				break;

			default:
				return;
		}

		wp_mail( $email, $subject, $message );
	}
}
