<?php
/**
 * User Enumeration Protection for Guard Dog.
 *
 * Protects against user enumeration attacks through REST API, author archives,
 * login errors, password reset, XML-RPC, oEmbed, and registration.
 *
 * @package GuardDog
 * @since 1.9.11
 */

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

class Guard_Dog_User_Enumeration {

    /**
     * Option keys for settings
     */
    const OPTION_ENABLED = 'guard_dog_enum_enabled';
    const OPTION_BYPASS_ROLES = 'guard_dog_enum_bypass_roles';
    const OPTION_AUTO_BLOCK_ENABLED = 'guard_dog_enum_auto_block_enabled';
    const OPTION_AUTO_BLOCK_THRESHOLD = 'guard_dog_enum_auto_block_threshold';
    const OPTION_AUTO_BLOCK_WINDOW = 'guard_dog_enum_auto_block_window';
    const OPTION_AUTO_BLOCK_DURATION = 'guard_dog_enum_auto_block_duration';

    // Per-vector blocking options
    const OPTION_REST_API_BLOCK = 'guard_dog_enum_rest_api_block';
    const OPTION_AUTHOR_ARCHIVES_BLOCK = 'guard_dog_enum_author_archives_block';
    const OPTION_LOGIN_ERRORS_BLOCK = 'guard_dog_enum_login_errors_block';
    const OPTION_PASSWORD_RESET_BLOCK = 'guard_dog_enum_password_reset_block';
    const OPTION_XMLRPC_BLOCK = 'guard_dog_enum_xmlrpc_block';
    const OPTION_OEMBED_BLOCK = 'guard_dog_enum_oembed_block';
    const OPTION_REGISTRATION_BLOCK = 'guard_dog_enum_registration_block';

    // Notification options
    const OPTION_ALERT_IMMEDIATE_ENABLED = 'guard_dog_enum_alert_immediate_enabled';
    const OPTION_ALERT_IMMEDIATE_THRESHOLD = 'guard_dog_enum_alert_immediate_threshold';
    const OPTION_ALERT_IMMEDIATE_WINDOW = 'guard_dog_enum_alert_immediate_window';
    const OPTION_ALERT_IMMEDIATE_EMAIL = 'guard_dog_enum_alert_immediate_email';
    const OPTION_ALERT_IMMEDIATE_COOLDOWN = 'guard_dog_enum_alert_immediate_cooldown';
    const OPTION_ALERT_DIGEST_ENABLED = 'guard_dog_enum_alert_digest_enabled';
    const OPTION_ALERT_DIGEST_FREQUENCY = 'guard_dog_enum_alert_digest_frequency';
    const OPTION_ALERT_DIGEST_THRESHOLD = 'guard_dog_enum_alert_digest_threshold';
    const OPTION_ALERT_DIGEST_EMAIL = 'guard_dog_enum_alert_digest_email';

    /**
     * Activity log instance
     *
     * @var Guard_Dog_Activity_Log|null
     */
    private $activity_log;

    /**
     * Track if we've shown generic password reset message
     *
     * @var bool
     */
    private $password_reset_generic_shown = false;

    /**
     * Enumeration vectors
     *
     * @var array
     */
    private static $vectors = [
        'rest_api' => 'REST API Users',
        'author_archive' => 'Author Archives',
        'login_error' => 'Login Errors',
        'password_reset' => 'Password Reset',
        'xmlrpc' => 'XML-RPC',
        'oembed' => 'oEmbed',
        'registration' => 'Registration',
    ];

    /**
     * Constructor
     *
     * @param Guard_Dog_Activity_Log|null $activity_log Activity log instance
     */
    public function __construct($activity_log = null) {
        $this->activity_log = $activity_log;

        if ($this->is_enabled()) {
            $this->init_hooks();
        }

        // Always init cron for digest emails
        $this->init_cron();
    }

    /**
     * Check if feature is enabled
     *
     * @return bool
     */
    public function is_enabled(): bool {
        return (bool) get_option(self::OPTION_ENABLED, false);
    }

    /**
     * Initialize WordPress hooks
     */
    private function init_hooks(): void {
        // REST API protection
        add_filter('rest_pre_dispatch', [$this, 'handle_rest_api_request'], 10, 3);

        // Author archive protection
        add_action('template_redirect', [$this, 'handle_author_archive_redirect'], 1);

        // Login error protection (only if not handled by Guard_Dog_Login_Errors)
        add_filter('authenticate', [$this, 'handle_login_errors'], 30, 3);

        // Password reset protection
        add_action('lostpassword_post', [$this, 'handle_password_reset_request'], 10, 2);
        add_filter('retrieve_password_message', [$this, 'handle_password_reset_message'], 10, 4);

        // Additional password reset protection - override error display
        add_filter('lostpassword_errors', [$this, 'handle_lostpassword_errors'], 99, 2);

        // XML-RPC protection - remove methods and log actual calls
        add_filter('xmlrpc_methods', [$this, 'handle_xmlrpc_methods'], 10, 1);
        add_action('xmlrpc_call', [$this, 'handle_xmlrpc_call'], 10, 1);

        // oEmbed protection
        add_filter('oembed_response_data', [$this, 'handle_oembed_response'], 10, 4);

        // Registration protection
        add_filter('registration_errors', [$this, 'handle_registration_errors'], 10, 3);
    }

    /**
     * Initialize cron jobs for digest emails
     */
    private function init_cron(): void {
        if (!wp_next_scheduled('guard_dog_enum_digest_email')) {
            wp_schedule_event(time(), 'daily', 'guard_dog_enum_digest_email');
        }
        add_action('guard_dog_enum_digest_email', [$this, 'maybe_send_digest_email']);
    }

    /**
     * Check if current user can bypass enumeration protection
     *
     * @return bool
     */
    private function user_can_bypass(): bool {
        if (!is_user_logged_in()) {
            return false;
        }

        $bypass_roles = get_option(self::OPTION_BYPASS_ROLES, ['administrator']);
        if (!is_array($bypass_roles)) {
            $bypass_roles = ['administrator'];
        }

        $user = wp_get_current_user();
        foreach ($bypass_roles as $role) {
            if (in_array($role, $user->roles, true)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Check if a specific vector should block (vs just log)
     *
     * @param string $vector Vector identifier
     * @return bool
     */
    private function should_block(string $vector): bool {
        $option_map = [
            'rest_api' => self::OPTION_REST_API_BLOCK,
            'author_archive' => self::OPTION_AUTHOR_ARCHIVES_BLOCK,
            'login_error' => self::OPTION_LOGIN_ERRORS_BLOCK,
            'password_reset' => self::OPTION_PASSWORD_RESET_BLOCK,
            'xmlrpc' => self::OPTION_XMLRPC_BLOCK,
            'oembed' => self::OPTION_OEMBED_BLOCK,
            'registration' => self::OPTION_REGISTRATION_BLOCK,
        ];

        if (!isset($option_map[$vector])) {
            return true; // Default to blocking unknown vectors
        }

        return (bool) get_option($option_map[$vector], true);
    }

    /**
     * Log an enumeration attempt
     *
     * @param string $vector Vector identifier
     * @param string $action 'blocked' or 'logged'
     * @param array $extra_details Additional details to log
     */
    private function log_attempt(string $vector, string $action, array $extra_details = []): void {
        if (!$this->activity_log) {
            return;
        }

        $ip = $this->get_client_ip();
        $details = array_merge([
            'vector' => $vector,
            'action' => $action,
            'request_url' => $this->get_request_url(),
            'query_params' => $this->get_query_params(),
            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
            'referrer' => isset($_SERVER['HTTP_REFERER']) ? sanitize_url(wp_unslash($_SERVER['HTTP_REFERER'])) : '',
            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
            'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '',
        ], $extra_details);

        // Add geo data if available
        $geo_data = $this->get_geo_data($ip);
        if (!empty($geo_data)) {
            $details['geo'] = $geo_data;
        }

        $this->activity_log->add_log_entry('enumeration_attempt', [
            'ip_address' => $ip,
            'details' => $details,
        ]);

        // Check for auto-blocking
        $this->maybe_auto_block_ip($ip);

        // Check for immediate alerts
        $this->maybe_send_immediate_alert($ip);
    }

    /**
     * Get client IP address
     *
     * @return string
     */
    private function get_client_ip(): string {
        if (class_exists('Guard_Dog_Utils')) {
            return Guard_Dog_Utils::get_client_ip();
        }

        // Fallback
        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
        return isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '';
    }

    /**
     * Get current request URL
     *
     * @return string
     */
    private function get_request_url(): string {
        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
        $url = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : '';
        return $url;
    }

    /**
     * Get query parameters from current request
     *
     * @return array
     */
    private function get_query_params(): array {
        // phpcs:disable WordPress.Security.NonceVerification.Recommended
        // Context: This is a logging function that captures request details for security analysis
        // Security: All values are sanitized with sanitize_text_field before storage
        // Justification: Nonce verification not applicable for logging inbound requests
        return array_map('sanitize_text_field', wp_unslash($_GET));
        // phpcs:enable WordPress.Security.NonceVerification.Recommended
    }

    /**
     * Get geo data for IP if available
     *
     * @param string $ip IP address
     * @return array
     */
    private function get_geo_data(string $ip): array {
        if (!function_exists('Guard_Dog') || !isset(Guard_Dog()->features['ip_reputation'])) {
            return [];
        }

        $ip_reputation = Guard_Dog()->features['ip_reputation'];
        if (!method_exists($ip_reputation, 'get_ip_info')) {
            return [];
        }

        $info = $ip_reputation->get_ip_info($ip);
        if (empty($info)) {
            return [];
        }

        return [
            'country' => $info['country'] ?? '',
            'city' => $info['city'] ?? '',
            'isp' => $info['org'] ?? '',
        ];
    }

    /**
     * Maybe auto-block IP after threshold reached
     *
     * @param string $ip IP address
     */
    private function maybe_auto_block_ip(string $ip): void {
        if (!get_option(self::OPTION_AUTO_BLOCK_ENABLED, true)) {
            return;
        }

        $threshold = (int) get_option(self::OPTION_AUTO_BLOCK_THRESHOLD, 20);
        $window = (int) get_option(self::OPTION_AUTO_BLOCK_WINDOW, 60);
        $duration = (int) get_option(self::OPTION_AUTO_BLOCK_DURATION, 1440);

        $transient_key = 'guard_dog_enum_count_' . md5($ip);
        $count = (int) get_transient($transient_key);
        $count++;

        set_transient($transient_key, $count, $window * MINUTE_IN_SECONDS);

        if ($count >= $threshold) {
            $this->block_ip($ip, $duration);
            delete_transient($transient_key);
        }
    }

    /**
     * Block an IP address
     *
     * @param string $ip IP address
     * @param int $duration Duration in minutes
     */
    private function block_ip(string $ip, int $duration): void {
        $access_control = function_exists('Guard_Dog') && isset(Guard_Dog()->features['access_control'])
            ? Guard_Dog()->features['access_control']
            : null;

        if (!$access_control) {
            return;
        }

        // Convert minutes to days (ceiling, minimum 1)
        $expiration_days = max(1, (int) ceil($duration / 1440));

        $result = $access_control->add_ip_to_blacklist(
            $ip,
            $expiration_days,
            __('User enumeration auto-block', 'guard-dog'),
            'enum_auto_block'
        );

        // Log the auto-block
        if ($result && $this->activity_log) {
            $this->activity_log->add_log_entry('ip_added_to_blacklist', [
                'ip_address' => $ip,
                'details' => [
                    'reason' => 'User enumeration auto-block',
                    'duration_minutes' => $duration,
                ],
            ]);
        }
    }

    /**
     * Maybe send immediate alert
     *
     * @param string $ip IP address
     */
    private function maybe_send_immediate_alert(string $ip): void {
        if (!get_option(self::OPTION_ALERT_IMMEDIATE_ENABLED, false)) {
            return;
        }

        $threshold = (int) get_option(self::OPTION_ALERT_IMMEDIATE_THRESHOLD, 10);
        $window = (int) get_option(self::OPTION_ALERT_IMMEDIATE_WINDOW, 5);
        $cooldown = (int) get_option(self::OPTION_ALERT_IMMEDIATE_COOLDOWN, 60);

        // Check cooldown
        $cooldown_key = 'guard_dog_enum_alert_cooldown_' . md5($ip);
        if (get_transient($cooldown_key)) {
            return;
        }

        // Check threshold
        $count_key = 'guard_dog_enum_alert_count_' . md5($ip);
        $count = (int) get_transient($count_key);
        $count++;
        set_transient($count_key, $count, $window * MINUTE_IN_SECONDS);

        if ($count >= $threshold) {
            $this->send_immediate_alert($ip, $count);
            set_transient($cooldown_key, true, $cooldown * MINUTE_IN_SECONDS);
            delete_transient($count_key);
        }
    }

    /**
     * Send immediate alert email
     *
     * @param string $ip IP address
     * @param int $attempt_count Number of attempts
     */
    private function send_immediate_alert(string $ip, int $attempt_count): void {
        $email = get_option(self::OPTION_ALERT_IMMEDIATE_EMAIL, get_option('admin_email'));
        $site_name = get_bloginfo('name');

        $subject = sprintf(
            /* translators: 1: site name, 2: IP address */
            __('[%1$s] User Enumeration Alert - %2$s', 'guard-dog'),
            $site_name,
            $ip
        );

        $geo = $this->get_geo_data($ip);
        $geo_text = !empty($geo['country']) ? sprintf('%s, %s', $geo['city'] ?? '', $geo['country']) : __('Unknown location', 'guard-dog');

        $message = sprintf(
            /* translators: 1: attempt count, 2: IP address, 3: geo location, 4: admin URL */
            __("User enumeration attack detected!\n\nAttempts: %1\$d\nIP Address: %2\$s\nLocation: %3\$s\n\nView activity log: %4\$s", 'guard-dog'),
            $attempt_count,
            $ip,
            $geo_text,
            admin_url('admin.php?page=guard-dog-logs&filter_ip=' . urlencode($ip))
        );

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

    /**
     * Maybe send digest email (called by cron)
     */
    public function maybe_send_digest_email(): void {
        if (!get_option(self::OPTION_ALERT_DIGEST_ENABLED, false)) {
            return;
        }

        $frequency = get_option(self::OPTION_ALERT_DIGEST_FREQUENCY, 'weekly');
        $threshold = (int) get_option(self::OPTION_ALERT_DIGEST_THRESHOLD, 50);

        // Determine time period based on frequency
        $period = ($frequency === 'daily') ? DAY_IN_SECONDS : WEEK_IN_SECONDS;

        // Get enumeration attempts from activity log
        $attempts = $this->get_enumeration_attempts_in_period($period);

        if (count($attempts) < $threshold) {
            return;
        }

        $this->send_digest_email($attempts, $frequency);
    }

    /**
     * Get enumeration attempts in time period
     *
     * @param int $period Period in seconds
     * @return array
     */
    private function get_enumeration_attempts_in_period(int $period): array {
        global $wpdb;
        $since = gmdate('Y-m-d H:i:s', time() - $period);

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
        $results = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT * FROM {$wpdb->prefix}guard_dog_logs WHERE event_type = %s AND timestamp >= %s ORDER BY timestamp DESC",
                'enumeration_attempt',
                $since
            ),
            ARRAY_A
        );

        return $results ?: [];
    }

    /**
     * Send digest email
     *
     * @param array $attempts Enumeration attempts
     * @param string $frequency 'daily' or 'weekly'
     */
    private function send_digest_email(array $attempts, string $frequency): void {
        $email = get_option(self::OPTION_ALERT_DIGEST_EMAIL, get_option('admin_email'));
        $site_name = get_bloginfo('name');

        $period_label = ($frequency === 'daily') ? __('Daily', 'guard-dog') : __('Weekly', 'guard-dog');

        $subject = sprintf(
            /* translators: 1: site name, 2: period (Daily/Weekly) */
            __('[%1$s] %2$s User Enumeration Report', 'guard-dog'),
            $site_name,
            $period_label
        );

        // Aggregate stats
        $total = count($attempts);
        $by_vector = [];
        $by_ip = [];

        foreach ($attempts as $attempt) {
            $details = $this->parse_attempt_details($attempt['details'] ?? null);
            $vector = $details['vector'] ?? 'unknown';
            $ip = $attempt['ip_address'] ?? 'unknown';

            $by_vector[$vector] = ($by_vector[$vector] ?? 0) + 1;
            $by_ip[$ip] = ($by_ip[$ip] ?? 0) + 1;
        }

        arsort($by_ip);
        $top_ips = array_slice($by_ip, 0, 10, true);

        $message = sprintf(
            /* translators: 1: period label, 2: total attempts */
            __("%1\$s User Enumeration Report\n\nTotal Attempts: %2\$d\n\n", 'guard-dog'),
            $period_label,
            $total
        );

        $message .= __("By Vector:\n", 'guard-dog');
        foreach ($by_vector as $vector => $count) {
            $message .= sprintf("  %s: %d\n", self::$vectors[$vector] ?? $vector, $count);
        }

        $message .= __("\nTop Offending IPs:\n", 'guard-dog');
        foreach ($top_ips as $ip => $count) {
            $message .= sprintf("  %s: %d attempts\n", $ip, $count);
        }

        $message .= sprintf(
            __("\nView full activity log: %s", 'guard-dog'),
            admin_url('admin.php?page=guard-dog-logs&filter_event=enumeration_attempt')
        );

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

    /**
     * Parse stored attempt details from JSON or serialized array format.
     *
     * @param mixed $details Raw details value from the activity log.
     * @return array
     */
    private function parse_attempt_details($details): array {
        if (is_array($details)) {
            return $details;
        }

        if (!is_string($details) || $details === '') {
            return [];
        }

        $decoded = json_decode($details, true);
        if (is_array($decoded)) {
            return $decoded;
        }

        $unserialized = maybe_unserialize($details);
        return is_array($unserialized) ? $unserialized : [];
    }

    // ========================================
    // VECTOR HANDLERS - Implemented in subsequent tasks
    // ========================================

    /**
     * Handle REST API user endpoint requests
     *
     * @param mixed $result Response to replace
     * @param WP_REST_Server $server Server instance
     * @param WP_REST_Request $request Request
     * @return mixed
     */
    public function handle_rest_api_request($result, $server, $request) {
        // Only intercept user-related endpoints
        $route = $request->get_route();

        // Check if this is a users endpoint
        if (strpos($route, '/wp/v2/users') === false) {
            return $result;
        }

        // Allow bypass for authorized roles
        if ($this->user_can_bypass()) {
            return $result;
        }

        // Determine action based on blocking setting
        $action = $this->should_block('rest_api') ? 'blocked' : 'logged';

        // Log the attempt
        $this->log_attempt('rest_api', $action, [
            'route' => $route,
            'method' => $request->get_method(),
        ]);

        // If blocking, return empty result (looks like no users exist)
        if ($action === 'blocked') {
            // For list requests, return empty array
            if ($route === '/wp/v2/users' || preg_match('#^/wp/v2/users$#', $route)) {
                return new WP_REST_Response([], 200);
            }

            // For individual user requests, return 404
            if (preg_match('#^/wp/v2/users/\d+#', $route)) {
                return new WP_Error(
                    'rest_user_invalid_id',
                    __('Invalid user ID.', 'guard-dog'),
                    ['status' => 404]
                );
            }
        }

        return $result;
    }

    /**
     * Handle author archive redirect
     */
    public function handle_author_archive_redirect(): void {
        // Only treat raw ?author=N style requests as enumeration attempts.
        // Legit pretty author archives like /author/jane-doe/ should remain accessible.
        if (!$this->is_direct_author_enumeration_request()) {
            return;
        }

        // Allow bypass for authorized roles
        if ($this->user_can_bypass()) {
            return;
        }

        // Determine action based on blocking setting
        $action = $this->should_block('author_archive') ? 'blocked' : 'logged';

        // Get the author ID being queried
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
        $author_id = isset($_GET['author']) ? absint($_GET['author']) : get_queried_object_id();

        // Log the attempt
        $this->log_attempt('author_archive', $action, [
            'author_id_tried' => $author_id,
        ]);

        // If blocking, redirect to 404
        if ($action === 'blocked') {
            global $wp_query;
            $wp_query->set_404();
            status_header(404);
            nocache_headers();

            // Try to load 404 template
            $template = get_404_template();
            if ($template) {
                include $template;
                exit;
            }

            // Fallback: redirect to home
            wp_safe_redirect(home_url(), 302);
            exit;
        }
    }

    /**
     * Determine whether the current request is a direct author enumeration probe.
     *
     * We intentionally scope this to raw `?author=` requests so sites can keep
     * public `/author/slug/` archives enabled without tripping this protection.
     *
     * @return bool
     */
    private function is_direct_author_enumeration_request(): bool {
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
        if (!isset($_GET['author']) || !is_author()) {
            return false;
        }

        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
        if (!is_scalar($_GET['author'])) {
            return false;
        }

        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
        $author = trim((string) wp_unslash($_GET['author']));

        return '' !== $author;
    }

    /**
     * Handle login authentication for error masking
     *
     * @param WP_User|WP_Error|null $user User or error
     * @param string $username Username
     * @param string $password Password
     * @return WP_User|WP_Error|null
     */
    public function handle_login_errors($user, $username, $password) {
        // Only process errors
        if (!is_wp_error($user)) {
            return $user;
        }

        // Do not re-mask Guard Dog-originated auth errors (e.g. access control),
        // otherwise operators lose the actual block reason.
        if ( $this->is_guard_dog_auth_error( $user ) ) {
            return $user;
        }

        // Check if Guard_Dog_Login_Errors is already handling this
        $generic_error = get_option('guard_dog_generic_login_error', '');
        if (!empty(trim($generic_error))) {
            // Guard_Dog_Login_Errors will handle the masking
            return $user;
        }

        // Allow bypass for authorized roles (unlikely as user isn't logged in, but check anyway)
        if ($this->user_can_bypass()) {
            return $user;
        }

        // Check error codes that reveal user existence
        $revealing_codes = [
            'invalid_username',
            'invalid_email',
            'incorrect_password', // Different message reveals user exists
        ];

        $error_codes = $user->get_error_codes();
        $is_revealing = !empty(array_intersect($error_codes, $revealing_codes));

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

        // Determine action
        $action = $this->should_block('login_error') ? 'blocked' : 'logged';

        // Log the attempt
        $this->log_attempt('login_error', $action, [
            'user_targeted' => $username,
            'original_error_codes' => $error_codes,
        ]);

        // If blocking, replace with generic error
        if ($action === 'blocked') {
            // Create new error with generic message
            return new WP_Error(
                'authentication_failed',
                __('<strong>Error:</strong> Invalid username or password.', 'guard-dog')
            );
        }

        return $user;
    }

    /**
     * Check whether a WP_Error originated from another Guard Dog auth component.
     *
     * @param WP_Error $error Auth error object.
     * @return bool
     */
    private function is_guard_dog_auth_error( WP_Error $error ): bool {
        foreach ( $error->get_error_codes() as $code ) {
            $data = $error->get_error_data( $code );
            if ( is_array( $data ) && ! empty( $data['guard_dog_source'] ) ) {
                return true;
            }
        }

        return false;
    }

    /**
     * Handle password reset request
     *
     * @param WP_Error $errors Errors object
     * @param WP_User|false $user_data User data or false if not found
     */
    public function handle_password_reset_request($errors, $user_data): void {
        // Allow bypass for authorized roles
        if ($this->user_can_bypass()) {
            return;
        }

        // If user_data is false, user doesn't exist - this reveals information
        if ($user_data === false) {
            // Determine action
            $action = $this->should_block('password_reset') ? 'blocked' : 'logged';

            // Get the attempted login/email
            // phpcs:disable WordPress.Security.NonceVerification.Missing
            // 1. Context: This runs during WordPress password reset flow before user auth
            // 2. Security: Accessing standard WordPress password reset form field ('user_login')
            // 3. Sanitization: Data sanitized with sanitize_text_field() and wp_unslash()
            // 4. Justification: Security feature for unauthenticated users
            // 5. Nonce limitation: WordPress lostpassword form has its own handling
            // 6. Risk assessment: Minimal - read-only with proper sanitization
            $user_login = '';
            if (isset($_POST['user_login'])) {
                $user_login = sanitize_text_field(wp_unslash($_POST['user_login']));
            }
            // phpcs:enable WordPress.Security.NonceVerification.Missing

            // Log the attempt
            $this->log_attempt('password_reset', $action, [
                'user_targeted' => $user_login,
                'user_exists' => false,
            ]);

            // If blocking, we need to fake success
            // This is tricky - we can't easily prevent the error from showing
            // Instead, we'll use the message filter to show generic response
            if ($action === 'blocked') {
                $this->password_reset_generic_shown = true;
            }
        }
    }

    /**
     * Handle password reset message - show generic response regardless of user existence
     *
     * @param string $message Email message
     * @param string $key Reset key
     * @param string $user_login Username
     * @param WP_User $user_data User object
     * @return string
     */
    public function handle_password_reset_message($message, $key, $user_login, $user_data) {
        // This only fires if user exists, so no action needed here
        // The protection happens in handle_password_reset_request
        return $message;
    }

    /**
     * Handle lost password errors to mask user existence
     *
     * @param WP_Error $errors Error object
     * @param WP_User|false $user_data User data or false
     * @return WP_Error
     */
    public function handle_lostpassword_errors($errors, $user_data) {
        // Allow bypass
        if ($this->user_can_bypass()) {
            return $errors;
        }

        // If user not found and we're blocking
        if ($user_data === false && $this->should_block('password_reset')) {
            // Clear revealing errors and add generic success-like message
            // Actually, we can't easily fake success, so use generic error
            $errors = new WP_Error();
            $errors->add(
                'check_email',
                __('If this email address is registered, you will receive a password reset link.', 'guard-dog')
            );
        }

        return $errors;
    }

    /**
     * Handle XML-RPC methods - remove user enumeration methods
     *
     * This filter receives ALL available methods, not the method being called.
     * We remove the user enumeration methods from the available list.
     * Logging happens in handle_xmlrpc_call() when methods are actually called.
     *
     * @param array $methods Available XML-RPC methods
     * @return array
     */
    public function handle_xmlrpc_methods($methods) {
        // Allow bypass for authorized roles
        if ($this->user_can_bypass()) {
            return $methods;
        }

        // If not blocking, just return methods unchanged
        // (Logging happens in handle_xmlrpc_call when methods are actually called)
        if (!$this->should_block('xmlrpc')) {
            return $methods;
        }

        // Methods that expose user information
        $user_methods = [
            'wp.getAuthors',
            'wp.getUsers',
            'wp.getUsersBlogs',
        ];

        // Remove the methods to prevent enumeration
        foreach ($user_methods as $method) {
            unset($methods[$method]);
        }

        return $methods;
    }

    /**
     * Handle XML-RPC method call - log actual enumeration attempts
     *
     * @param string $method The XML-RPC method being called
     */
    public function handle_xmlrpc_call($method): void {
        // Allow bypass for authorized roles
        if ($this->user_can_bypass()) {
            return;
        }

        // Methods that expose user information
        $user_methods = [
            'wp.getAuthors',
            'wp.getUsers',
            'wp.getUsersBlogs',
        ];

        if (!in_array($method, $user_methods, true)) {
            return;
        }

        // Determine action
        $action = $this->should_block('xmlrpc') ? 'blocked' : 'logged';

        // Log the attempt
        $this->log_attempt('xmlrpc', $action, [
            'method_called' => $method,
        ]);
    }

    /**
     * Handle oEmbed response - strip author data
     *
     * @param array $data Response data
     * @param WP_Post $post Post object
     * @param int $width Width
     * @param int $height Height
     * @return array
     */
    public function handle_oembed_response($data, $post, $width, $height) {
        // Allow bypass for authorized roles
        if ($this->user_can_bypass()) {
            return $data;
        }

        // Check if author data is present
        if (!isset($data['author_name']) && !isset($data['author_url'])) {
            return $data;
        }

        // Determine action
        $action = $this->should_block('oembed') ? 'blocked' : 'logged';

        // Log the attempt
        $this->log_attempt('oembed', $action, [
            'post_id' => $post->ID,
            'author_id' => $post->post_author,
        ]);

        // If blocking, strip author information
        if ($action === 'blocked') {
            unset($data['author_name']);
            unset($data['author_url']);
        }

        return $data;
    }

    /**
     * Handle registration errors - mask username/email existence
     *
     * @param WP_Error $errors Errors object
     * @param string $sanitized_user_login Username
     * @param string $user_email Email
     * @return WP_Error
     */
    public function handle_registration_errors($errors, $sanitized_user_login, $user_email) {
        // Allow bypass for authorized roles
        if ($this->user_can_bypass()) {
            return $errors;
        }

        // Check for revealing error codes
        $revealing_codes = [
            'username_exists',
            'email_exists',
        ];

        $error_codes = $errors->get_error_codes();
        $revealing_errors = array_intersect($error_codes, $revealing_codes);

        if (empty($revealing_errors)) {
            return $errors;
        }

        // Determine action
        $action = $this->should_block('registration') ? 'blocked' : 'logged';

        // Log the attempt
        $this->log_attempt('registration', $action, [
            'user_targeted' => $sanitized_user_login,
            'email_targeted' => $user_email,
            'original_error_codes' => $revealing_errors,
        ]);

        // If blocking, replace with generic errors
        if ($action === 'blocked') {
            // Remove revealing errors
            foreach ($revealing_codes as $code) {
                $errors->remove($code);
            }

            // Add generic error
            $errors->add(
                'registration_failed',
                __('<strong>Error:</strong> Registration could not be completed. Please try different credentials.', 'guard-dog')
            );
        }

        return $errors;
    }

    /**
     * Get available vectors
     *
     * @return array
     */
    public static function get_vectors(): array {
        return self::$vectors;
    }

} // End class Guard_Dog_User_Enumeration
