<?php
/**
 * Name: 2FA
 * Description: Add an extra layer of security with multiple two-factor authentication methods including TOTP apps, email codes, and recovery codes
 * Category: Security
 * Icon: -
 * Version: 2.0
 * Database: no
 * UseCases: [
 *   {
 *     "title": "Protect Admin Accounts",
 *     "description": "Require 2FA for administrator accounts to prevent unauthorized access even if passwords are compromised."
 *   },
 *   {
 *     "title": "Compliance Requirements",
 *     "description": "Meet security compliance requirements that mandate multi-factor authentication for sensitive systems."
 *   },
 *   {
 *     "title": "Flexible Security",
 *     "description": "Allow users to choose between authenticator apps, email codes, or recovery codes based on their preference."
 *   }
 * ]
 * Added: 2025-01-01
 * Tags: security, 2fa, totp, authentication, auth, login, mfa, two-factor
 * Conflicts: otp-login
 * WhereToFind: {
 *   "location": "User Profile",
 *   "url": "profile.php",
 *   "instructions": "Users can set up 2FA from their profile page under Two-Factor Options",
 *   "newTab": false
 * }
 */

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

/**
 * Class SwitchboardTwoFactorAuthentication
 *
 * Core orchestration class for Two-Factor Authentication.
 * Based on WordPress.org Two-Factor plugin patterns.
 *
 * @since 1.0.0
 */
class Switchboard2fa {

    /**
     * The user meta key for the primary provider.
     *
     * @var string
     */
    const PROVIDER_USER_META_KEY = '_switchboard_2fa_provider';

    /**
     * The user meta key for enabled providers.
     *
     * @var string
     */
    const ENABLED_PROVIDERS_USER_META_KEY = '_switchboard_2fa_enabled_providers';

    /**
     * The user meta key for the login nonce.
     *
     * @var string
     */
    const USER_META_NONCE_KEY = '_switchboard_2fa_nonce';

    /**
     * The user meta key for the last failed login timestamp.
     *
     * @var string
     */
    const USER_RATE_LIMIT_KEY = '_switchboard_2fa_last_login_failure';

    /**
     * The user meta key for failed login attempts count.
     *
     * @var string
     */
    const USER_FAILED_LOGIN_ATTEMPTS_KEY = '_switchboard_2fa_failed_login_attempts';

    /**
     * The user meta key to store whether the password was reset.
     *
     * @var string
     */
    const USER_PASSWORD_WAS_RESET_KEY = '_switchboard_2fa_password_was_reset';

    /**
     * The user meta key for trusted devices.
     *
     * @var string
     */
    const USER_TRUSTED_DEVICES_KEY = '_switchboard_2fa_trusted_devices';

    /**
     * Single instance of the class.
     *
     * @var SwitchboardTwoFactorAuthentication|null
     */
    private static $switchboard_instance = null;

    /**
     * Keep track of all the password-based authentication sessions.
     *
     * @var array
     */
    private static $password_auth_tokens = array();

    /**
     * Get the singleton instance.
     *
     * @since 1.0.0
     * @return SwitchboardTwoFactorAuthentication
     */
    public static function get_instance() {
        if (null === self::$switchboard_instance) {
            self::$switchboard_instance = new self();
        }
        return self::$switchboard_instance;
    }

    /**
     * Class constructor.
     *
     * @since 1.0.0
     */
    public function __construct() {
        // Prevent hooks from being registered multiple times.
        static $hooks_registered = false;
        if ($hooks_registered) {
            return;
        }
        $hooks_registered = true;

        // Load providers immediately for AJAX requests, otherwise on init.
        if (wp_doing_ajax()) {
            $this->get_providers();
        } else {
            add_action('init', array($this, 'get_providers'), 1);
        }

        // Enqueue scripts for profile page.
        add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts'));

        // Login hooks.
        add_action('wp_login', array($this, 'wp_login'), 10, 2);
        add_action('login_form_validate_2fa', array($this, 'login_form_validate_2fa'));
        add_filter('wp_login_errors', array($this, 'maybe_show_reset_password_notice'));
        add_filter('wp_login_errors', array($this, 'maybe_show_2fa_session_expired_notice'));
        add_action('after_password_reset', array($this, 'clear_password_reset_notice'));

        // Profile page hooks.
        add_action('show_user_profile', array($this, 'user_two_factor_options'));
        add_action('edit_user_profile', array($this, 'user_two_factor_options'));
        add_action('personal_options_update', array($this, 'user_two_factor_options_update'));
        add_action('edit_user_profile_update', array($this, 'user_two_factor_options_update'));

        // Users list column.
        add_filter('manage_users_columns', array($this, 'filter_manage_users_columns'));
        add_filter('manage_users_custom_column', array($this, 'manage_users_custom_column'), 10, 3);

        // Track auth cookie tokens.
        add_action('set_auth_cookie', array($this, 'collect_auth_cookie_tokens'));
        add_action('set_logged_in_cookie', array($this, 'collect_auth_cookie_tokens'));

        // Block cookies until 2FA verified.
        add_filter('authenticate', array($this, 'filter_authenticate_block_cookies'), PHP_INT_MAX);

        // Clear device token cookie on logout.
        add_action('wp_logout', array($this, 'clear_device_token_on_logout'));

        // REST API routes.
        add_action('rest_api_init', array($this, 'register_rest_routes'));

        // AJAX handlers for settings panel.
        add_action('wp_ajax_switchboard_2fa_search_users', array($this, 'ajax_search_users'));
    }

    /**
     * Get the default providers.
     *
     * @since 1.0.0
     * @return array
     */
    private function get_default_providers() {
        $providers_dir = plugin_dir_path(__FILE__) . '../../assets/2fa/';

        return array(
            'Switchboard_2FA_TOTP' => $providers_dir . 'class-totp.php',
            'Switchboard_2FA_Email' => $providers_dir . 'class-email.php',
            'Switchboard_2FA_Backup_Codes' => $providers_dir . 'class-backup-codes.php',
        );
    }

    /**
     * Get all registered two-factor providers.
     *
     * @since 1.0.0
     * @return array List of provider instances.
     */
    public function get_providers() {
        static $providers_cache = null;

        if (null !== $providers_cache) {
            return $providers_cache;
        }

        $providers = $this->get_default_providers();

        // Load provider base class first.
        require_once plugin_dir_path(__FILE__) . '../../assets/2fa/class-provider.php';

        /**
         * Filter the supplied providers.
         *
         * @param array $providers A key-value array where the key is the class name, and
         *                         the value is the path to the file containing the class.
         */
        $providers = apply_filters('switchboard_2fa_providers', $providers);

        // Load and instantiate providers.
        foreach ($providers as $provider_key => $path) {
            if (!empty($path) && is_readable($path)) {
                require_once $path;
            }

            if (class_exists($provider_key)) {
                try {
                    $providers[$provider_key] = call_user_func(array($provider_key, 'get_instance'));
                } catch (Exception $e) {
                    unset($providers[$provider_key]);
                }
            } else {
                unset($providers[$provider_key]);
            }
        }

        $providers_cache = $providers;
        return $providers;
    }

    /**
     * Get providers for a specific user.
     *
     * @since 1.0.0
     * @param WP_User|int|null $user User object or ID.
     * @return array List of provider instances.
     */
    public function get_providers_for_user($user = null) {
        $user = $this->fetch_user($user);
        if (!$user) {
            return array();
        }

        $providers = $this->get_providers();

        /**
         * Filter the providers available for a user.
         *
         * @param array   $providers List of provider instances.
         * @param WP_User $user      User object.
         */
        return apply_filters('switchboard_2fa_providers_for_user', $providers, $user);
    }

    /**
     * Get enabled providers for a user.
     *
     * @since 1.0.0
     * @param WP_User|int|null $user User object or ID.
     * @return array List of enabled provider class names.
     */
    public function get_enabled_providers_for_user($user = null) {
        $user = $this->fetch_user($user);
        if (!$user) {
            return array();
        }

        $providers = $this->get_providers_for_user($user);
        $enabled_providers = get_user_meta($user->ID, self::ENABLED_PROVIDERS_USER_META_KEY, true);

        if (empty($enabled_providers)) {
            $enabled_providers = array();
        }

        $enabled_providers = array_intersect($enabled_providers, array_keys($providers));

        /**
         * Filter the enabled providers for a user.
         *
         * @param array $enabled_providers The enabled provider class names.
         * @param int   $user_id           The user ID.
         */
        return apply_filters('switchboard_2fa_enabled_providers_for_user', $enabled_providers, $user->ID);
    }

    /**
     * Get available (enabled and configured) providers for a user.
     *
     * @since 1.0.0
     * @param WP_User|int|null $user User object or ID.
     * @return array List of available provider instances.
     */
    public function get_available_providers_for_user($user = null) {
        $user = $this->fetch_user($user);
        if (!$user) {
            return array();
        }

        $providers = $this->get_providers_for_user($user);
        $enabled_providers = $this->get_enabled_providers_for_user($user);
        $configured_providers = array();

        foreach ($providers as $provider_key => $provider) {
            if (in_array($provider_key, $enabled_providers, true) && $provider->is_available_for_user($user)) {
                $configured_providers[$provider_key] = $provider;
            }
        }

        return $configured_providers;
    }

    /**
     * Get the primary provider for a user.
     *
     * @since 1.0.0
     * @param WP_User|int|null $user User object or ID.
     * @return object|null Provider instance or null.
     */
    public function get_primary_provider_for_user($user = null) {
        $user = $this->fetch_user($user);
        if (!$user) {
            return null;
        }

        $providers = $this->get_providers_for_user($user);
        $available_providers = $this->get_available_providers_for_user($user);

        if (empty($available_providers)) {
            return null;
        } elseif (1 === count($available_providers)) {
            $provider = key($available_providers);
        } else {
            $provider = get_user_meta($user->ID, self::PROVIDER_USER_META_KEY, true);

            // If the provider specified isn't enabled, grab the first one that is.
            if (!isset($available_providers[$provider])) {
                $provider = key($available_providers);
            }
        }

        /**
         * Filter the primary provider for a user.
         *
         * @param string $provider The provider class name.
         * @param int    $user_id  The user ID.
         */
        $provider = apply_filters('switchboard_2fa_primary_provider_for_user', $provider, $user->ID);

        if (isset($providers[$provider])) {
            return $providers[$provider];
        }

        return null;
    }

    /**
     * Enable a provider for a user.
     *
     * @since 1.0.0
     * @param int    $user_id  User ID.
     * @param string $provider Provider class name.
     * @return bool
     */
    public function enable_provider_for_user($user_id, $provider) {
        $enabled_providers = $this->get_enabled_providers_for_user($user_id);

        if (!in_array($provider, $enabled_providers, true)) {
            $enabled_providers[] = $provider;
            return update_user_meta($user_id, self::ENABLED_PROVIDERS_USER_META_KEY, $enabled_providers);
        }

        return true;
    }

    /**
     * Disable a provider for a user.
     *
     * @since 1.0.0
     * @param int    $user_id  User ID.
     * @param string $provider Provider class name.
     * @return bool
     */
    public function disable_provider_for_user($user_id, $provider) {
        $enabled_providers = $this->get_enabled_providers_for_user($user_id);
        $key = array_search($provider, $enabled_providers, true);

        if (false !== $key) {
            unset($enabled_providers[$key]);
            return update_user_meta($user_id, self::ENABLED_PROVIDERS_USER_META_KEY, array_values($enabled_providers));
        }

        return true;
    }

    /**
     * Check if a user is using two-factor authentication.
     *
     * @since 1.0.0
     * @param WP_User|int|null $user User object or ID.
     * @return bool
     */
    public function is_user_using_two_factor($user = null) {
        $provider = $this->get_primary_provider_for_user($user);
        return !empty($provider);
    }

    /**
     * Fetch a user object.
     *
     * @since 1.0.0
     * @param WP_User|int|null $user User object, ID, or null for current user.
     * @return WP_User|false
     */
    public function fetch_user($user = null) {
        if (null === $user) {
            $user = wp_get_current_user();
        } elseif (!($user instanceof WP_User)) {
            $user = get_user_by('id', $user);
        }

        if (!$user || !$user->exists()) {
            return false;
        }

        return $user;
    }

    /**
     * Handle the browser-based login.
     *
     * @since 1.0.0
     * @param string  $user_login Username.
     * @param WP_User $user       User object.
     */
    public function wp_login($user_login, $user) {
        if (!$this->is_user_using_two_factor($user->ID)) {
            return;
        }

        // Check for trusted device.
        if ($this->is_trusted_device($user->ID)) {
            return;
        }

        // Invalidate the current login session.
        $this->destroy_current_session_for_user($user);

        // Clear the cookies.
        wp_clear_auth_cookie();

        $this->show_two_factor_login($user);
        exit;
    }

    /**
     * Check if current device is trusted.
     *
     * @since 1.0.0
     * @param int $user_id User ID.
     * @return bool
     */
    public function is_trusted_device($user_id) {
        $settings = get_option('switchboard_2fa_settings', array());
        if (empty($settings['trusted_devices_enabled'])) {
            return false;
        }

        $devices = get_user_meta($user_id, self::USER_TRUSTED_DEVICES_KEY, true);
        if (!is_array($devices) || empty($devices)) {
            return false;
        }

        $device_token = isset($_COOKIE['switchboard_2fa_device']) ? sanitize_text_field($_COOKIE['switchboard_2fa_device']) : '';
        if (!$device_token) {
            return false;
        }

        $now = time();
        foreach ($devices as $device) {
            if (isset($device['token']) && hash_equals($device['token'], hash('sha256', $device_token))) {
                if (isset($device['expires']) && $device['expires'] > $now) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Add current device as trusted.
     *
     * @since 1.0.0
     * @param int $user_id User ID.
     */
    public function add_trusted_device($user_id) {
        $settings = get_option('switchboard_2fa_settings', array());
        $days = isset($settings['trusted_devices_days']) ? intval($settings['trusted_devices_days']) : 30;

        $devices = get_user_meta($user_id, self::USER_TRUSTED_DEVICES_KEY, true);
        if (!is_array($devices)) {
            $devices = array();
        }

        // Remove expired devices.
        $now = time();
        $devices = array_filter($devices, function ($device) use ($now) {
            return isset($device['expires']) && $device['expires'] > $now;
        });

        // Generate device token.
        $token = bin2hex(random_bytes(32));
        $expires = $now + ($days * DAY_IN_SECONDS);

        $devices[] = array(
            'token' => hash('sha256', $token),
            'expires' => $expires,
            'ip' => $this->get_client_ip(),
            'user_agent' => substr(sanitize_text_field($_SERVER['HTTP_USER_AGENT'] ?? ''), 0, 255),
            'created' => $now,
        );

        // Keep only last 5 devices.
        if (count($devices) > 5) {
            $devices = array_slice($devices, -5);
        }

        update_user_meta($user_id, self::USER_TRUSTED_DEVICES_KEY, $devices);

        // Set cookie.
        setcookie('switchboard_2fa_device', $token, $expires, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true);
    }

    /**
     * Destroy the known password-based authentication sessions for a user.
     *
     * @since 1.0.0
     * @param WP_User $user User object.
     */
    public function destroy_current_session_for_user($user) {
        $session_manager = WP_Session_Tokens::get_instance($user->ID);

        foreach (self::$password_auth_tokens as $auth_token) {
            $session_manager->destroy($auth_token);
        }
    }

    /**
     * Clear the trusted device cookie on logout.
     *
     * @since 1.0.0
     */
    public function clear_device_token_on_logout() {
        // Clear the device token cookie by setting it to expire in the past.
        if (isset($_COOKIE['switchboard_2fa_device'])) {
            setcookie('switchboard_2fa_device', '', time() - YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true);
        }
    }

    /**
     * Collect auth cookie tokens.
     *
     * @since 1.0.0
     * @param string $cookie Cookie string.
     */
    public function collect_auth_cookie_tokens($cookie) {
        $parsed = wp_parse_auth_cookie($cookie);

        if (!empty($parsed['token'])) {
            self::$password_auth_tokens[] = $parsed['token'];
        }
    }

    /**
     * Prevent login cookies being set on login for 2FA users.
     *
     * @since 1.0.0
     * @param WP_User|WP_Error $user User object or error.
     * @return WP_User|WP_Error
     */
    public function filter_authenticate_block_cookies($user) {
        if ($user instanceof WP_User && $this->is_user_using_two_factor($user->ID) && did_action('login_init')) {
            add_filter('send_auth_cookies', '__return_false', PHP_INT_MAX);
        }

        return $user;
    }

    /**
     * Display the two-factor login form.
     *
     * @since 1.0.0
     * @param WP_User $user User object.
     */
    public function show_two_factor_login($user) {
        if (!$user) {
            $user = wp_get_current_user();
        }

        $login_nonce = $this->create_login_nonce($user->ID);
        if (!$login_nonce) {
            wp_die(esc_html__('Failed to create a login nonce.', 'wpswitchboard'));
        }

        $redirect_to = isset($_REQUEST['redirect_to']) ? esc_url_raw($_REQUEST['redirect_to']) : admin_url();

        $this->login_html($user, $login_nonce['key'], $redirect_to);
    }

    /**
     * Generate the two-factor login form HTML.
     *
     * @since 1.0.0
     * @param WP_User     $user        User object.
     * @param string      $login_nonce Login nonce.
     * @param string      $redirect_to Redirect URL.
     * @param string      $error_msg   Error message.
     * @param string|null $provider    Provider class name.
     */
    public function login_html($user, $login_nonce, $redirect_to, $error_msg = '', $provider = null) {
        if ($provider) {
            $providers = $this->get_providers();
            $provider = isset($providers[$provider]) ? $providers[$provider] : null;
        }

        if (!$provider) {
            $provider = $this->get_primary_provider_for_user($user);
        }

        if (!$provider) {
            wp_die(esc_html__('No two-factor provider available.', 'wpswitchboard'));
        }

        $provider_key = $provider->get_key();
        $available_providers = $this->get_available_providers_for_user($user);
        $backup_providers = array_diff_key($available_providers, array($provider_key => null));
        $rememberme = isset($_REQUEST['rememberme']) ? intval($_REQUEST['rememberme']) : 0;

        // Get settings for trusted devices.
        $settings = get_option('switchboard_2fa_settings', array());
        $trusted_devices_enabled = !empty($settings['trusted_devices_enabled']);
        $trusted_devices_days = isset($settings['trusted_devices_days']) ? intval($settings['trusted_devices_days']) : 30;
        ?>
        <!DOCTYPE html>
        <html <?php language_attributes(); ?>>
        <head>
            <meta charset="<?php bloginfo('charset'); ?>">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title><?php esc_html_e('Two-Factor Authentication', 'wpswitchboard'); ?> - <?php bloginfo('name'); ?></title>
            <?php wp_head(); ?>
            <style>
                body {
                    background: #f0f0f1;
                    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
                    margin: 0;
                    padding: 0;
                }
                .switchboard-2fa-wrapper {
                    display: flex;
                    justify-content: center;
                    align-items: center;
                    min-height: 100vh;
                    padding: 20px;
                    box-sizing: border-box;
                }
                .switchboard-2fa-box {
                    background: #fff;
                    border-radius: 8px;
                    box-shadow: 0 1px 3px rgba(0,0,0,0.1);
                    max-width: 400px;
                    width: 100%;
                    padding: 40px;
                }
                .switchboard-2fa-logo {
                    text-align: center;
                    margin-bottom: 24px;
                }
                .switchboard-2fa-logo h1 {
                    font-size: 24px;
                    font-weight: 600;
                    color: #1e293b;
                    margin: 16px 0 8px;
                }
                .switchboard-2fa-logo p {
                    color: #64748b;
                    font-size: 14px;
                    margin: 0;
                }
                .switchboard-2fa-user {
                    text-align: center;
                    padding: 16px;
                    background: #f8fafc;
                    border-radius: 6px;
                    margin-bottom: 24px;
                }
                .switchboard-2fa-user-avatar {
                    width: 48px;
                    height: 48px;
                    border-radius: 50%;
                    margin-bottom: 8px;
                }
                .switchboard-2fa-user-name {
                    font-weight: 600;
                    color: #1e293b;
                }
                .switchboard-2fa-user-email {
                    font-size: 13px;
                    color: #64748b;
                }
                .switchboard-2fa-form label {
                    display: block;
                    font-weight: 500;
                    color: #374151;
                    margin-bottom: 8px;
                    font-size: 14px;
                }
                .switchboard-2fa-form input[type="text"] {
                    width: 100%;
                    padding: 12px 16px;
                    border: 1px solid #d1d5db;
                    border-radius: 6px;
                    font-size: 18px;
                    letter-spacing: 4px;
                    text-align: center;
                    font-family: 'SF Mono', Monaco, monospace;
                    box-sizing: border-box;
                }
                .switchboard-2fa-form input[type="text"]:focus {
                    outline: none;
                    border-color: #3b82f6;
                    box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
                }
                .switchboard-2fa-form button,
                .switchboard-2fa-form input[type="submit"] {
                    width: 100%;
                    padding: 12px 24px;
                    background: #3b82f6;
                    color: #fff;
                    border: none;
                    border-radius: 6px;
                    font-size: 15px;
                    font-weight: 600;
                    cursor: pointer;
                    margin-top: 16px;
                    transition: background 0.15s ease;
                }
                .switchboard-2fa-form button:hover,
                .switchboard-2fa-form input[type="submit"]:hover {
                    background: #2563eb;
                }
                .switchboard-2fa-remember {
                    display: flex;
                    align-items: center;
                    gap: 8px;
                    margin-top: 16px;
                    font-size: 13px;
                    color: #64748b;
                }
                .switchboard-2fa-remember input {
                    width: 16px;
                    height: 16px;
                }
                .switchboard-2fa-error {
                    background: #fef2f2;
                    border: 1px solid #fecaca;
                    color: #dc2626;
                    padding: 12px 16px;
                    border-radius: 6px;
                    margin-bottom: 16px;
                    font-size: 14px;
                }
                .switchboard-2fa-providers {
                    margin-top: 24px;
                    padding-top: 24px;
                    border-top: 1px solid #e5e7eb;
                }
                .switchboard-2fa-providers p {
                    font-size: 13px;
                    color: #64748b;
                    margin: 0 0 12px;
                }
                .switchboard-2fa-providers ul {
                    margin: 0;
                    padding: 0;
                    list-style: none;
                }
                .switchboard-2fa-providers li {
                    margin-bottom: 8px;
                }
                .switchboard-2fa-providers a {
                    color: #3b82f6;
                    text-decoration: none;
                    font-size: 13px;
                }
                .switchboard-2fa-providers a:hover {
                    text-decoration: underline;
                }
                .switchboard-2fa-back {
                    text-align: center;
                    margin-top: 16px;
                }
                .switchboard-2fa-back a {
                    color: #64748b;
                    text-decoration: none;
                    font-size: 13px;
                }
                .two-factor-prompt {
                    color: #64748b;
                    font-size: 14px;
                    margin-bottom: 16px;
                }
                p.submit {
                    margin: 16px 0 0;
                }
                #wp-submit {
                    width: 100%;
                    padding: 12px 24px;
                    background: #3b82f6;
                    color: #fff;
                    border: none;
                    border-radius: 6px;
                    font-size: 15px;
                    font-weight: 600;
                    cursor: pointer;
                }
                #wp-submit:hover {
                    background: #2563eb;
                }
                .two-factor-email-resend {
                    margin-top: 16px;
                    text-align: center;
                }
                .two-factor-email-resend input[type="submit"] {
                    background: #f1f5f9;
                    color: #475569;
                    font-weight: 500;
                }
                .two-factor-email-resend input[type="submit"]:hover {
                    background: #e2e8f0;
                }
            </style>
        </head>
        <body>
            <div class="switchboard-2fa-wrapper">
                <div class="switchboard-2fa-box">
                    <div class="switchboard-2fa-logo">
                        <span class="dashicons dashicons-lock" style="font-size: 48px; width: 48px; height: 48px;"></span>
                        <h1><?php esc_html_e('Two-Factor Authentication', 'wpswitchboard'); ?></h1>
                        <p><?php echo esc_html($provider->get_label()); ?></p>
                    </div>

                    <div class="switchboard-2fa-user">
                        <?php echo get_avatar($user->ID, 48, '', '', array('class' => 'switchboard-2fa-user-avatar')); ?>
                        <div class="switchboard-2fa-user-name"><?php echo esc_html($user->display_name); ?></div>
                        <div class="switchboard-2fa-user-email"><?php echo esc_html($user->user_email); ?></div>
                    </div>

                    <?php if ($error_msg) : ?>
                        <div class="switchboard-2fa-error"><?php echo esc_html($error_msg); ?></div>
                    <?php endif; ?>

                    <form name="validate_2fa_form" id="loginform" action="<?php echo esc_url(site_url('wp-login.php?action=validate_2fa', 'login_post')); ?>" method="post" autocomplete="off" class="switchboard-2fa-form">
                        <input type="hidden" name="provider" id="provider" value="<?php echo esc_attr($provider_key); ?>" />
                        <input type="hidden" name="wp-auth-id" id="wp-auth-id" value="<?php echo esc_attr($user->ID); ?>" />
                        <input type="hidden" name="wp-auth-nonce" id="wp-auth-nonce" value="<?php echo esc_attr($login_nonce); ?>" />
                        <input type="hidden" name="redirect_to" value="<?php echo esc_attr($redirect_to); ?>" />
                        <input type="hidden" name="rememberme" id="rememberme" value="<?php echo esc_attr($rememberme); ?>" />

                        <?php $provider->authentication_page($user); ?>

                        <?php if ($trusted_devices_enabled) : ?>
                            <label class="switchboard-2fa-remember">
                                <input type="checkbox" name="switchboard_2fa_remember_device" value="1">
                                <?php printf(esc_html__('Remember this device for %d days', 'wpswitchboard'), $trusted_devices_days); ?>
                            </label>
                        <?php endif; ?>
                    </form>

                    <?php if ($backup_providers) : ?>
                        <div class="switchboard-2fa-providers">
                            <p><?php esc_html_e('Having problems?', 'wpswitchboard'); ?></p>
                            <ul>
                                <?php foreach ($backup_providers as $backup_provider_key => $backup_provider) : ?>
                                    <li>
                                        <a href="<?php echo esc_url(add_query_arg(array(
                                            'action' => 'validate_2fa',
                                            'provider' => $backup_provider_key,
                                            'wp-auth-id' => $user->ID,
                                            'wp-auth-nonce' => $login_nonce,
                                            'redirect_to' => $redirect_to,
                                            'rememberme' => $rememberme,
                                        ), site_url('wp-login.php', 'login'))); ?>">
                                            <?php echo esc_html($backup_provider->get_alternative_provider_label()); ?>
                                        </a>
                                    </li>
                                <?php endforeach; ?>
                            </ul>
                        </div>
                    <?php endif; ?>

                    <div class="switchboard-2fa-back">
                        <a href="<?php echo esc_url(wp_login_url()); ?>">&larr; <?php esc_html_e('Back to login', 'wpswitchboard'); ?></a>
                    </div>
                </div>
            </div>

            <script>
                (function() {
                    var form = document.querySelector('#loginform');
                    var inputEl = document.querySelector('input.authcode[inputmode="numeric"]');
                    var expectedLength = inputEl && inputEl.dataset.digits ? parseInt(inputEl.dataset.digits) : 0;

                    if (inputEl) {
                        inputEl.addEventListener('input', function() {
                            var value = this.value.replace(/[^0-9 ]/g, '').trimStart();
                            this.value = value;

                            if (expectedLength && value.replace(/ /g, '').length == expectedLength) {
                                if (typeof form.requestSubmit === 'function') {
                                    form.requestSubmit();
                                }
                            }
                        });
                    }
                })();
            </script>
            <?php wp_footer(); ?>
        </body>
        </html>
        <?php
    }

    /**
     * Handle the 2FA validation form submission.
     *
     * @since 1.0.0
     */
    public function login_form_validate_2fa() {
        if (!isset($_REQUEST['wp-auth-id'], $_REQUEST['wp-auth-nonce'])) {
            return;
        }

        $user_id = intval($_REQUEST['wp-auth-id']);
        $nonce = sanitize_text_field($_REQUEST['wp-auth-nonce']);
        $provider_key = isset($_REQUEST['provider']) ? sanitize_text_field($_REQUEST['provider']) : '';
        $redirect_to = isset($_REQUEST['redirect_to']) ? esc_url_raw($_REQUEST['redirect_to']) : admin_url();
        $is_post_request = ('POST' === strtoupper($_SERVER['REQUEST_METHOD']));

        $user = get_user_by('id', $user_id);

        if (!$user) {
            wp_safe_redirect(wp_login_url());
            exit;
        }

        if (!$this->verify_login_nonce($user->ID, $nonce)) {
            // Nonce expired or invalid - redirect with a helpful message.
            $login_url = add_query_arg(
                'switchboard_2fa_error',
                'session_expired',
                wp_login_url()
            );
            wp_safe_redirect($login_url);
            exit;
        }

        // Get the provider.
        $providers = $this->get_providers();
        $provider = isset($providers[$provider_key]) ? $providers[$provider_key] : $this->get_primary_provider_for_user($user);

        if (!$provider) {
            wp_safe_redirect(wp_login_url());
            exit;
        }

        // Allow provider to do pre-processing (e.g., resend code).
        $pre_process_result = $provider->pre_process_authentication($user);
        if ($pre_process_result) {
            $login_nonce = $this->create_login_nonce($user->ID);
            // If WP_Error returned (e.g., rate limited), show the error message.
            $error_msg = is_wp_error($pre_process_result) ? $pre_process_result->get_error_message() : '';
            $this->login_html($user, $login_nonce['key'], $redirect_to, $error_msg, $provider_key);
            exit;
        }

        // If it's not a POST request, just show the authentication form without validating.
        if (!$is_post_request) {
            $login_nonce = $this->create_login_nonce($user->ID);
            $this->login_html($user, $login_nonce['key'], $redirect_to, '', $provider_key);
            exit;
        }

        // Check rate limiting (only for POST requests attempting validation).
        if ($this->is_user_rate_limited($user)) {
            $time_delay = $this->get_user_time_delay($user);
            $error_msg = sprintf(
                __('Too many failed attempts. Please wait %d seconds before trying again.', 'wpswitchboard'),
                $time_delay
            );

            $login_nonce = $this->create_login_nonce($user->ID);
            $this->login_html($user, $login_nonce['key'], $redirect_to, $error_msg, $provider_key);
            exit;
        }

        // Validate the authentication.
        $validation_result = $provider->validate_authentication($user);

        // Check if validation succeeded (true means success).
        if (true === $validation_result) {
            // Success! Clear rate limiting and login.
            $this->delete_login_nonce($user->ID);
            delete_user_meta($user->ID, self::USER_RATE_LIMIT_KEY);
            delete_user_meta($user->ID, self::USER_FAILED_LOGIN_ATTEMPTS_KEY);

            // Remember device if requested.
            if (!empty($_REQUEST['switchboard_2fa_remember_device'])) {
                $this->add_trusted_device($user->ID);
            }

            $rememberme = isset($_REQUEST['rememberme']) && $_REQUEST['rememberme'];

            // Remove the cookie blocking filter before setting auth cookies.
            // This filter was added by filter_authenticate_block_cookies() during initial login.
            remove_filter('send_auth_cookies', '__return_false', PHP_INT_MAX);

            wp_set_current_user($user->ID);
            wp_set_auth_cookie($user->ID, $rememberme);

            /**
             * Fires after a user has been authenticated via 2FA.
             *
             * @param WP_User $user     User object.
             * @param object  $provider Provider object.
             */
            do_action('switchboard_2fa_user_authenticated', $user, $provider);

            // Apply login_redirect filter for compatibility with Login Redirect module
            $redirect_to = apply_filters('login_redirect', $redirect_to, $redirect_to, $user);

            wp_safe_redirect($redirect_to);
            exit;
        }

        // Authentication failed - determine error message.
        if (is_wp_error($validation_result)) {
            $error_msg = $validation_result->get_error_message();
            $error_code = $validation_result->get_error_code();

            // Don't count expired tokens or empty codes as failed attempts.
            if (!in_array($error_code, array('token_expired', 'no_token', 'empty_code'), true)) {
                $this->record_failed_attempt($user);
                $this->maybe_reset_password($user);
            }
        } else {
            // Generic failure (false returned).
            $error_msg = __('Invalid verification code.', 'wpswitchboard');
            $this->record_failed_attempt($user);
            $this->maybe_reset_password($user);
        }

        $login_nonce = $this->create_login_nonce($user->ID);
        $this->login_html($user, $login_nonce['key'], $redirect_to, $error_msg, $provider_key);
        exit;
    }

    /**
     * Record a failed authentication attempt.
     *
     * @since 1.0.0
     * @param WP_User $user User object.
     */
    public function record_failed_attempt($user) {
        $failed_attempts = (int) get_user_meta($user->ID, self::USER_FAILED_LOGIN_ATTEMPTS_KEY, true);
        update_user_meta($user->ID, self::USER_FAILED_LOGIN_ATTEMPTS_KEY, $failed_attempts + 1);
        update_user_meta($user->ID, self::USER_RATE_LIMIT_KEY, time());
    }

    /**
     * Get the user time delay for rate limiting.
     *
     * @since 1.0.0
     * @param WP_User $user User object.
     * @return int Time delay in seconds.
     */
    public function get_user_time_delay($user) {
        /**
         * Filter the minimum time duration between 2FA attempts.
         *
         * @param int $rate_limit The number of seconds between attempts.
         */
        $rate_limit = apply_filters('switchboard_2fa_rate_limit', 1);

        $user_failed_logins = (int) get_user_meta($user->ID, self::USER_FAILED_LOGIN_ATTEMPTS_KEY, true);
        if ($user_failed_logins) {
            $rate_limit = pow(2, $user_failed_logins) * $rate_limit;

            /**
             * Filter the maximum rate limit duration.
             *
             * @param int $max_rate_limit The maximum seconds a user might be locked out for.
             */
            $max_rate_limit = apply_filters('switchboard_2fa_max_rate_limit', 15 * MINUTE_IN_SECONDS);

            $rate_limit = min($max_rate_limit, $rate_limit);
        }

        return $rate_limit;
    }

    /**
     * Check if a user is rate limited.
     *
     * @since 1.0.0
     * @param WP_User $user User object.
     * @return bool
     */
    public function is_user_rate_limited($user) {
        $rate_limit = $this->get_user_time_delay($user);
        $last_failed = get_user_meta($user->ID, self::USER_RATE_LIMIT_KEY, true);

        if ($last_failed && $last_failed + $rate_limit > time()) {
            return true;
        }

        return false;
    }

    /**
     * Maybe reset the user's password after too many failed attempts.
     *
     * @since 1.0.0
     * @param WP_User $user User object.
     */
    public function maybe_reset_password($user) {
        /**
         * Filter the number of failed attempts before password reset.
         *
         * @param int $limit The number of attempts before password reset.
         */
        $limit = apply_filters('switchboard_2fa_failed_attempt_limit', 30);

        $failed_attempts = (int) get_user_meta($user->ID, self::USER_FAILED_LOGIN_ATTEMPTS_KEY, true);

        if ($failed_attempts >= $limit) {
            // Reset the password.
            $new_password = wp_generate_password(25);
            wp_set_password($new_password, $user->ID);

            // Mark that the password was reset.
            update_user_meta($user->ID, self::USER_PASSWORD_WAS_RESET_KEY, time());

            // Clear the failed attempts.
            delete_user_meta($user->ID, self::USER_FAILED_LOGIN_ATTEMPTS_KEY);

            // Send notification email.
            $this->send_password_reset_email($user);
        }
    }

    /**
     * Send password reset notification email to user and admin.
     *
     * @since 1.0.0
     * @param WP_User $user User object.
     */
    private function send_password_reset_email($user) {
        $site_name = wp_specialchars_decode(get_option('blogname'), ENT_QUOTES);

        // Email to user.
        $user_subject = sprintf(
            /* translators: %s: Site name */
            __('[%s] Password Reset Due to Failed Two-Factor Attempts', 'wpswitchboard'),
            $site_name
        );

        $user_message = sprintf(
            /* translators: %s: Site URL */
            __("Your password on %s has been reset because of too many failed two-factor authentication attempts.\n\nIf this was you, please use the password reset link to create a new password.\n\nIf this wasn't you, your account may have been compromised. Please reset your password immediately and review your account security.", 'wpswitchboard'),
            home_url()
        );

        wp_mail($user->user_email, $user_subject, $user_message);

        // Email to admin (filterable).
        /**
         * Filter whether to notify admin when a user's password is reset due to failed 2FA attempts.
         *
         * @param bool    $notify Whether to notify admin. Default true.
         * @param WP_User $user   The user whose password was reset.
         */
        $notify_admin = apply_filters('switchboard_2fa_notify_admin_password_reset', true, $user);

        if ($notify_admin) {
            $admin_email = get_option('admin_email');

            // Don't send duplicate if admin is the affected user.
            if ($admin_email !== $user->user_email) {
                $admin_subject = sprintf(
                    /* translators: %s: Site name */
                    __('[%s] Security Alert: User Password Reset Due to Failed 2FA', 'wpswitchboard'),
                    $site_name
                );

                $admin_message = sprintf(
                    /* translators: 1: User login, 2: User email, 3: User ID, 4: Site URL, 5: Client IP */
                    __("A user's password has been automatically reset due to too many failed two-factor authentication attempts.\n\nUser Details:\n- Username: %1\$s\n- Email: %2\$s\n- User ID: %3\$d\n\nSite: %4\$s\n\nClient IP: %5\$s\n\nThis could indicate a brute force attack on this account. You may want to:\n- Review the user's account for suspicious activity\n- Check server logs for additional failed attempts\n- Consider temporarily blocking the IP address if attacks persist", 'wpswitchboard'),
                    $user->user_login,
                    $user->user_email,
                    $user->ID,
                    home_url(),
                    $this->get_client_ip()
                );

                wp_mail($admin_email, $admin_subject, $admin_message);
            }
        }
    }

    /**
     * Show password reset notice on login.
     *
     * @since 1.0.0
     * @param WP_Error $errors Error object.
     * @return WP_Error
     */
    public function maybe_show_reset_password_notice($errors) {
        if ('incorrect_password' !== $errors->get_error_code()) {
            return $errors;
        }

        if (!isset($_POST['log'])) {
            return $errors;
        }

        $user_name = sanitize_user(wp_unslash($_POST['log']));
        $attempted_user = get_user_by('login', $user_name);
        if (!$attempted_user && strpos($user_name, '@') !== false) {
            $attempted_user = get_user_by('email', $user_name);
        }

        if (!$attempted_user) {
            return $errors;
        }

        $password_was_reset = get_user_meta($attempted_user->ID, self::USER_PASSWORD_WAS_RESET_KEY, true);

        if (!$password_was_reset) {
            return $errors;
        }

        $errors->remove('incorrect_password');
        $errors->add(
            'switchboard_2fa_password_reset',
            sprintf(
                __('Your password was reset because of too many failed two-factor attempts. Please <a href="%s">reset your password</a>.', 'wpswitchboard'),
                esc_url(add_query_arg('action', 'lostpassword', wp_login_url()))
            )
        );

        return $errors;
    }

    /**
     * Clear the password reset notice after password reset.
     *
     * @since 1.0.0
     * @param WP_User $user User object.
     */
    public function clear_password_reset_notice($user) {
        delete_user_meta($user->ID, self::USER_PASSWORD_WAS_RESET_KEY);
    }

    /**
     * Show a notice when the 2FA session has expired.
     *
     * @since 1.0.0
     * @param WP_Error $errors Login errors.
     * @return WP_Error
     */
    public function maybe_show_2fa_session_expired_notice($errors) {
        if (!isset($_GET['switchboard_2fa_error'])) {
            return $errors;
        }

        $error_code = sanitize_text_field($_GET['switchboard_2fa_error']);

        if ('session_expired' === $error_code) {
            $errors->add(
                'switchboard_2fa_session_expired',
                __('Your two-factor authentication session has expired. Please log in again.', 'wpswitchboard'),
                'message' // Show as info message, not error
            );
        }

        return $errors;
    }

    /**
     * Create a login nonce.
     *
     * @since 1.0.0
     * @param int $user_id User ID.
     * @return array|false
     */
    public function create_login_nonce($user_id) {
        $login_nonce = array(
            'user_id' => $user_id,
            'expiration' => time() + (10 * MINUTE_IN_SECONDS),
        );

        try {
            $login_nonce['key'] = bin2hex(random_bytes(32));
        } catch (Exception $ex) {
            $login_nonce['key'] = wp_hash($user_id . wp_rand() . microtime(), 'nonce');
        }

        $hashed_key = $this->hash_login_nonce($login_nonce);

        if ($hashed_key) {
            $login_nonce_stored = array(
                'expiration' => $login_nonce['expiration'],
                'key' => $hashed_key,
            );

            if (update_user_meta($user_id, self::USER_META_NONCE_KEY, $login_nonce_stored)) {
                return $login_nonce;
            }
        }

        return false;
    }

    /**
     * Hash a login nonce.
     *
     * @since 1.0.0
     * @param array $nonce Nonce array.
     * @return string|false
     */
    private function hash_login_nonce($nonce) {
        $message = wp_json_encode($nonce);

        if (!$message) {
            return false;
        }

        return wp_hash($message, 'nonce');
    }

    /**
     * Verify a login nonce.
     *
     * @since 1.0.0
     * @param int    $user_id User ID.
     * @param string $nonce   Nonce key.
     * @return bool
     */
    public function verify_login_nonce($user_id, $nonce) {
        $login_nonce = get_user_meta($user_id, self::USER_META_NONCE_KEY, true);

        if (!$login_nonce || empty($login_nonce['key']) || empty($login_nonce['expiration'])) {
            return false;
        }

        $unverified_nonce = array(
            'user_id' => $user_id,
            'expiration' => $login_nonce['expiration'],
            'key' => $nonce,
        );

        $unverified_hash = $this->hash_login_nonce($unverified_nonce);
        $hashes_match = $unverified_hash && hash_equals($login_nonce['key'], $unverified_hash);

        if ($hashes_match && time() < $login_nonce['expiration']) {
            return true;
        }

        $this->delete_login_nonce($user_id);

        return false;
    }

    /**
     * Delete a login nonce.
     *
     * @since 1.0.0
     * @param int $user_id User ID.
     * @return bool
     */
    public function delete_login_nonce($user_id) {
        return delete_user_meta($user_id, self::USER_META_NONCE_KEY);
    }

    /**
     * Get the client IP address.
     *
     * @since 1.0.0
     * @return string
     */
    private function get_client_ip() {
        $ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '0.0.0.0';
        return filter_var($ip, FILTER_VALIDATE_IP) ? $ip : '0.0.0.0';
    }

    /**
     * Display two-factor options on user profile.
     *
     * @since 1.0.0
     * @param WP_User $user User object.
     */
    public function user_two_factor_options($user) {
        $providers = $this->get_providers_for_user($user);
        $enabled_providers = $this->get_enabled_providers_for_user($user);
        $primary_provider = get_user_meta($user->ID, self::PROVIDER_USER_META_KEY, true);
        $is_own_profile = $user->ID === get_current_user_id();

        wp_nonce_field('switchboard_2fa_options', '_switchboard_2fa_nonce');
        ?>
        <h2 id="two-factor-options"><?php esc_html_e('Two-Factor Options', 'wpswitchboard'); ?> <span class="switchboard-badge" style="font-size: 10px; background: #3b82f6; color: #fff; padding: 2px 8px; border-radius: 4px; vertical-align: middle;">SWITCHBOARD</span></h2>
        <table class="form-table" role="presentation">
            <?php if (empty($providers)) : ?>
                <tr>
                    <th><?php esc_html_e('Status', 'wpswitchboard'); ?></th>
                    <td>
                        <p><?php esc_html_e('No two-factor providers are available.', 'wpswitchboard'); ?></p>
                    </td>
                </tr>
            <?php else : ?>
                <tr>
                    <th><?php esc_html_e('Two-Factor Methods', 'wpswitchboard'); ?></th>
                    <td>
                        <p class="description" style="margin-bottom: 16px;">
                            <?php esc_html_e('Select the methods you want to use for two-factor authentication.', 'wpswitchboard'); ?>
                        </p>
                        <?php foreach ($providers as $provider_key => $provider) :
                            $is_available = $provider->is_available_for_user($user);
                            $is_enabled = in_array($provider_key, $enabled_providers, true);
                            // TOTP requires setup before enabling, Email is always available, Backup Codes require generation.
                            $needs_setup = !$is_available && $provider_key !== 'Switchboard_2FA_Email';
                        ?>
                            <div style="margin-bottom: 24px; padding: 16px; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px;">
                                <label style="display: flex; align-items: center; gap: 10px; margin-bottom: 12px; <?php echo $needs_setup ? 'opacity: 0.7;' : ''; ?>">
                                    <input type="checkbox" name="switchboard_2fa_enabled_providers[]"
                                           id="enabled-<?php echo esc_attr($provider_key); ?>"
                                           value="<?php echo esc_attr($provider_key); ?>"
                                           <?php checked($is_enabled && ($is_available || $provider_key === 'Switchboard_2FA_Email')); ?>
                                           <?php disabled($needs_setup); ?> />
                                    <span style="font-weight: 600; font-size: 14px;"><?php echo esc_html($provider->get_label()); ?></span>
                                    <?php if ($is_available) : ?>
                                        <span style="background: #dcfce7; color: #166534; font-size: 11px; padding: 2px 8px; border-radius: 4px;"><?php esc_html_e('Configured', 'wpswitchboard'); ?></span>
                                    <?php elseif ($needs_setup) : ?>
                                        <span style="background: #fef3c7; color: #92400e; font-size: 11px; padding: 2px 8px; border-radius: 4px;"><?php esc_html_e('Setup Required', 'wpswitchboard'); ?></span>
                                    <?php endif; ?>
                                </label>
                                <div class="switchboard-2fa-provider-options" style="margin-left: 26px;">
                                    <?php
                                    /**
                                     * Fires to allow providers to add their own user options.
                                     *
                                     * @param WP_User $user User object.
                                     */
                                    do_action('switchboard_2fa_user_options_' . $provider_key, $user);
                                    ?>
                                </div>
                            </div>
                        <?php endforeach; ?>
                    </td>
                </tr>
                <?php
                $available_providers = $this->get_available_providers_for_user($user);
                if (count($available_providers) > 1) :
                    ?>
                    <tr>
                        <th>
                            <label for="switchboard_2fa_primary_provider"><?php esc_html_e('Primary Method', 'wpswitchboard'); ?></label>
                        </th>
                        <td>
                            <select name="switchboard_2fa_primary_provider" id="switchboard_2fa_primary_provider">
                                <?php foreach ($available_providers as $provider_key => $provider) : ?>
                                    <option value="<?php echo esc_attr($provider_key); ?>" <?php selected($primary_provider, $provider_key); ?>>
                                        <?php echo esc_html($provider->get_label()); ?>
                                    </option>
                                <?php endforeach; ?>
                            </select>
                            <p class="description">
                                <?php esc_html_e('This is the method you will be asked to use by default when logging in.', 'wpswitchboard'); ?>
                            </p>
                        </td>
                    </tr>
                <?php endif; ?>
            <?php endif; ?>
        </table>
        <?php
    }

    /**
     * Update two-factor options on profile save.
     *
     * @since 1.0.0
     * @param int $user_id User ID.
     */
    public function user_two_factor_options_update($user_id) {
        if (!isset($_POST['_switchboard_2fa_nonce']) || !wp_verify_nonce($_POST['_switchboard_2fa_nonce'], 'switchboard_2fa_options')) {
            return;
        }

        if (!current_user_can('edit_user', $user_id)) {
            return;
        }

        $user = get_user_by('id', $user_id);
        if (!$user) {
            return;
        }

        $providers = $this->get_providers_for_user($user);
        $requested_providers = isset($_POST['switchboard_2fa_enabled_providers']) ? array_map('sanitize_text_field', $_POST['switchboard_2fa_enabled_providers']) : array();

        // Only allow enabling providers that are actually configured/available.
        // This prevents enabling TOTP without verifying, etc.
        $enabled_providers = array();
        foreach ($requested_providers as $provider_key) {
            if (isset($providers[$provider_key])) {
                $provider = $providers[$provider_key];
                // Allow enabling if provider is available (configured) OR if it's Email (always available).
                if ($provider->is_available_for_user($user) || $provider_key === 'Switchboard_2FA_Email') {
                    $enabled_providers[] = $provider_key;
                }
            }
        }

        update_user_meta($user_id, self::ENABLED_PROVIDERS_USER_META_KEY, $enabled_providers);

        if (isset($_POST['switchboard_2fa_primary_provider'])) {
            update_user_meta($user_id, self::PROVIDER_USER_META_KEY, sanitize_text_field($_POST['switchboard_2fa_primary_provider']));
        }
    }

    /**
     * Enqueue admin scripts for profile page.
     *
     * @since 1.0.0
     * @param string $hook_suffix Admin page hook suffix.
     */
    public function enqueue_admin_scripts($hook_suffix) {
        if (!in_array($hook_suffix, array('profile.php', 'user-edit.php'), true)) {
            return;
        }

        wp_enqueue_script('jquery');

        wp_enqueue_script(
            'switchboard-2fa-qrcode',
            SWITCHBOARD_URL . 'assets/2fa/qrcode-generator/qrcode.min.js',
            array('jquery'),
            SWITCHBOARD_VERSION,
            true
        );
    }

    /**
     * Add 2FA status column to users list.
     *
     * @since 1.0.0
     * @param array $columns Columns array.
     * @return array
     */
    public function filter_manage_users_columns($columns) {
        $columns['switchboard_2fa'] = esc_html__('2FA', 'wpswitchboard');
        return $columns;
    }

    /**
     * Render 2FA status column.
     *
     * @since 1.0.0
     * @param string $value       Column value.
     * @param string $column_name Column name.
     * @param int    $user_id     User ID.
     * @return string
     */
    public function manage_users_custom_column($value, $column_name, $user_id) {
        if ('switchboard_2fa' !== $column_name) {
            return $value;
        }

        if ($this->is_user_using_two_factor($user_id)) {
            return '<span style="color: #16a34a; font-weight: 500;" title="' . esc_attr__('2FA Enabled', 'wpswitchboard') . '">
                <span style="display: inline-block; width: 8px; height: 8px; background: #16a34a; border-radius: 50%; margin-right: 4px;"></span>
                ' . esc_html__('On', 'wpswitchboard') . '
            </span>';
        }

        return '<span style="color: #9ca3af;" title="' . esc_attr__('2FA Not Enabled', 'wpswitchboard') . '">' . esc_html__('Off', 'wpswitchboard') . '</span>';
    }

    /**
     * Register REST API routes.
     *
     * @since 1.0.0
     */
    public function register_rest_routes() {
        register_rest_route('switchboard/v1', '/2fa/can-edit-user/(?P<user_id>\d+)', array(
            'methods' => WP_REST_Server::READABLE,
            'callback' => array($this, 'rest_can_edit_user'),
            'permission_callback' => '__return_true',
            'args' => array(
                'user_id' => array(
                    'required' => true,
                    'type' => 'integer',
                ),
            ),
        ));
    }

    /**
     * REST API callback to check if current user can edit 2FA options for another user.
     *
     * @since 1.0.0
     * @param WP_REST_Request $request Request object.
     * @return WP_REST_Response
     */
    public function rest_can_edit_user($request) {
        $user_id = $request->get_param('user_id');

        $can_edit = current_user_can('edit_user', $user_id) ||
                    get_current_user_id() === $user_id;

        return new WP_REST_Response(array('can_edit' => $can_edit), 200);
    }

    /**
     * Module activation.
     *
     * @since 1.0.0
     */
    public function activate() {
        // Nothing to do on activation since we use user meta, not custom tables.
    }

    /**
     * Module deactivation.
     *
     * @since 1.0.0
     */
    public function deactivate() {
        // Clean up scheduled events if any.
    }

    /**
     * Register data locations for cleanup.
     *
     * @since 1.0.0
     * @return array
     */
    public function register_data_locations() {
        return array(
            'options' => array('switchboard_2fa_settings'),
            'user_meta' => array(
                self::PROVIDER_USER_META_KEY,
                self::ENABLED_PROVIDERS_USER_META_KEY,
                self::USER_META_NONCE_KEY,
                self::USER_RATE_LIMIT_KEY,
                self::USER_FAILED_LOGIN_ATTEMPTS_KEY,
                self::USER_PASSWORD_WAS_RESET_KEY,
                self::USER_TRUSTED_DEVICES_KEY,
                '_switchboard_2fa_totp_key',
                '_switchboard_2fa_totp_last_successful_login',
                '_switchboard_2fa_email_token',
                '_switchboard_2fa_email_token_timestamp',
                '_switchboard_2fa_email_rate_limit',
                '_switchboard_2fa_backup_codes',
            ),
        );
    }

    /**
     * AJAX handler for searching users in settings panel.
     *
     * @since 1.0.0
     */
    public function ajax_search_users() {
        check_ajax_referer('switchboard_2fa_search_users', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => __('Permission denied.', 'wpswitchboard')));
        }

        $search = isset($_POST['search']) ? sanitize_text_field($_POST['search']) : '';

        if (strlen($search) < 2) {
            wp_send_json_success(array('users' => array()));
        }

        $users = get_users(array(
            'search' => '*' . $search . '*',
            'search_columns' => array('user_login', 'user_email', 'display_name'),
            'number' => 10,
            'orderby' => 'display_name',
            'order' => 'ASC',
        ));

        $results = array();
        foreach ($users as $user) {
            $results[] = array(
                'id' => $user->ID,
                'name' => $user->display_name,
                'email' => $user->user_email,
            );
        }

        wp_send_json_success(array('users' => $results));
    }

    /**
     * Render module settings panel.
     *
     * @since 1.0.0
     * @param array $settings Module settings.
     */
    public function render_settings($settings = array()) {
        $required_roles = isset($settings['required_roles']) ? (array) $settings['required_roles'] : array();
        $required_users = isset($settings['required_users']) ? (array) $settings['required_users'] : array();
        $enforce_all_users = isset($settings['enforce_all_users']) ? (bool) $settings['enforce_all_users'] : false;
        $grace_period = isset($settings['grace_period']) ? intval($settings['grace_period']) : 7;

        // Get all roles.
        $roles = wp_roles()->roles;

        // Get users for selection (limit to reasonable number).
        $users = get_users(array(
            'number' => 100,
            'orderby' => 'display_name',
            'order' => 'ASC',
        ));

        // Get 2FA stats.
        $total_users = count_users();
        $users_with_2fa = $this->get_users_with_2fa_count();
        ?>
        <input type="hidden" name="module" value="2fa">

        <!-- How It Works Info Box -->
        <div class="sb-sp-info-box sb-sp-info-box-info" style="margin-bottom: 24px;">
            <h4 class="sb-sp-heading-sm" style="margin: 0 0 8px 0;"><?php esc_html_e('How It Works', 'wpswitchboard'); ?></h4>
            <p class="sb-sp-description" style="margin: 0;"><?php esc_html_e('Two-factor authentication adds an extra layer of security by requiring users to verify their identity using an authenticator app, email code, or recovery codes. Configure which users are required to set up 2FA below.', 'wpswitchboard'); ?></p>
        </div>

        <!-- Simple Verification Alternative -->
        <div class="sb-sp-info-box" style="margin-bottom: 24px; background: #fefce8; border-color: #fde047;">
            <p class="sb-sp-description" style="margin: 0; color: #713f12;">
                <strong><?php esc_html_e('Looking for simpler OTP verification?', 'wpswitchboard'); ?></strong>
                <?php esc_html_e('If you don\'t need authenticator apps and just want email or SMS codes sent at login, use the', 'wpswitchboard'); ?>
                <a href="<?php echo esc_url(admin_url('admin.php?page=switchboard&module=otp-login')); ?>" style="color: #854d0e; font-weight: 500;"><?php esc_html_e('Email/SMS Verification (OTP)', 'wpswitchboard'); ?></a>
                <?php esc_html_e('module instead. It provides a familiar one-time password experience without requiring users to install any apps.', 'wpswitchboard'); ?>
            </p>
        </div>

        <!-- 2FA Stats -->
        <div class="sb-sp-group" style="margin-bottom: 24px; display: flex; gap: 16px;">
            <div style="flex: 1; padding: 16px; background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%); border-radius: 8px; border: 1px solid #bbf7d0;">
                <div style="font-size: 28px; font-weight: 700; color: #166534;"><?php echo esc_html($users_with_2fa); ?></div>
                <div style="font-size: 13px; color: #15803d; margin-top: 4px;"><?php esc_html_e('Users with 2FA', 'wpswitchboard'); ?></div>
            </div>
            <div style="flex: 1; padding: 16px; background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); border-radius: 8px; border: 1px solid #e2e8f0;">
                <div style="font-size: 28px; font-weight: 700; color: #334155;"><?php echo esc_html($total_users['total_users']); ?></div>
                <div style="font-size: 13px; color: #64748b; margin-top: 4px;"><?php esc_html_e('Total Users', 'wpswitchboard'); ?></div>
            </div>
        </div>

        <!-- Enforcement Settings Section -->
        <div class="sb-sp-group" style="margin-bottom: 8px;">
            <h3 class="sb-sp-heading" style="margin: 0;"><?php esc_html_e('Enforcement Settings', 'wpswitchboard'); ?></h3>
        </div>

        <div class="sb-sp-divider" style="margin-bottom: 24px;"></div>

        <!-- Enforce All Users Toggle -->
        <div class="sb-sp-group" style="margin-bottom: 24px;">
            <div class="sb-module-option-item sb-module-toggle-item">
                <div class="sb-module-option-content">
                    <div class="sb-module-toggle-header">
                        <strong class="sb-module-option-title"><?php esc_html_e('Require 2FA for All Users', 'wpswitchboard'); ?></strong>
                        <label class="sb-module-toggle">
                            <input type="checkbox" name="setting_enforce_all_users" value="1" <?php checked($enforce_all_users); ?> id="enforce_all_users_toggle">
                            <span class="sb-module-toggle-slider"></span>
                        </label>
                    </div>
                    <p class="sb-module-option-text"><?php esc_html_e('When enabled, every user on your site must set up two-factor authentication.', 'wpswitchboard'); ?></p>
                </div>
            </div>
        </div>

        <div id="selective-enforcement" style="<?php echo $enforce_all_users ? 'opacity: 0.5; pointer-events: none;' : ''; ?>">
            <!-- Role Enforcement -->
            <div class="sb-sp-group sb-sp-field-horizontal" style="margin-bottom: 24px;">
                <div class="sb-sp-field-horizontal-left">
                    <label class="sb-sp-label"><?php esc_html_e('Require 2FA for Roles', 'wpswitchboard'); ?></label>
                    <p class="sb-sp-description"><?php esc_html_e('Select which user roles must set up 2FA. Leave unchecked to make optional.', 'wpswitchboard'); ?></p>
                </div>
                <div style="display: flex; flex-wrap: wrap; gap: 8px; flex: 1;">
                    <?php foreach ($roles as $role_key => $role) :
                        $is_checked = in_array($role_key, $required_roles, true);
                    ?>
                        <label class="sb-sp-checkbox-field" style="display: inline-flex; align-items: center; gap: 8px; padding: 10px 14px; background: <?php echo $is_checked ? '#eff6ff' : '#f8fafc'; ?>; border: 1px solid <?php echo $is_checked ? '#3b82f6' : '#e2e8f0'; ?>; border-radius: 6px; cursor: pointer; font-size: 13px; transition: all 0.15s ease;">
                            <input type="checkbox" name="setting_required_roles[]" value="<?php echo esc_attr($role_key); ?>" <?php checked($is_checked); ?> class="sb-sp-checkbox" style="margin: 0;">
                            <span class="sb-sp-checkbox-label" style="color: <?php echo $is_checked ? '#1e40af' : '#374151'; ?>; font-weight: <?php echo $is_checked ? '500' : '400'; ?>;"><?php echo esc_html($role['name']); ?></span>
                        </label>
                    <?php endforeach; ?>
                </div>
            </div>

            <!-- Specific Users Enforcement -->
            <div class="sb-sp-group sb-sp-field-horizontal" style="margin-bottom: 24px;">
                <div class="sb-sp-field-horizontal-left">
                    <label class="sb-sp-label"><?php esc_html_e('Require 2FA for Specific Users', 'wpswitchboard'); ?></label>
                    <p class="sb-sp-description"><?php esc_html_e('Search and select individual users who must use 2FA.', 'wpswitchboard'); ?></p>
                </div>
                <div style="flex: 1;">
                    <!-- Selected Users Tags -->
                    <div id="sb-2fa-selected-users" style="display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px;">
                        <?php
                        // Get selected users data
                        if (!empty($required_users)) {
                            $selected_users_data = get_users(array('include' => $required_users));
                            foreach ($selected_users_data as $selected_user) :
                        ?>
                            <span class="sb-2fa-user-tag" data-user-id="<?php echo esc_attr($selected_user->ID); ?>" style="display: inline-flex; align-items: center; gap: 6px; padding: 6px 10px; background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 6px; font-size: 13px; color: #1e40af;">
                                <span><?php echo esc_html($selected_user->display_name); ?></span>
                                <button type="button" class="sb-2fa-remove-user" style="background: none; border: none; cursor: pointer; padding: 0; color: #3b82f6; font-size: 16px; line-height: 1;">&times;</button>
                                <input type="hidden" name="setting_required_users[]" value="<?php echo esc_attr($selected_user->ID); ?>">
                            </span>
                        <?php
                            endforeach;
                        }
                        ?>
                    </div>

                    <!-- Search Input -->
                    <div style="position: relative;">
                        <input type="text" id="sb-2fa-user-search" class="sb-sp-input" style="width: 100%;" placeholder="<?php esc_attr_e('Search users by name or email...', 'wpswitchboard'); ?>" autocomplete="off">
                        <div id="sb-2fa-search-results" style="display: none; position: absolute; top: 100%; left: 0; right: 0; background: #fff; border: 1px solid #e2e8f0; border-top: none; border-radius: 0 0 6px 6px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1); max-height: 200px; overflow-y: auto; z-index: 100;"></div>
                    </div>
                    <p class="sb-sp-description" style="margin-top: 8px;">
                        <?php esc_html_e('Type at least 2 characters to search.', 'wpswitchboard'); ?>
                    </p>
                </div>
            </div>
        </div>

        <!-- Grace Period -->
        <div class="sb-sp-group sb-sp-field-horizontal" style="margin-bottom: 24px;">
            <div class="sb-sp-field-horizontal-left">
                <label for="setting_grace_period" class="sb-sp-label"><?php esc_html_e('Grace Period', 'wpswitchboard'); ?></label>
                <p class="sb-sp-description"><?php esc_html_e('How many days users have to set up 2FA after enforcement is enabled.', 'wpswitchboard'); ?></p>
            </div>
            <div style="display: flex; align-items: center; gap: 8px;">
                <input type="number" name="setting_grace_period" id="setting_grace_period" value="<?php echo esc_attr($grace_period); ?>" min="0" max="30" class="sb-sp-input" style="width: 80px;">
                <span class="sb-sp-text-sm" style="color: #64748b;"><?php esc_html_e('days (0 = immediate)', 'wpswitchboard'); ?></span>
            </div>
        </div>

        <!-- Info Box -->
        <div class="sb-sp-info-box sb-sp-info-box-tip" style="margin-top: 24px;">
            <p class="sb-sp-description" style="margin: 0;">
                <strong><?php esc_html_e('Where to set up 2FA:', 'wpswitchboard'); ?></strong> <?php esc_html_e('Users configure their 2FA methods from their profile page under "Two-Factor Options".', 'wpswitchboard'); ?>
                <a href="<?php echo esc_url(admin_url('profile.php#two-factor-options')); ?>" style="margin-left: 4px;"><?php esc_html_e('View your profile →', 'wpswitchboard'); ?></a>
            </p>
        </div>

        <!-- Related Security Modules -->
        <div class="sb-sp-info-box" style="margin-top: 16px; background: #f8fafc; border-color: #e2e8f0;">
            <p class="sb-sp-description" style="margin: 0 0 8px 0;">
                <strong><?php esc_html_e('Recommended Security Modules:', 'wpswitchboard'); ?></strong>
                <?php esc_html_e('For maximum protection, consider enabling these related modules:', 'wpswitchboard'); ?>
            </p>
            <ul style="margin: 0; padding-left: 20px; font-size: 13px; color: #475569;">
                <li>
                    <a href="<?php echo esc_url(admin_url('admin.php?page=switchboard&module=disable-xmlrpc')); ?>" style="color: #3b82f6;">
                        <?php esc_html_e('Disable XML-RPC', 'wpswitchboard'); ?>
                    </a>
                    — <?php esc_html_e('Blocks remote login attempts via XML-RPC API', 'wpswitchboard'); ?>
                </li>
                <li>
                    <a href="<?php echo esc_url(admin_url('admin.php?page=switchboard&module=disable-rest-api')); ?>" style="color: #3b82f6;">
                        <?php esc_html_e('Disable REST API', 'wpswitchboard'); ?>
                    </a>
                    — <?php esc_html_e('Restricts REST API access for unauthenticated users', 'wpswitchboard'); ?>
                </li>
                <li>
                    <a href="<?php echo esc_url(admin_url('admin.php?page=switchboard&module=limit-login-attempts')); ?>" style="color: #3b82f6;">
                        <?php esc_html_e('Limit Login Attempts', 'wpswitchboard'); ?>
                    </a>
                    — <?php esc_html_e('Protects against brute-force password attacks', 'wpswitchboard'); ?>
                </li>
            </ul>
        </div>

        <script>
        (function() {
            var toggle = document.getElementById('enforce_all_users_toggle');
            var selective = document.getElementById('selective-enforcement');
            if (toggle && selective) {
                toggle.addEventListener('change', function() {
                    selective.style.opacity = this.checked ? '0.5' : '1';
                    selective.style.pointerEvents = this.checked ? 'none' : 'auto';
                });
            }

            // Update checkbox styling on change
            document.querySelectorAll('input[name="setting_required_roles[]"]').forEach(function(checkbox) {
                checkbox.addEventListener('change', function() {
                    var label = this.closest('label');
                    if (this.checked) {
                        label.style.background = '#eff6ff';
                        label.style.borderColor = '#3b82f6';
                        label.querySelector('span').style.color = '#1e40af';
                        label.querySelector('span').style.fontWeight = '500';
                    } else {
                        label.style.background = '#f8fafc';
                        label.style.borderColor = '#e2e8f0';
                        label.querySelector('span').style.color = '#374151';
                        label.querySelector('span').style.fontWeight = '400';
                    }
                });
            });

            // User search functionality
            var searchInput = document.getElementById('sb-2fa-user-search');
            var searchResults = document.getElementById('sb-2fa-search-results');
            var selectedUsers = document.getElementById('sb-2fa-selected-users');
            var searchTimeout = null;

            if (searchInput && searchResults && selectedUsers) {
                // Search on input
                searchInput.addEventListener('input', function() {
                    var query = this.value.trim();
                    clearTimeout(searchTimeout);

                    if (query.length < 2) {
                        searchResults.style.display = 'none';
                        return;
                    }

                    searchTimeout = setTimeout(function() {
                        searchUsers(query);
                    }, 300);
                });

                // Hide results on blur (with delay for click)
                searchInput.addEventListener('blur', function() {
                    setTimeout(function() {
                        searchResults.style.display = 'none';
                    }, 200);
                });

                // Show results on focus if has content
                searchInput.addEventListener('focus', function() {
                    if (searchResults.innerHTML && this.value.length >= 2) {
                        searchResults.style.display = 'block';
                    }
                });

                // Remove user tag
                selectedUsers.addEventListener('click', function(e) {
                    if (e.target.classList.contains('sb-2fa-remove-user')) {
                        e.target.closest('.sb-2fa-user-tag').remove();
                    }
                });

                function searchUsers(query) {
                    var xhr = new XMLHttpRequest();
                    xhr.open('POST', ajaxurl, true);
                    xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
                    xhr.onreadystatechange = function() {
                        if (xhr.readyState === 4 && xhr.status === 200) {
                            var response = JSON.parse(xhr.responseText);
                            if (response.success && response.data.users) {
                                renderResults(response.data.users);
                            }
                        }
                    };
                    xhr.send('action=switchboard_2fa_search_users&search=' + encodeURIComponent(query) + '&nonce=<?php echo wp_create_nonce('switchboard_2fa_search_users'); ?>');
                }

                function renderResults(users) {
                    var selectedIds = getSelectedUserIds();
                    var html = '';

                    if (users.length === 0) {
                        html = '<div style="padding: 12px; color: #64748b; font-size: 13px;"><?php echo esc_js(__('No users found', 'wpswitchboard')); ?></div>';
                    } else {
                        users.forEach(function(user) {
                            var isSelected = selectedIds.indexOf(user.id.toString()) !== -1;
                            if (!isSelected) {
                                html += '<div class="sb-2fa-search-result" data-user-id="' + user.id + '" data-user-name="' + escapeHtml(user.name) + '" style="padding: 10px 12px; cursor: pointer; border-bottom: 1px solid #f1f5f9; display: flex; align-items: center; gap: 10px;">';
                                html += '<div style="width: 32px; height: 32px; border-radius: 50%; background: #e2e8f0; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 600; color: #64748b;">' + user.name.charAt(0).toUpperCase() + '</div>';
                                html += '<div style="flex: 1;"><div style="font-weight: 500; color: #1e293b; font-size: 13px;">' + escapeHtml(user.name) + '</div><div style="font-size: 12px; color: #64748b;">' + escapeHtml(user.email) + '</div></div>';
                                html += '</div>';
                            }
                        });

                        if (!html) {
                            html = '<div style="padding: 12px; color: #64748b; font-size: 13px;"><?php echo esc_js(__('All matching users are already selected', 'wpswitchboard')); ?></div>';
                        }
                    }

                    searchResults.innerHTML = html;
                    searchResults.style.display = 'block';

                    // Add click handlers
                    searchResults.querySelectorAll('.sb-2fa-search-result').forEach(function(result) {
                        result.addEventListener('click', function() {
                            addUser(this.dataset.userId, this.dataset.userName);
                            searchInput.value = '';
                            searchResults.style.display = 'none';
                        });
                        result.addEventListener('mouseenter', function() {
                            this.style.background = '#f8fafc';
                        });
                        result.addEventListener('mouseleave', function() {
                            this.style.background = '#fff';
                        });
                    });
                }

                function getSelectedUserIds() {
                    var ids = [];
                    selectedUsers.querySelectorAll('.sb-2fa-user-tag').forEach(function(tag) {
                        ids.push(tag.dataset.userId);
                    });
                    return ids;
                }

                function addUser(userId, userName) {
                    var tag = document.createElement('span');
                    tag.className = 'sb-2fa-user-tag';
                    tag.dataset.userId = userId;
                    tag.style.cssText = 'display: inline-flex; align-items: center; gap: 6px; padding: 6px 10px; background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 6px; font-size: 13px; color: #1e40af;';
                    tag.innerHTML = '<span>' + escapeHtml(userName) + '</span><button type="button" class="sb-2fa-remove-user" style="background: none; border: none; cursor: pointer; padding: 0; color: #3b82f6; font-size: 16px; line-height: 1;">&times;</button><input type="hidden" name="setting_required_users[]" value="' + userId + '">';
                    selectedUsers.appendChild(tag);
                }

                function escapeHtml(text) {
                    var div = document.createElement('div');
                    div.textContent = text;
                    return div.innerHTML;
                }
            }
        })();
        </script>
        <?php
    }

    /**
     * Get count of users with 2FA enabled.
     *
     * @since 1.0.0
     * @return int
     */
    private function get_users_with_2fa_count() {
        global $wpdb;
        return (int) $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(DISTINCT user_id) FROM {$wpdb->usermeta} WHERE meta_key = %s AND meta_value != %s AND meta_value != %s",
                self::ENABLED_PROVIDERS_USER_META_KEY,
                '',
                'a:0:{}'
            )
        );
    }
}

// Initialize the singleton.
$switchboard_instance = Switchboard2fa::get_instance();
Switchboard_Data_Registry::get_instance()->auto_register($switchboard_instance, __FILE__);
