<?php
/**
 * Name: Email/SMS Verification (OTP)
 * Description: Simple OTP login verification via email or SMS - users receive a 6-digit one-time password after entering their credentials. No authenticator app required.
 * Category: Security
 * Icon: -
 * Version: 1.0
 * Database: yes
 * UseCases: [
 *   {
 *     "title": "Simple Two-Step Verification",
 *     "description": "Add an extra security layer without requiring users to install authenticator apps - codes are sent directly to email or phone."
 *   },
 *   {
 *     "title": "Familiar Experience",
 *     "description": "Users are already familiar with verification codes from banking and other services - no learning curve."
 *   },
 *   {
 *     "title": "Flexible Delivery",
 *     "description": "Send codes via email (built-in) or SMS through Twilio integration for instant delivery."
 *   }
 * ]
 * Added: 2025-01-01
 * Tags: otp, verification, sms, email, authentication, auth, security, login, one-time-password, code
 * Conflicts: 2fa
 * WhereToFind: {
 *   "location": "Login Page",
 *   "url": "wp-login.php",
 *   "instructions": "Verification code prompt appears after successful password entry",
 *   "newTab": false
 * }
 */

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

class SwitchboardOtpLogin {

    private $table_name;
    private $code_length = 6;
    private $default_expiry = 10; // minutes
    private $max_attempts = 5;
    private $max_verify_attempts_per_ip = 10; // max verification attempts per IP per hour
    private $encryption_key_option = 'switchboard_otp_encryption_key';

    /**
     * Encrypt sensitive data using AES-256-GCM
     *
     * @param string $data The data to encrypt
     * @return string|false Encrypted data (base64 encoded) or false on failure
     */
    private function encrypt_credential($data) {
        if (empty($data)) {
            return '';
        }

        $key = $this->get_encryption_key();
        if (!$key) {
            return false;
        }

        // Use AES-256-GCM for authenticated encryption
        $cipher = 'aes-256-gcm';
        $iv_length = openssl_cipher_iv_length($cipher);
        $iv = openssl_random_pseudo_bytes($iv_length);
        $tag = '';

        $encrypted = openssl_encrypt($data, $cipher, $key, OPENSSL_RAW_DATA, $iv, $tag, '', 16);
        if ($encrypted === false) {
            return false;
        }

        // Combine IV + tag + encrypted data and base64 encode
        return base64_encode($iv . $tag . $encrypted);
    }

    /**
     * Decrypt sensitive data
     *
     * @param string $encrypted_data The encrypted data (base64 encoded)
     * @return string|false Decrypted data or false on failure
     */
    private function decrypt_credential($encrypted_data) {
        if (empty($encrypted_data)) {
            return '';
        }

        // Check if data is encrypted (base64 and proper format)
        $decoded = base64_decode($encrypted_data, true);
        if ($decoded === false) {
            // Not encrypted, return as-is (for backwards compatibility)
            return $encrypted_data;
        }

        $key = $this->get_encryption_key();
        if (!$key) {
            return false;
        }

        $cipher = 'aes-256-gcm';
        $iv_length = openssl_cipher_iv_length($cipher);
        $tag_length = 16;

        // Check minimum length
        if (strlen($decoded) < $iv_length + $tag_length) {
            // Not encrypted properly, return as-is (backwards compatibility)
            return $encrypted_data;
        }

        // Extract IV, tag, and encrypted data
        $iv = substr($decoded, 0, $iv_length);
        $tag = substr($decoded, $iv_length, $tag_length);
        $encrypted = substr($decoded, $iv_length + $tag_length);

        $decrypted = openssl_decrypt($encrypted, $cipher, $key, OPENSSL_RAW_DATA, $iv, $tag);
        if ($decrypted === false) {
            // Decryption failed, might be plain text (backwards compatibility)
            return $encrypted_data;
        }

        return $decrypted;
    }

    /**
     * Get or generate encryption key
     *
     * @return string The encryption key
     */
    private function get_encryption_key() {
        // First, try to use LOGGED_IN_KEY if available (from wp-config.php)
        if (defined('LOGGED_IN_KEY') && strlen(LOGGED_IN_KEY) >= 32) {
            return hash('sha256', LOGGED_IN_KEY . 'switchboard_otp', true);
        }

        // Fallback: Generate and store a key
        $stored_key = get_option($this->encryption_key_option);
        if ($stored_key) {
            return base64_decode($stored_key);
        }

        // Generate new key
        $new_key = openssl_random_pseudo_bytes(32);
        update_option($this->encryption_key_option, base64_encode($new_key), false);
        return $new_key;
    }

    /**
     * Check if a credential value is encrypted
     *
     * @param string $value The value to check
     * @return bool True if encrypted
     */
    private function is_encrypted($value) {
        if (empty($value)) {
            return false;
        }
        $decoded = base64_decode($value, true);
        if ($decoded === false) {
            return false;
        }
        // Check if it looks like our encrypted format (IV + tag + data)
        $cipher = 'aes-256-gcm';
        $iv_length = openssl_cipher_iv_length($cipher);
        return strlen($decoded) > $iv_length + 16;
    }

    public function __construct() {
        global $wpdb;
        $this->table_name = $wpdb->prefix . 'switchboard_otp_codes';

        // Create table
        add_action('admin_init', [$this, 'maybe_create_table']);

        // Intercept login
        add_action('wp_login', [$this, 'initiate_otp_verification'], 10, 2);
        add_action('login_form_switchboard_otp', [$this, 'handle_otp_form']);

        // AJAX handlers
        add_action('wp_ajax_nopriv_switchboard_resend_otp', [$this, 'ajax_resend_otp']);
        add_action('wp_ajax_switchboard_resend_otp', [$this, 'ajax_resend_otp']);

        // Cleanup cron
        add_action('switchboard_otp_cleanup', [$this, 'cleanup_expired']);

        // User deletion cleanup - remove orphaned OTP records
        add_action('delete_user', [$this, 'cleanup_user_data']);
        add_action('deleted_user', [$this, 'cleanup_user_data']);

        // User profile
        add_action('show_user_profile', [$this, 'render_user_settings']);
        add_action('edit_user_profile', [$this, 'render_user_settings']);
        add_action('personal_options_update', [$this, 'save_user_settings']);
        add_action('edit_user_profile_update', [$this, 'save_user_settings']);

        // Module settings save filter (for encryption)
        add_filter('switchboard_before_save_settings', [$this, 'encrypt_settings_before_save'], 10, 2);

        // Display session expired message on login page
        add_filter('login_message', [$this, 'show_session_expired_message']);
    }

    /**
     * Show session expired message on login page
     */
    public function show_session_expired_message($message) {
        if (isset($_GET['switchboard_otp_expired'])) {
            $message .= '<div id="login_error" style="margin-bottom: 16px;">' .
                        esc_html__('Your verification session expired. Please log in again.', 'wpswitchboard') .
                        '</div>';
        }
        return $message;
    }

    /**
     * Create database table
     */
    public function maybe_create_table() {
        global $wpdb;
        if ($wpdb->get_var("SHOW TABLES LIKE '{$this->table_name}'") != $this->table_name) {
            $this->create_table();
        }
    }

    private function create_table() {
        global $wpdb;
        $charset_collate = $wpdb->get_charset_collate();

        $sql = "CREATE TABLE IF NOT EXISTS {$this->table_name} (
            id bigint(20) NOT NULL AUTO_INCREMENT,
            user_id bigint(20) NOT NULL,
            code_hash varchar(255) NOT NULL,
            expires_at datetime NOT NULL,
            created_at datetime NOT NULL,
            verified_at datetime DEFAULT NULL,
            attempts int DEFAULT 0,
            delivery_method varchar(20) DEFAULT 'email',
            ip_address varchar(45) DEFAULT NULL,
            PRIMARY KEY (id),
            KEY user_id (user_id),
            KEY expires_at (expires_at)
        ) $charset_collate;";

        require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
        dbDelta($sql);
    }

    /**
     * Check if OTP is required for user
     * User preference always takes priority to prevent lockouts
     */
    private function is_otp_required($user) {
        // Always honor user preference - they can disable OTP even if admin recommends it
        $user_enabled = get_user_meta($user->ID, 'switchboard_otp_enabled', true);
        return $user_enabled === 'yes';
    }

    /**
     * Check if admin recommends OTP for user's role
     * This is used for UI messaging only, not enforcement
     */
    private function is_otp_recommended_for_role($user) {
        $settings = get_option('switchboard_otp-login_settings', []);
        $required_roles = isset($settings['required_roles']) ? $settings['required_roles'] : [];

        if (empty($required_roles)) {
            return false;
        }

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

        return false;
    }

    /**
     * Initiate OTP verification after successful password login
     */
    public function initiate_otp_verification($user_login, $user) {
        // Skip if this is being called from OTP verification success (prevents redirect loop)
        if (defined('SWITCHBOARD_OTP_VERIFICATION_COMPLETE')) {
            return;
        }

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

        if (!$this->is_otp_required($user)) {
            return;
        }

        // Clear any existing pending OTP sessions for this user to prevent concurrent session issues
        $this->clear_pending_sessions($user->ID);

        // Generate and store OTP session token
        $token = bin2hex(random_bytes(32));
        $remember_me = isset($_POST['rememberme']) && $_POST['rememberme'];
        set_transient('switchboard_otp_pending_' . $token, [
            'user_id' => $user->ID,
            'time' => time(),
            'ip' => $this->get_client_ip(),
            'token' => $token, // Store token for cleanup
            'remember_me' => $remember_me // Preserve remember me preference
        ], 10 * MINUTE_IN_SECONDS);

        // Store the active token for this user (for cleanup of concurrent sessions)
        set_transient('switchboard_otp_active_token_' . $user->ID, $token, 10 * MINUTE_IN_SECONDS);

        // Clear auth cookie until OTP is verified
        wp_clear_auth_cookie();

        // Generate and send OTP
        $this->generate_and_send_otp($user);

        // Redirect to OTP verification form
        wp_safe_redirect(add_query_arg([
            'action' => 'switchboard_otp',
            'token' => $token,
            'redirect_to' => isset($_REQUEST['redirect_to']) ? esc_url($_REQUEST['redirect_to']) : admin_url()
        ], wp_login_url()));
        exit;
    }

    /**
     * Generate and send OTP code
     */
    private function generate_and_send_otp($user) {
        global $wpdb;

        // Invalidate any existing codes (use UTC timestamp for consistency)
        $wpdb->update(
            $this->table_name,
            ['verified_at' => gmdate('Y-m-d H:i:s')],
            ['user_id' => $user->ID, 'verified_at' => null],
            ['%s'],
            ['%d', '%s']
        );

        // Generate 6-digit code
        $code = str_pad(random_int(0, 999999), $this->code_length, '0', STR_PAD_LEFT);

        // Get settings
        $settings = get_option('switchboard_otp-login_settings', []);
        $expiry_minutes = isset($settings['expiry_minutes']) ? intval($settings['expiry_minutes']) : $this->default_expiry;
        $expiry_minutes = max(5, min(30, $expiry_minutes));

        $delivery_method = isset($settings['delivery_method']) ? $settings['delivery_method'] : 'email';

        // User preference for delivery method
        $user_method = get_user_meta($user->ID, 'switchboard_otp_method', true);
        if ($user_method && in_array($user_method, ['email', 'sms'])) {
            $delivery_method = $user_method;
        }

        // Store hashed code - use UTC for all timestamps
        $expires_at = gmdate('Y-m-d H:i:s', time() + ($expiry_minutes * MINUTE_IN_SECONDS));
        $created_at = gmdate('Y-m-d H:i:s');

        $wpdb->insert(
            $this->table_name,
            [
                'user_id' => $user->ID,
                'code_hash' => wp_hash_password($code),
                'expires_at' => $expires_at,
                'created_at' => $created_at,
                'delivery_method' => $delivery_method,
                'ip_address' => $this->get_client_ip()
            ],
            ['%d', '%s', '%s', '%s', '%s', '%s']
        );

        // Send code
        if (defined('WP_DEBUG') && WP_DEBUG) {
            error_log('Switchboard OTP: Sending code via ' . strtoupper($delivery_method) . ' to user ' . $user->user_login);
        }

        if ($delivery_method === 'sms') {
            $this->send_sms_otp($user, $code, $expiry_minutes);
        } else {
            $this->send_email_otp($user, $code, $expiry_minutes);
        }
    }

    /**
     * Send OTP via email
     */
    private function send_email_otp($user, $code, $expiry_minutes) {
        $site_name = get_bloginfo('name');
        if (empty($site_name)) {
            $site_name = 'WordPress';
        }

        $subject = sprintf(__('[%s] Your verification code', 'wpswitchboard'), $site_name);

        // Format code for readability
        $formatted_code = implode(' ', str_split($code, 3)); // "123 456"

        $message = sprintf(
            __("Hi %s,\n\nYour verification code is:\n\n%s\n\nThis code will expire in %d minutes.\n\nIf you didn't try to log in, please ignore this email and consider changing your password.\n\n---\n%s", 'wpswitchboard'),
            $user->display_name,
            $formatted_code,
            $expiry_minutes,
            $site_name
        );

        // Set proper headers for plain text email
        $headers = [
            'Content-Type: text/plain; charset=UTF-8',
            'From: ' . $site_name . ' <' . get_option('admin_email') . '>'
        ];

        $sent = wp_mail($user->user_email, $subject, $message, $headers);

        // Log if email failed to send
        if (!$sent) {
            error_log('Switchboard OTP: Failed to send email to ' . $user->user_email);
        }

        return $sent;
    }

    /**
     * Send OTP via SMS (Twilio)
     *
     * @param WP_User $user The user object
     * @param string $code The OTP code
     * @param int $expiry_minutes Code expiry time
     * @return array Result with 'success' and 'fallback' keys
     */
    private function send_sms_otp($user, $code, $expiry_minutes) {
        $settings = get_option('switchboard_otp-login_settings', []);

        // Decrypt Twilio credentials
        $twilio_sid = isset($settings['twilio_sid']) ? $this->decrypt_credential($settings['twilio_sid']) : '';
        $twilio_token = isset($settings['twilio_token']) ? $this->decrypt_credential($settings['twilio_token']) : '';
        $twilio_from = isset($settings['twilio_from']) ? $settings['twilio_from'] : '';

        $phone = get_user_meta($user->ID, 'switchboard_otp_phone', true);

        // Check if SMS is properly configured
        if (!$twilio_sid || !$twilio_token || !$twilio_from) {
            // Fall back to email - SMS not configured by admin
            $this->send_email_otp($user, $code, $expiry_minutes);
            $this->update_delivery_method_fallback($user->ID, 'email', 'sms_not_configured');
            return ['success' => true, 'fallback' => true, 'reason' => 'sms_not_configured'];
        }

        if (!$phone) {
            // Fall back to email - user has no phone number
            $this->send_email_otp($user, $code, $expiry_minutes);
            $this->update_delivery_method_fallback($user->ID, 'email', 'no_phone');
            return ['success' => true, 'fallback' => true, 'reason' => 'no_phone'];
        }

        // Normalize phone numbers to E.164 format if needed
        $twilio_from = $this->normalize_phone_number($twilio_from);
        $phone = $this->normalize_phone_number($phone);

        $site_name = get_bloginfo('name');
        if (empty($site_name)) {
            $site_name = 'WordPress';
        }
        $formatted_code = implode(' ', str_split($code, 3));
        $message = sprintf(__('%s - Your code: %s (expires in %d min)', 'wpswitchboard'), $site_name, $formatted_code, $expiry_minutes);

        // Send via Twilio
        $url = "https://api.twilio.com/2010-04-01/Accounts/{$twilio_sid}/Messages.json";

        $response = wp_remote_post($url, [
            'timeout' => 15,
            'headers' => [
                'Authorization' => 'Basic ' . base64_encode($twilio_sid . ':' . $twilio_token)
            ],
            'body' => [
                'From' => $twilio_from,
                'To' => $phone,
                'Body' => $message
            ]
        ]);

        // Handle Twilio API errors
        if (is_wp_error($response)) {
            error_log('Switchboard OTP: Twilio API connection error - ' . $response->get_error_message());
            $this->send_email_otp($user, $code, $expiry_minutes);
            $this->update_delivery_method_fallback($user->ID, 'email', 'api_error');
            return ['success' => true, 'fallback' => true, 'reason' => 'api_error'];
        }

        $response_code = wp_remote_retrieve_response_code($response);
        $response_body = json_decode(wp_remote_retrieve_body($response), true);

        // Check for Twilio error responses (non-2xx status codes)
        if ($response_code < 200 || $response_code >= 300) {
            $error_message = isset($response_body['message']) ? $response_body['message'] : 'Unknown error';
            $error_code = isset($response_body['code']) ? $response_body['code'] : $response_code;
            error_log("Switchboard OTP: Twilio error {$error_code} - {$error_message}");

            // Fall back to email
            $this->send_email_otp($user, $code, $expiry_minutes);
            $this->update_delivery_method_fallback($user->ID, 'email', 'twilio_error');
            return ['success' => true, 'fallback' => true, 'reason' => 'twilio_error', 'error' => $error_message];
        }

        // Log success
        if (defined('WP_DEBUG') && WP_DEBUG) {
            error_log('Switchboard OTP: SMS sent successfully to ' . $this->mask_phone($phone));
        }

        return ['success' => true, 'fallback' => false];
    }

    /**
     * Normalize phone number to E.164 format
     * Twilio requires phone numbers in E.164 format (+country code + number)
     *
     * @param string $phone The phone number to normalize
     * @return string The normalized phone number
     */
    private function normalize_phone_number($phone) {
        // Remove all non-digit characters except leading +
        $phone = trim($phone);

        // If it already starts with +, just clean it
        if (strpos($phone, '+') === 0) {
            return '+' . preg_replace('/[^0-9]/', '', substr($phone, 1));
        }

        // If it starts with 00 (international prefix), replace with +
        if (strpos($phone, '00') === 0) {
            return '+' . preg_replace('/[^0-9]/', '', substr($phone, 2));
        }

        // Otherwise, assume it needs a + prefix (user should have included country code)
        $cleaned = preg_replace('/[^0-9]/', '', $phone);

        // If it's a US number (10 digits), add +1
        if (strlen($cleaned) === 10) {
            return '+1' . $cleaned;
        }

        // Otherwise just add + prefix
        return '+' . $cleaned;
    }

    /**
     * Update the delivery method in the database when fallback occurs
     *
     * @param int $user_id The user ID
     * @param string $actual_method The actual delivery method used
     * @param string $reason Reason for fallback
     */
    private function update_delivery_method_fallback($user_id, $actual_method, $reason) {
        global $wpdb;

        // Update the most recent OTP record with the actual delivery method
        $wpdb->query($wpdb->prepare(
            "UPDATE {$this->table_name}
             SET delivery_method = %s
             WHERE user_id = %d AND verified_at IS NULL
             ORDER BY created_at DESC LIMIT 1",
            $actual_method . '_fallback',
            $user_id
        ));

        // Store fallback info in transient for display to user
        set_transient('switchboard_otp_fallback_' . $user_id, [
            'original' => 'sms',
            'actual' => $actual_method,
            'reason' => $reason
        ], 10 * MINUTE_IN_SECONDS);
    }

    /**
     * Handle OTP verification form
     */
    public function handle_otp_form() {
        $token = isset($_GET['token']) ? sanitize_text_field($_GET['token']) : '';
        $redirect_to = isset($_GET['redirect_to']) ? esc_url_raw($_GET['redirect_to']) : admin_url();

        // Validate token
        $pending = get_transient('switchboard_otp_pending_' . $token);
        if (!$pending) {
            // Session expired - redirect to login with message
            wp_safe_redirect(add_query_arg([
                'switchboard_otp_expired' => '1'
            ], wp_login_url()));
            exit;
        }

        $user = get_userdata($pending['user_id']);
        if (!$user) {
            delete_transient('switchboard_otp_pending_' . $token);
            wp_safe_redirect(wp_login_url());
            exit;
        }

        $error = '';
        $client_ip = $this->get_client_ip();

        // Check IP-based rate limiting before processing
        $rate_check = $this->check_verification_rate_limit($client_ip);
        if ($rate_check === 'rate_limited') {
            $error = __('Too many verification attempts. Please wait an hour before trying again.', 'wpswitchboard');
        }

        // Handle form submission
        if (empty($error) && $_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['switchboard_otp_code'])) {
            if (!wp_verify_nonce($_POST['_wpnonce'] ?? '', 'switchboard_otp_verify')) {
                $error = __('Security check failed. Please try again.', 'wpswitchboard');
            } else {
                // CAPTCHA validation filter (reCAPTCHA, Turnstile, Simple CAPTCHA)
                $captcha_valid = apply_filters('switchboard_otp_validate', true, $pending['user_id']);
                if (is_wp_error($captcha_valid)) {
                    $error = $captcha_valid->get_error_message();
                } elseif ($captcha_valid === false) {
                    $error = __('CAPTCHA verification failed. Please try again.', 'wpswitchboard');
                }

                if (empty($error)) {
                    // Increment IP-based attempt counter
                    $this->increment_verification_attempts($client_ip);

                    $code = preg_replace('/[^0-9]/', '', sanitize_text_field($_POST['switchboard_otp_code']));

                    $result = $this->verify_otp($pending['user_id'], $code);

                    if ($result === true) {
                        // Success - log user in
                        delete_transient('switchboard_otp_pending_' . $token);
                        delete_transient('switchboard_otp_active_token_' . $user->ID);

                        // Use remember_me preference from original login
                        $remember_me = isset($pending['remember_me']) ? $pending['remember_me'] : false;

                        wp_clear_auth_cookie();
                        wp_set_current_user($user->ID);
                        wp_set_auth_cookie($user->ID, $remember_me);

                        // Define flag to prevent OTP module from re-triggering on wp_login action
                        define('SWITCHBOARD_OTP_VERIFICATION_COMPLETE', true);

                        do_action('wp_login', $user->user_login, $user);

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

                        wp_redirect($redirect_to);
                        exit;
                    } elseif ($result === 'expired') {
                        $error = __('This code has expired. Please request a new one.', 'wpswitchboard');
                    } elseif ($result === 'max_attempts') {
                        $error = __('Too many incorrect attempts for this code. Please request a new code.', 'wpswitchboard');
                    } else {
                        $error = __('Incorrect verification code. Please check and try again.', 'wpswitchboard');
                    }
                }
            }
        }

        // Get settings for expiry calculation
        $settings = get_option('switchboard_otp-login_settings', []);
        $expiry_minutes = isset($settings['expiry_minutes']) ? intval($settings['expiry_minutes']) : $this->default_expiry;

        // Get current OTP info
        global $wpdb;
        $current_otp = $wpdb->get_row($wpdb->prepare(
            "SELECT * FROM {$this->table_name} WHERE user_id = %d AND verified_at IS NULL ORDER BY created_at DESC LIMIT 1",
            $pending['user_id']
        ));

        $delivery_method = $current_otp ? $current_otp->delivery_method : 'email';

        // Calculate actual remaining time for expiry timer
        $remaining_seconds = $expiry_minutes * 60; // Default
        if ($current_otp) {
            $expires_timestamp = strtotime($current_otp->expires_at . ' UTC');
            $remaining_seconds = max(0, $expires_timestamp - time());
        }

        // Check for fallback notification
        $fallback_info = get_transient('switchboard_otp_fallback_' . $pending['user_id']);
        $fallback_notice = '';
        if ($fallback_info) {
            delete_transient('switchboard_otp_fallback_' . $pending['user_id']);
            // Show notice that SMS failed and email was used instead
            $fallback_notice = __('SMS delivery failed. Code sent to your email instead.', 'wpswitchboard');
            $delivery_method = 'email'; // Override to show email UI
        }

        // Handle fallback delivery method indicator in database
        if (strpos($delivery_method, '_fallback') !== false) {
            $delivery_method = str_replace('_fallback', '', $delivery_method);
        }

        $masked_destination = $delivery_method === 'sms'
            ? $this->mask_phone(get_user_meta($pending['user_id'], 'switchboard_otp_phone', true))
            : $this->mask_email($user->user_email);

        $this->render_otp_form($token, $redirect_to, $user, $error, $delivery_method, $masked_destination, $fallback_notice, $remaining_seconds);
        exit;
    }

    /**
     * Verify OTP code
     */
    private function verify_otp($user_id, $code) {
        global $wpdb;

        // Get the latest unused code
        $otp = $wpdb->get_row($wpdb->prepare(
            "SELECT * FROM {$this->table_name}
             WHERE user_id = %d AND verified_at IS NULL
             ORDER BY created_at DESC LIMIT 1",
            $user_id
        ));

        if (!$otp) {
            return 'expired';
        }

        // Check expiry (expires_at is stored in UTC, append UTC to ensure correct parsing)
        if (strtotime($otp->expires_at . ' UTC') < time()) {
            return 'expired';
        }

        // Check attempts
        if ($otp->attempts >= $this->max_attempts) {
            return 'max_attempts';
        }

        // Increment attempts
        $wpdb->update(
            $this->table_name,
            ['attempts' => $otp->attempts + 1],
            ['id' => $otp->id],
            ['%d'],
            ['%d']
        );

        // Verify code
        if (wp_check_password($code, $otp->code_hash)) {
            // Mark as verified (use UTC)
            $wpdb->update(
                $this->table_name,
                ['verified_at' => gmdate('Y-m-d H:i:s')],
                ['id' => $otp->id],
                ['%s'],
                ['%d']
            );
            return true;
        }

        return false;
    }

    /**
     * Check if IP is rate limited for verification attempts
     *
     * @param string $ip The client IP address
     * @return bool|string True if allowed, error message if rate limited
     */
    private function check_verification_rate_limit($ip) {
        $rate_key = 'switchboard_otp_verify_' . md5($ip);
        $attempts = get_transient($rate_key);

        if ($attempts && $attempts >= $this->max_verify_attempts_per_ip) {
            return 'rate_limited';
        }

        return true;
    }

    /**
     * Increment verification attempt counter for IP
     *
     * @param string $ip The client IP address
     */
    private function increment_verification_attempts($ip) {
        $rate_key = 'switchboard_otp_verify_' . md5($ip);
        $attempts = get_transient($rate_key);
        set_transient($rate_key, ($attempts ? $attempts + 1 : 1), HOUR_IN_SECONDS);
    }

    /**
     * Clear pending OTP sessions for a user
     * This prevents issues with concurrent login attempts from multiple tabs
     * Note: OTP code invalidation is handled separately in generate_and_send_otp()
     *
     * @param int $user_id The user ID
     */
    private function clear_pending_sessions($user_id) {
        // Get the currently active token for this user
        $active_token = get_transient('switchboard_otp_active_token_' . $user_id);

        if ($active_token) {
            // Delete the pending session transient
            delete_transient('switchboard_otp_pending_' . $active_token);
            // Delete the active token tracker
            delete_transient('switchboard_otp_active_token_' . $user_id);
        }

        // Note: We do NOT invalidate OTP codes here - that's done in generate_and_send_otp()
        // to avoid race conditions where the new code gets invalidated
    }

    /**
     * Render OTP verification form
     *
     * @param string $token Session token
     * @param string $redirect_to Redirect URL after login
     * @param WP_User $user User object
     * @param string $error Error message to display
     * @param string $delivery_method 'email' or 'sms'
     * @param string $masked_destination Masked email or phone
     * @param string $fallback_notice Optional notice about SMS fallback to email
     * @param int $remaining_seconds Seconds remaining until code expires
     */
    private function render_otp_form($token, $redirect_to, $user, $error, $delivery_method, $masked_destination, $fallback_notice = '', $remaining_seconds = 600) {
        $settings = get_option('switchboard_otp-login_settings', []);
        $expiry_minutes = isset($settings['expiry_minutes']) ? intval($settings['expiry_minutes']) : $this->default_expiry;
        ?>
        <!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 _e('Verify Your Identity', '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-otp-wrapper {
                    display: flex;
                    justify-content: center;
                    align-items: center;
                    min-height: 100vh;
                    padding: 20px;
                    box-sizing: border-box;
                }
                .switchboard-otp-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-otp-header {
                    text-align: center;
                    margin-bottom: 24px;
                }
                .switchboard-otp-header .icon {
                    font-size: 48px;
                    margin-bottom: 16px;
                }
                .switchboard-otp-header h1 {
                    font-size: 24px;
                    font-weight: 600;
                    color: #1e293b;
                    margin: 0 0 8px;
                }
                .switchboard-otp-header p {
                    color: #64748b;
                    font-size: 14px;
                    margin: 0;
                }
                .switchboard-otp-destination {
                    background: #f8fafc;
                    border-radius: 6px;
                    padding: 12px 16px;
                    text-align: center;
                    margin-bottom: 24px;
                    font-size: 14px;
                    color: #374151;
                }
                .switchboard-otp-destination strong {
                    font-family: 'SF Mono', Monaco, monospace;
                }
                .switchboard-otp-input-group {
                    display: flex;
                    gap: 8px;
                    justify-content: center;
                    margin-bottom: 24px;
                }
                .switchboard-otp-input {
                    width: 48px;
                    height: 56px;
                    text-align: center;
                    font-size: 24px;
                    font-weight: 600;
                    border: 2px solid #e2e8f0;
                    border-radius: 8px;
                    outline: none;
                    transition: all 0.15s ease;
                }
                .switchboard-otp-input:focus {
                    border-color: #3b82f6;
                    box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
                }
                .switchboard-otp-input.error {
                    border-color: #ef4444;
                    animation: shake 0.4s ease;
                }
                @keyframes shake {
                    0%, 100% { transform: translateX(0); }
                    25% { transform: translateX(-4px); }
                    75% { transform: translateX(4px); }
                }
                .switchboard-otp-hidden-input {
                    position: absolute;
                    opacity: 0;
                    pointer-events: none;
                }
                .switchboard-otp-submit {
                    width: 100%;
                    padding: 14px 24px;
                    background: #3b82f6;
                    color: #fff;
                    border: none;
                    border-radius: 8px;
                    font-size: 16px;
                    font-weight: 600;
                    cursor: pointer;
                    transition: background 0.15s ease;
                }
                .switchboard-otp-submit:hover {
                    background: #2563eb;
                }
                .switchboard-otp-submit:disabled {
                    background: #94a3b8;
                    cursor: not-allowed;
                }
                .switchboard-otp-error {
                    background: #fef2f2;
                    border: 1px solid #fecaca;
                    color: #dc2626;
                    padding: 12px 16px;
                    border-radius: 6px;
                    margin-bottom: 16px;
                    font-size: 14px;
                    text-align: center;
                }
                .switchboard-otp-resend {
                    text-align: center;
                    margin-top: 24px;
                    padding-top: 24px;
                    border-top: 1px solid #e5e7eb;
                }
                .switchboard-otp-resend p {
                    color: #64748b;
                    font-size: 13px;
                    margin: 0 0 8px;
                }
                .switchboard-otp-resend-btn {
                    background: none;
                    border: none;
                    color: #3b82f6;
                    font-size: 14px;
                    font-weight: 500;
                    cursor: pointer;
                    padding: 0;
                }
                .switchboard-otp-resend-btn:hover {
                    text-decoration: underline;
                }
                .switchboard-otp-resend-btn:disabled {
                    color: #94a3b8;
                    cursor: not-allowed;
                    text-decoration: none;
                }
                .switchboard-otp-timer {
                    display: inline-block;
                    color: #64748b;
                    font-size: 13px;
                }
                .switchboard-otp-back {
                    display: block;
                    text-align: center;
                    margin-top: 16px;
                    color: #64748b;
                    text-decoration: none;
                    font-size: 13px;
                }
                .switchboard-otp-back:hover {
                    color: #374151;
                }
                .switchboard-otp-success {
                    background: #f0fdf4;
                    border: 1px solid #bbf7d0;
                    color: #16a34a;
                    padding: 12px 16px;
                    border-radius: 6px;
                    margin-bottom: 16px;
                    font-size: 14px;
                    text-align: center;
                }
                .switchboard-otp-submit.loading {
                    position: relative;
                    color: transparent;
                }
                .switchboard-otp-submit.loading::after {
                    content: '';
                    position: absolute;
                    width: 20px;
                    height: 20px;
                    top: 50%;
                    left: 50%;
                    margin-left: -10px;
                    margin-top: -10px;
                    border: 2px solid transparent;
                    border-top-color: #fff;
                    border-radius: 50%;
                    animation: spin 0.8s linear infinite;
                }
                @keyframes spin {
                    to { transform: rotate(360deg); }
                }
                .switchboard-otp-expiry {
                    text-align: center;
                    font-size: 13px;
                    color: #64748b;
                    margin-bottom: 16px;
                }
                .switchboard-otp-expiry.warning {
                    color: #d97706;
                }
                .switchboard-otp-expiry.danger {
                    color: #dc2626;
                }
            </style>
        </head>
        <body>
            <div class="switchboard-otp-wrapper">
                <div class="switchboard-otp-box">
                    <div class="switchboard-otp-header">
                        <div class="icon"><span class="dashicons dashicons-<?php echo $delivery_method === 'sms' ? 'smartphone' : 'email'; ?>" style="font-size: 36px; width: 36px; height: 36px;"></span></div>
                        <h1><?php _e('Enter verification code', 'wpswitchboard'); ?></h1>
                        <p><?php _e('We sent a code to verify it\'s you', 'wpswitchboard'); ?></p>
                    </div>

                    <div class="switchboard-otp-destination">
                        <?php if ($delivery_method === 'sms'): ?>
                            <?php _e('Code sent via SMS to', 'wpswitchboard'); ?>
                        <?php else: ?>
                            <?php _e('Code sent to', 'wpswitchboard'); ?>
                        <?php endif; ?>
                        <br><strong><?php echo esc_html($masked_destination); ?></strong>
                    </div>

                    <?php if ($fallback_notice): ?>
                        <div class="switchboard-otp-warning" style="background: #fef3c7; border: 1px solid #fcd34d; color: #92400e; padding: 12px 16px; border-radius: 6px; margin-bottom: 16px; font-size: 14px; text-align: center;">
                            <?php echo esc_html($fallback_notice); ?>
                        </div>
                    <?php endif; ?>

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

                    <div id="switchboard-otp-success" class="switchboard-otp-success" style="display: none;">
                        <?php _e('New code sent!', 'wpswitchboard'); ?>
                    </div>

                    <div class="switchboard-otp-expiry" id="switchboard-otp-expiry">
                        <?php
                        $display_mins = floor($remaining_seconds / 60);
                        $display_secs = $remaining_seconds % 60;
                        ?>
                        <?php _e('Code expires in', 'wpswitchboard'); ?> <span id="switchboard-expiry-time"><?php echo $display_mins . ':' . str_pad($display_secs, 2, '0', STR_PAD_LEFT); ?></span>
                    </div>

                    <form method="post" id="switchboard-otp-form">
                        <?php wp_nonce_field('switchboard_otp_verify'); ?>

                        <?php
                        // Hook for CAPTCHA integration (reCAPTCHA, Turnstile, Simple CAPTCHA)
                        do_action('switchboard_otp_form_fields', 'otp-verify');
                        ?>

                        <div class="switchboard-otp-input-group">
                            <input type="text" class="switchboard-otp-input" maxlength="1" pattern="[0-9]" inputmode="numeric" autocomplete="off" data-index="0">
                            <input type="text" class="switchboard-otp-input" maxlength="1" pattern="[0-9]" inputmode="numeric" autocomplete="off" data-index="1">
                            <input type="text" class="switchboard-otp-input" maxlength="1" pattern="[0-9]" inputmode="numeric" autocomplete="off" data-index="2">
                            <input type="text" class="switchboard-otp-input" maxlength="1" pattern="[0-9]" inputmode="numeric" autocomplete="off" data-index="3">
                            <input type="text" class="switchboard-otp-input" maxlength="1" pattern="[0-9]" inputmode="numeric" autocomplete="off" data-index="4">
                            <input type="text" class="switchboard-otp-input" maxlength="1" pattern="[0-9]" inputmode="numeric" autocomplete="off" data-index="5">
                        </div>
                        <input type="hidden" name="switchboard_otp_code" id="switchboard-otp-code">

                        <button type="submit" class="switchboard-otp-submit" id="switchboard-otp-submit" disabled>
                            <?php _e('Verify', 'wpswitchboard'); ?>
                        </button>
                    </form>

                    <div class="switchboard-otp-resend">
                        <p><?php _e("Didn't receive a code?", 'wpswitchboard'); ?></p>
                        <button type="button" class="switchboard-otp-resend-btn" id="switchboard-otp-resend" disabled>
                            <?php _e('Resend code', 'wpswitchboard'); ?>
                        </button>
                        <span class="switchboard-otp-timer" id="switchboard-otp-timer">(0:30)</span>
                    </div>

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

            <script>
            (function() {
                var inputs = document.querySelectorAll('.switchboard-otp-input');
                var hiddenInput = document.getElementById('switchboard-otp-code');
                var submitBtn = document.getElementById('switchboard-otp-submit');
                var resendBtn = document.getElementById('switchboard-otp-resend');
                var timerEl = document.getElementById('switchboard-otp-timer');
                var successEl = document.getElementById('switchboard-otp-success');
                var form = document.getElementById('switchboard-otp-form');
                var expiryEl = document.getElementById('switchboard-otp-expiry');
                var expiryTimeEl = document.getElementById('switchboard-expiry-time');

                var cooldown = 30;
                var timer;
                var expirySeconds = <?php echo intval($remaining_seconds); ?>;
                var fullExpirySeconds = <?php echo intval($expiry_minutes) * 60; ?>; // Used for resend reset
                var expiryTimer;
                var isSubmitting = false;

                function updateCode() {
                    var code = '';
                    inputs.forEach(function(input) {
                        code += input.value;
                    });
                    hiddenInput.value = code;
                    submitBtn.disabled = code.length !== 6 || isSubmitting;
                }

                function clearInputs() {
                    inputs.forEach(function(input) {
                        input.value = '';
                        input.classList.remove('error');
                    });
                    inputs[0].focus();
                    updateCode();
                }

                function showLoading() {
                    isSubmitting = true;
                    submitBtn.classList.add('loading');
                    submitBtn.disabled = true;
                }

                function hideLoading() {
                    isSubmitting = false;
                    submitBtn.classList.remove('loading');
                    updateCode();
                }

                function startCooldown() {
                    cooldown = 30;
                    resendBtn.disabled = true;
                    timerEl.style.display = 'inline-block';
                    updateResendTimer();

                    timer = setInterval(function() {
                        cooldown--;
                        if (cooldown <= 0) {
                            clearInterval(timer);
                            resendBtn.disabled = false;
                            timerEl.style.display = 'none';
                        } else {
                            updateResendTimer();
                        }
                    }, 1000);
                }

                function updateResendTimer() {
                    var mins = Math.floor(cooldown / 60);
                    var secs = cooldown % 60;
                    timerEl.textContent = '(' + mins + ':' + (secs < 10 ? '0' : '') + secs + ')';
                }

                function startExpiryTimer() {
                    updateExpiryDisplay();
                    expiryTimer = setInterval(function() {
                        expirySeconds--;
                        if (expirySeconds <= 0) {
                            clearInterval(expiryTimer);
                            expiryEl.innerHTML = '<?php echo esc_js(__('Code expired. Please request a new one.', 'wpswitchboard')); ?>';
                            expiryEl.className = 'switchboard-otp-expiry danger';
                            // Disable form
                            submitBtn.disabled = true;
                            inputs.forEach(function(input) { input.disabled = true; });
                        } else {
                            updateExpiryDisplay();
                        }
                    }, 1000);
                }

                function updateExpiryDisplay() {
                    var mins = Math.floor(expirySeconds / 60);
                    var secs = expirySeconds % 60;
                    expiryTimeEl.textContent = mins + ':' + (secs < 10 ? '0' : '') + secs;

                    // Update color based on time remaining
                    if (expirySeconds <= 60) {
                        expiryEl.className = 'switchboard-otp-expiry danger';
                    } else if (expirySeconds <= 180) {
                        expiryEl.className = 'switchboard-otp-expiry warning';
                    } else {
                        expiryEl.className = 'switchboard-otp-expiry';
                    }
                }

                function resetExpiryTimer() {
                    clearInterval(expiryTimer);
                    expirySeconds = fullExpirySeconds; // Use full expiry time for new code
                    // Re-enable form if it was disabled
                    inputs.forEach(function(input) { input.disabled = false; });
                    startExpiryTimer();
                }

                inputs.forEach(function(input, index) {
                    input.addEventListener('input', function(e) {
                        var value = e.target.value.replace(/[^0-9]/g, '');
                        e.target.value = value;

                        if (value && index < inputs.length - 1) {
                            inputs[index + 1].focus();
                        }

                        updateCode();

                        // Auto-submit when all filled
                        if (hiddenInput.value.length === 6 && !isSubmitting) {
                            showLoading();
                            form.submit();
                        }
                    });

                    input.addEventListener('keydown', function(e) {
                        if (e.key === 'Backspace' && !e.target.value && index > 0) {
                            inputs[index - 1].focus();
                        }
                    });

                    input.addEventListener('paste', function(e) {
                        e.preventDefault();
                        var paste = (e.clipboardData || window.clipboardData).getData('text');
                        var digits = paste.replace(/[^0-9]/g, '').slice(0, 6);

                        for (var i = 0; i < digits.length && i < inputs.length; i++) {
                            inputs[i].value = digits[i];
                        }
                        updateCode();

                        if (digits.length >= inputs.length && !isSubmitting) {
                            inputs[inputs.length - 1].focus();
                            showLoading();
                            form.submit();
                        } else if (digits.length > 0) {
                            inputs[digits.length].focus();
                        }
                    });
                });

                // Handle form submit (for button click)
                form.addEventListener('submit', function() {
                    showLoading();
                });

                // Focus first input (or first empty one if there's an error)
                <?php if ($error): ?>
                clearInputs();
                <?php else: ?>
                inputs[0].focus();
                <?php endif; ?>

                // Start cooldown and expiry timer
                startCooldown();
                startExpiryTimer();

                // Resend handler
                resendBtn.addEventListener('click', function() {
                    resendBtn.disabled = true;
                    resendBtn.textContent = '<?php echo esc_js(__('Sending...', 'wpswitchboard')); ?>';

                    var xhr = new XMLHttpRequest();
                    xhr.open('POST', '<?php echo admin_url('admin-ajax.php'); ?>');
                    xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
                    xhr.onload = function() {
                        resendBtn.textContent = '<?php echo esc_js(__('Resend code', 'wpswitchboard')); ?>';
                        try {
                            var response = JSON.parse(xhr.responseText);
                            if (response.success) {
                                successEl.style.display = 'block';
                                setTimeout(function() {
                                    successEl.style.display = 'none';
                                }, 3000);

                                // Clear inputs and reset expiry timer
                                clearInputs();
                                resetExpiryTimer();
                            } else if (response.data && response.data.message) {
                                // Show error message
                                var errorEl = document.querySelector('.switchboard-otp-error');
                                if (!errorEl) {
                                    errorEl = document.createElement('div');
                                    errorEl.className = 'switchboard-otp-error';
                                    successEl.parentNode.insertBefore(errorEl, successEl.nextSibling);
                                }
                                errorEl.textContent = response.data.message;
                                errorEl.style.display = 'block';
                                setTimeout(function() { errorEl.style.display = 'none'; }, 5000);
                            }
                        } catch (e) {}
                        startCooldown();
                    };
                    xhr.onerror = function() {
                        resendBtn.textContent = '<?php echo esc_js(__('Resend code', 'wpswitchboard')); ?>';
                        startCooldown();
                    };
                    xhr.send('action=switchboard_resend_otp&token=<?php echo esc_js($token); ?>&nonce=<?php echo esc_js(wp_create_nonce('switchboard_resend_otp')); ?>');
                });
            })();
            </script>
            <?php wp_footer(); ?>
        </body>
        </html>
        <?php
    }

    /**
     * AJAX: Resend OTP
     */
    public function ajax_resend_otp() {
        if (!wp_verify_nonce($_POST['nonce'] ?? '', 'switchboard_resend_otp')) {
            wp_send_json_error(['message' => __('Security check failed.', 'wpswitchboard')]);
        }

        $token = sanitize_text_field($_POST['token'] ?? '');
        $pending = get_transient('switchboard_otp_pending_' . $token);

        if (!$pending) {
            wp_send_json_error(['message' => __('Session expired.', 'wpswitchboard')]);
        }

        // Rate limiting
        $rate_key = 'switchboard_otp_resend_' . $pending['user_id'];
        $resends = get_transient($rate_key);
        if ($resends && $resends >= 3) {
            wp_send_json_error(['message' => __('Too many resend requests.', 'wpswitchboard')]);
        }
        set_transient($rate_key, ($resends ? $resends + 1 : 1), 5 * MINUTE_IN_SECONDS);

        $user = get_userdata($pending['user_id']);
        if (!$user) {
            wp_send_json_error(['message' => __('User not found.', 'wpswitchboard')]);
        }

        $this->generate_and_send_otp($user);
        wp_send_json_success();
    }

    /**
     * Mask email for display
     */
    private function mask_email($email) {
        $parts = explode('@', $email);
        if (count($parts) !== 2) return '***@***.***';

        $local = $parts[0];
        $domain = $parts[1];

        if (strlen($local) <= 2) {
            $masked_local = str_repeat('*', strlen($local));
        } else {
            $masked_local = substr($local, 0, 2) . str_repeat('*', strlen($local) - 2);
        }

        $domain_parts = explode('.', $domain);
        $masked_domain = substr($domain_parts[0], 0, 1) . str_repeat('*', strlen($domain_parts[0]) - 1);
        if (count($domain_parts) > 1) {
            $masked_domain .= '.' . end($domain_parts);
        }

        return $masked_local . '@' . $masked_domain;
    }

    /**
     * Mask phone for display
     */
    private function mask_phone($phone) {
        if (!$phone) return '***';
        $phone = preg_replace('/[^0-9]/', '', $phone);
        if (strlen($phone) < 4) return '***' . $phone;
        return str_repeat('*', strlen($phone) - 4) . substr($phone, -4);
    }

    /**
     * Get client IP
     */
    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';
    }

    /**
     * Render user profile settings
     */
    public function render_user_settings($user) {
        global $wpdb;

        $is_own_profile = $user->ID === get_current_user_id();
        if (!$is_own_profile && !current_user_can('manage_options')) {
            return;
        }

        $otp_enabled = get_user_meta($user->ID, 'switchboard_otp_enabled', true);
        $otp_method = get_user_meta($user->ID, 'switchboard_otp_method', true);
        $otp_methods_enabled = get_user_meta($user->ID, 'switchboard_otp_methods_enabled', true);
        $otp_phone = get_user_meta($user->ID, 'switchboard_otp_phone', true);

        // For backward compatibility: if methods_enabled not set, derive from old method value
        if (!is_array($otp_methods_enabled)) {
            $otp_methods_enabled = [];
            if ($otp_enabled === 'yes' && $otp_method) {
                $otp_methods_enabled[] = $otp_method;
            }
        }

        // Check if SMS is available
        $settings = get_option('switchboard_otp-login_settings', []);
        $sms_available = !empty($settings['twilio_sid']) && !empty($settings['twilio_token']) && !empty($settings['twilio_from']);

        // Get recent OTP codes (limit to 5 for profile)
        $recent_codes = $wpdb->get_results($wpdb->prepare(
            "SELECT * FROM {$this->table_name} WHERE user_id = %d ORDER BY created_at DESC LIMIT 5",
            $user->ID
        ));
        ?>
        <h2><?php _e('Email/SMS Verification', 'wpswitchboard'); ?> <span class="switchboard-badge">Switchboard</span></h2>
        <table class="form-table" role="presentation">
            <tr>
                <th scope="row"><?php _e('Delivery Method', 'wpswitchboard'); ?></th>
                <td>
                    <label style="margin-right: 20px;">
                        <input type="checkbox" name="switchboard_otp_method_email" value="1" <?php checked(in_array('email', $otp_methods_enabled)); ?>>
                        <?php _e('Email', 'wpswitchboard'); ?>
                    </label>
                    <?php if ($sms_available): ?>
                        <label>
                            <input type="checkbox" name="switchboard_otp_method_sms" value="1" <?php checked(in_array('sms', $otp_methods_enabled)); ?>>
                            <?php _e('SMS', 'wpswitchboard'); ?>
                        </label>
                    <?php else: ?>
                        <label style="color: #888;">
                            <input type="checkbox" name="switchboard_otp_method_sms" value="1" disabled>
                            <?php _e('SMS (not configured)', 'wpswitchboard'); ?>
                        </label>
                    <?php endif; ?>
                    <p class="description"><?php _e('Select at least one to enable verification. Uncheck both to disable OTP.', 'wpswitchboard'); ?></p>
                    <?php if ($this->is_otp_recommended_for_role($user) && empty($otp_methods_enabled)): ?>
                        <p class="description" style="color: #d97706; margin-top: 8px;">
                            <strong><?php _e('Recommended:', 'wpswitchboard'); ?></strong>
                            <?php _e('Your site administrator recommends enabling OTP verification for your role. Please consider selecting at least one delivery method.', 'wpswitchboard'); ?>
                        </p>
                    <?php endif; ?>
                </td>
            </tr>
            <?php if ($sms_available): ?>
            <tr>
                <th scope="row"><label for="switchboard_otp_phone"><?php _e('Phone Number', 'wpswitchboard'); ?></label></th>
                <td>
                    <input type="tel" name="switchboard_otp_phone" id="switchboard_otp_phone" value="<?php echo esc_attr($otp_phone); ?>" class="regular-text" placeholder="+1234567890">
                    <p class="description"><?php _e('Required for SMS delivery. Include country code.', 'wpswitchboard'); ?></p>
                </td>
            </tr>
            <?php endif; ?>
            <?php if (!empty($recent_codes)): ?>
            <tr>
                <th scope="row"><?php _e('Recent Activity', 'wpswitchboard'); ?></th>
                <td>
                    <div style="max-width: 500px;">
                        <?php foreach ($recent_codes as $index => $code):
                            $is_verified = !empty($code->verified_at);
                            $is_expired = strtotime($code->expires_at . ' UTC') < time();
                            $expires_in = strtotime($code->expires_at . ' UTC') - time();

                            if ($is_verified) {
                                $icon = 'Verified:'; $status = __('Verified', 'wpswitchboard'); $color = '#16a34a';
                                $time = sprintf(__('%s ago', 'wpswitchboard'), human_time_diff(strtotime($code->verified_at)));
                            } elseif ($is_expired) {
                                $icon = 'Expired:'; $status = __('Expired', 'wpswitchboard'); $color = '#9ca3af';
                                $time = sprintf(__('%s ago', 'wpswitchboard'), human_time_diff(strtotime($code->expires_at . ' UTC')));
                            } else {
                                $icon = 'Pending:'; $status = __('Pending', 'wpswitchboard'); $color = '#d97706';
                                $time = sprintf(__('expires in %d min', 'wpswitchboard'), ceil($expires_in / 60));
                            }
                            $method_icon = $code->delivery_method === 'sms' ? 'SMS' : 'Email';
                        ?>
                            <div style="display: flex; align-items: center; gap: 8px; padding: 6px 0; <?php echo $index < count($recent_codes) - 1 ? 'border-bottom: 1px solid #f0f0f1;' : ''; ?>">
                                <span style="color: <?php echo $color; ?>;"><?php echo $icon; ?></span>
                                <span style="color: <?php echo $color; ?>; font-weight: 500;"><?php echo $status; ?></span>
                                <span style="color: #646970;">— <?php echo $time; ?></span>
                                <span style="color: #a7aaad; font-size: 12px;"><?php echo $method_icon; ?></span>
                                <span style="color: #a7aaad; font-size: 12px; margin-left: auto;"><?php echo esc_html($code->ip_address ?: ''); ?></span>
                            </div>
                        <?php endforeach; ?>
                    </div>
                </td>
            </tr>
            <?php endif; ?>
        </table>
        <?php
    }

    /**
     * Save user profile settings
     */
    public function save_user_settings($user_id) {
        if (!current_user_can('edit_user', $user_id)) {
            return;
        }

        // Handle delivery method checkboxes
        $email_selected = isset($_POST['switchboard_otp_method_email']);
        $sms_selected = isset($_POST['switchboard_otp_method_sms']);

        // Determine method based on checkbox selection
        if ($email_selected && $sms_selected) {
            // Both selected - prefer email as primary
            $method = 'email';
        } elseif ($sms_selected) {
            $method = 'sms';
        } elseif ($email_selected) {
            $method = 'email';
        } else {
            $method = '';
        }

        // Auto-enable/disable OTP based on delivery method selection
        // If at least one method is selected, enable OTP; otherwise disable
        $enabled = ($email_selected || $sms_selected) ? 'yes' : '';
        update_user_meta($user_id, 'switchboard_otp_enabled', $enabled);

        // Store the method
        update_user_meta($user_id, 'switchboard_otp_method', $method);

        // Also store which methods are enabled (for future multi-method support)
        $methods_enabled = [];
        if ($email_selected) $methods_enabled[] = 'email';
        if ($sms_selected) $methods_enabled[] = 'sms';
        update_user_meta($user_id, 'switchboard_otp_methods_enabled', $methods_enabled);

        if (isset($_POST['switchboard_otp_phone'])) {
            $phone = sanitize_text_field($_POST['switchboard_otp_phone']);
            if (!empty($phone)) {
                // Normalize and validate phone number
                $phone = $this->normalize_phone_number($phone);
                // Basic validation: must start with + and have at least 10 digits
                if (preg_match('/^\+[0-9]{10,15}$/', $phone)) {
                    update_user_meta($user_id, 'switchboard_otp_phone', $phone);
                } else {
                    // Store the original input but add admin notice
                    update_user_meta($user_id, 'switchboard_otp_phone', $phone);
                    add_action('admin_notices', function() {
                        echo '<div class="notice notice-warning is-dismissible"><p>' .
                             esc_html__('The phone number format may be invalid. Please use international format with country code (e.g., +1234567890).', 'wpswitchboard') .
                             '</p></div>';
                    });
                }
            } else {
                update_user_meta($user_id, 'switchboard_otp_phone', '');
            }
        }
    }

    /**
     * Cleanup expired codes
     */
    public function cleanup_expired() {
        global $wpdb;
        $wpdb->query($wpdb->prepare(
            "DELETE FROM {$this->table_name} WHERE expires_at < %s",
            gmdate('Y-m-d H:i:s', time() - HOUR_IN_SECONDS)
        ));
    }

    /**
     * Cleanup user data when a user is deleted
     * Removes OTP records and transients for the deleted user
     *
     * @param int $user_id The user ID being deleted
     */
    public function cleanup_user_data($user_id) {
        global $wpdb;

        // Delete all OTP records for this user
        $wpdb->delete(
            $this->table_name,
            ['user_id' => $user_id],
            ['%d']
        );

        // Clear any pending session transients
        $active_token = get_transient('switchboard_otp_active_token_' . $user_id);
        if ($active_token) {
            delete_transient('switchboard_otp_pending_' . $active_token);
            delete_transient('switchboard_otp_active_token_' . $user_id);
        }

        // Clear fallback transient
        delete_transient('switchboard_otp_fallback_' . $user_id);

        // Clear resend rate limit transient
        delete_transient('switchboard_otp_resend_' . $user_id);
    }

    /**
     * Module activation
     */
    public function activate() {
        if (!wp_next_scheduled('switchboard_otp_cleanup')) {
            wp_schedule_event(time(), 'hourly', 'switchboard_otp_cleanup');
        }
    }

    /**
     * Module deactivation
     */
    public function deactivate() {
        wp_clear_scheduled_hook('switchboard_otp_cleanup');
    }

    /**
     * Register data locations
     */
    public function register_data_locations() {
        return [
            'options' => ['switchboard_otp-login_settings'],
            'option_patterns' => [
                '_transient_switchboard_otp_pending_%',
                '_transient_timeout_switchboard_otp_pending_%',
                '_transient_switchboard_otp_resend_%',
                '_transient_timeout_switchboard_otp_resend_%',
            ],
            'tables' => ['switchboard_otp_codes'],
            'post_meta' => [],
            'user_meta' => [
                'switchboard_otp_enabled',
                'switchboard_otp_method',
                'switchboard_otp_methods_enabled',
                'switchboard_otp_phone'
            ],
        ];
    }

    /**
     * Encrypt Twilio credentials before saving settings
     *
     * @param array $settings The settings being saved
     * @param string $module The module being saved
     * @return array Modified settings with encrypted credentials
     */
    public function encrypt_settings_before_save($settings, $module) {
        if ($module !== 'otp-login') {
            return $settings;
        }

        // Encrypt Twilio credentials before saving
        if (isset($settings['twilio_sid']) && !empty($settings['twilio_sid'])) {
            // Only encrypt if not already encrypted
            if (!$this->is_encrypted($settings['twilio_sid'])) {
                $settings['twilio_sid'] = $this->encrypt_credential($settings['twilio_sid']);
            }
        }
        if (isset($settings['twilio_token']) && !empty($settings['twilio_token'])) {
            if (!$this->is_encrypted($settings['twilio_token'])) {
                $settings['twilio_token'] = $this->encrypt_credential($settings['twilio_token']);
            }
        }

        return $settings;
    }

    /**
     * Render module settings
     */
    public function render_settings($settings = []) {
        global $wpdb;

        $required_roles = isset($settings['required_roles']) ? $settings['required_roles'] : [];
        $expiry_minutes = isset($settings['expiry_minutes']) ? intval($settings['expiry_minutes']) : $this->default_expiry;
        $delivery_method = isset($settings['delivery_method']) ? $settings['delivery_method'] : 'email';

        // Decrypt credentials for display (masked)
        $twilio_sid = isset($settings['twilio_sid']) ? $this->decrypt_credential($settings['twilio_sid']) : '';
        $twilio_token = isset($settings['twilio_token']) ? $this->decrypt_credential($settings['twilio_token']) : '';
        $twilio_from = isset($settings['twilio_from']) ? $settings['twilio_from'] : '';

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

        // Stats
        $total_verifications = $wpdb->get_var("SELECT COUNT(*) FROM {$this->table_name} WHERE verified_at IS NOT NULL");
        $failed_attempts = $wpdb->get_var("SELECT COUNT(*) FROM {$this->table_name} WHERE verified_at IS NULL AND attempts > 0");
        $users_with_otp = $wpdb->get_var("SELECT COUNT(DISTINCT user_id) FROM {$wpdb->usermeta} WHERE meta_key = 'switchboard_otp_enabled' AND meta_value = 'yes'");
        ?>
        <input type="hidden" name="module" value="otp-login">

        <!-- How to Use Section -->
        <div class="sb-sp-info-box sb-sp-info-box-tip" style="margin-bottom: 24px; background: #fef3c7; border-color: #fcd34d;">
            <h4 class="sb-sp-heading-sm" style="color: #92400e;"><?php _e('How to Use Email/SMS Verification', 'wpswitchboard'); ?></h4>
            <div style="font-size: 13px; color: #78350f; line-height: 1.6;">
                <p style="margin: 0 0 12px;"><strong><?php _e('Option 1: Recommend for Specific Roles', 'wpswitchboard'); ?></strong><br>
                <?php _e('Select roles below - users with those roles will see a recommendation to enable OTP in their profile. They can still opt-out to prevent lockouts.', 'wpswitchboard'); ?></p>

                <p style="margin: 0 0 12px;"><strong><?php _e('Option 2: User Self-Service', 'wpswitchboard'); ?></strong><br>
                <?php _e('Leave roles unchecked. Users can enable/disable verification from their profile: Users → Profile → Email/SMS Verification', 'wpswitchboard'); ?></p>

                <p style="margin: 0 0 12px;"><strong><?php _e('How It Works:', 'wpswitchboard'); ?></strong><br>
                <?php _e('After entering username/password, users receive a 6-digit code via email (or SMS if configured). They enter this code to complete login.', 'wpswitchboard'); ?></p>

                <p style="margin: 0;"><strong><?php _e('Testing:', 'wpswitchboard'); ?></strong> <?php _e('Enable verification for your account in your profile, log out, and try logging in!', 'wpswitchboard'); ?>
                <a href="<?php echo admin_url('profile.php'); ?>" style="color: #92400e; text-decoration: underline;"><?php _e('Go to Profile →', 'wpswitchboard'); ?></a></p>
            </div>
        </div>

        <!-- Stats Card -->
        <div class="sb-module-card sb-module-form-section" style="margin-bottom: 24px;">
            <div class="sb-module-card-header">
                <div class="sb-module-card-header-left">
                    <h2 class="sb-module-card-title"><?php _e('Verification Overview', 'wpswitchboard'); ?></h2>
                    <p class="sb-module-card-desc"><?php _e('Email/SMS verification statistics', 'wpswitchboard'); ?></p>
                </div>
            </div>
            <div class="sb-module-card-body">
                <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 16px;">
                    <div style="background: #f0fdf4; border: 1px solid #bbf7d0; border-radius: 8px; padding: 20px; text-align: center;">
                        <div style="font-size: 32px; font-weight: 700; color: #16a34a;"><?php echo intval($total_verifications); ?></div>
                        <div style="font-size: 13px; color: #166534; margin-top: 4px;"><?php _e('Successful Logins', 'wpswitchboard'); ?></div>
                    </div>
                    <div style="background: #fef2f2; border: 1px solid #fecaca; border-radius: 8px; padding: 20px; text-align: center;">
                        <div style="font-size: 32px; font-weight: 700; color: #dc2626;"><?php echo intval($failed_attempts); ?></div>
                        <div style="font-size: 13px; color: #991b1b; margin-top: 4px;"><?php _e('Failed Attempts', 'wpswitchboard'); ?></div>
                    </div>
                    <div style="background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 8px; padding: 20px; text-align: center;">
                        <div style="font-size: 32px; font-weight: 700; color: #2563eb;"><?php echo intval($users_with_otp); ?></div>
                        <div style="font-size: 13px; color: #1e40af; margin-top: 4px;"><?php _e('Users Enabled', 'wpswitchboard'); ?></div>
                    </div>
                </div>
            </div>
        </div>

        <!-- General Settings -->
        <div class="sb-module-card sb-module-form-section" style="margin-bottom: 24px;">
            <div class="sb-module-card-header">
                <div class="sb-module-card-header-left">
                    <h2 class="sb-module-card-title"><?php _e('General Settings', 'wpswitchboard'); ?></h2>
                    <p class="sb-module-card-desc"><?php _e('Configure OTP verification behavior', 'wpswitchboard'); ?></p>
                </div>
            </div>
            <div class="sb-module-card-body">
                <!-- Code Expiry -->
                <div class="sb-sp-group sb-sp-field-horizontal" style="margin-bottom: 24px;">
                    <div class="sb-sp-field-horizontal-left">
                        <label for="setting_expiry_minutes" class="sb-sp-label"><?php _e('Code Expiry (Minutes)', 'wpswitchboard'); ?></label>
                        <p class="sb-sp-description"><?php _e('How long verification codes remain valid. Range: 5-30 minutes.', 'wpswitchboard'); ?></p>
                    </div>
                    <input type="number" name="setting_expiry_minutes" id="setting_expiry_minutes" value="<?php echo esc_attr($expiry_minutes); ?>" min="5" max="30" class="sb-sp-input" style="width: 100px;">
                </div>

                <!-- Default Delivery Method -->
                <div class="sb-sp-group sb-sp-field-horizontal" style="margin-bottom: 24px; padding-top: 24px; border-top: 1px solid #e5e7eb;">
                    <div class="sb-sp-field-horizontal-left">
                        <label class="sb-sp-label"><?php _e('Default Delivery Method', 'wpswitchboard'); ?></label>
                        <p class="sb-sp-description"><?php _e('How codes are sent by default. Users can override in their profile.', 'wpswitchboard'); ?></p>
                    </div>
                    <div style="display: flex; gap: 16px;">
                        <label style="display: flex; align-items: center; gap: 8px; padding: 12px 16px; background: <?php echo $delivery_method === 'email' ? 'var(--primary-50)' : '#f8fafc'; ?>; border: 1px solid <?php echo $delivery_method === 'email' ? 'var(--primary-200)' : '#e2e8f0'; ?>; border-radius: 8px; cursor: pointer;">
                            <input type="radio" name="setting_delivery_method" value="email" <?php checked($delivery_method, 'email'); ?> style="accent-color: var(--primary-500);">
                            <span><?php _e('Email', 'wpswitchboard'); ?></span>
                        </label>
                        <label style="display: flex; align-items: center; gap: 8px; padding: 12px 16px; background: <?php echo $delivery_method === 'sms' ? 'var(--primary-50)' : '#f8fafc'; ?>; border: 1px solid <?php echo $delivery_method === 'sms' ? 'var(--primary-200)' : '#e2e8f0'; ?>; border-radius: 8px; cursor: pointer;">
                            <input type="radio" name="setting_delivery_method" value="sms" <?php checked($delivery_method, 'sms'); ?> style="accent-color: var(--primary-500);">
                            <span><?php _e('SMS', 'wpswitchboard'); ?></span>
                        </label>
                    </div>
                </div>

                <!-- Recommended Roles -->
                <div class="sb-sp-group" style="padding-top: 24px; border-top: 1px solid #e5e7eb;">
                    <label class="sb-sp-label" style="margin-bottom: 12px; display: block;"><?php _e('Recommend Verification for Roles', 'wpswitchboard'); ?></label>
                    <p class="sb-sp-description" style="margin-bottom: 16px;"><?php _e('Users with these roles will see a recommendation to enable OTP in their profile. They can still choose to disable it to prevent lockouts.', 'wpswitchboard'); ?></p>

                    <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 12px;">
                        <?php foreach ($roles as $role_key => $role): ?>
                            <label style="display: flex; align-items: center; gap: 10px; padding: 12px 16px; background: <?php echo in_array($role_key, $required_roles) ? 'var(--primary-50)' : '#f8fafc'; ?>; border: 1px solid <?php echo in_array($role_key, $required_roles) ? 'var(--primary-200)' : '#e2e8f0'; ?>; border-radius: 8px; cursor: pointer; transition: all 0.15s ease;">
                                <input type="checkbox" name="setting_required_roles[]" value="<?php echo esc_attr($role_key); ?>" <?php checked(in_array($role_key, $required_roles)); ?> style="width: 18px; height: 18px; accent-color: var(--primary-500);">
                                <span style="font-weight: 500; color: #374151;"><?php echo esc_html($role['name']); ?></span>
                            </label>
                        <?php endforeach; ?>
                    </div>
                </div>
            </div>
        </div>

        <!-- SMS Settings (Twilio) - Only shown when SMS is selected -->
        <div class="sb-module-card sb-module-form-section" id="twilio-settings-card" style="margin-bottom: 24px; <?php echo $delivery_method !== 'sms' ? 'display: none;' : ''; ?>">
            <div class="sb-module-card-header">
                <div class="sb-module-card-header-left">
                    <h2 class="sb-module-card-title"><?php _e('SMS Settings (Twilio)', 'wpswitchboard'); ?></h2>
                    <p class="sb-module-card-desc"><?php _e('Required for SMS delivery - get credentials from twilio.com/console', 'wpswitchboard'); ?></p>
                </div>
                <?php if ($twilio_sid && $twilio_token && $twilio_from): ?>
                    <span style="background: #dcfce7; color: #166534; padding: 4px 12px; border-radius: 4px; font-size: 12px; font-weight: 500;"><?php _e('Configured', 'wpswitchboard'); ?></span>
                <?php else: ?>
                    <span style="background: #fef3c7; color: #92400e; padding: 4px 12px; border-radius: 4px; font-size: 12px; font-weight: 500;"><?php _e('Setup Required', 'wpswitchboard'); ?></span>
                <?php endif; ?>
            </div>
            <div class="sb-module-card-body">
                <div class="sb-sp-info-box" style="background: #eff6ff; border-color: #bfdbfe; margin-bottom: 20px;">
                    <p class="sb-sp-description" style="margin: 0; color: #1e40af; font-size: 13px;">
                        <?php _e('To use SMS delivery, you need a Twilio account.', 'wpswitchboard'); ?>
                        <a href="https://www.twilio.com/try-twilio" target="_blank" style="color: #1e40af; text-decoration: underline;"><?php _e('Sign up for free →', 'wpswitchboard'); ?></a>
                    </p>
                </div>

                <div class="sb-sp-group sb-sp-field-horizontal" style="margin-bottom: 16px;">
                    <div class="sb-sp-field-horizontal-left">
                        <label for="setting_twilio_sid" class="sb-sp-label"><?php _e('Account SID', 'wpswitchboard'); ?></label>
                        <p class="sb-sp-description"><?php _e('Found on your Twilio Console dashboard', 'wpswitchboard'); ?></p>
                    </div>
                    <input type="text" name="setting_twilio_sid" id="setting_twilio_sid" value="<?php echo esc_attr($twilio_sid); ?>" class="sb-sp-input" placeholder="ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" style="width: 320px;">
                </div>

                <div class="sb-sp-group sb-sp-field-horizontal" style="margin-bottom: 16px;">
                    <div class="sb-sp-field-horizontal-left">
                        <label for="setting_twilio_token" class="sb-sp-label"><?php _e('Auth Token', 'wpswitchboard'); ?></label>
                        <p class="sb-sp-description"><?php _e('Keep this secret - never share publicly', 'wpswitchboard'); ?></p>
                    </div>
                    <input type="password" name="setting_twilio_token" id="setting_twilio_token" value="<?php echo esc_attr($twilio_token); ?>" class="sb-sp-input" placeholder="••••••••••••••••" style="width: 320px;">
                </div>

                <div class="sb-sp-group sb-sp-field-horizontal">
                    <div class="sb-sp-field-horizontal-left">
                        <label for="setting_twilio_from" class="sb-sp-label"><?php _e('From Number', 'wpswitchboard'); ?></label>
                        <p class="sb-sp-description"><?php _e('Your purchased Twilio phone number (with country code)', 'wpswitchboard'); ?></p>
                    </div>
                    <input type="tel" name="setting_twilio_from" id="setting_twilio_from" value="<?php echo esc_attr($twilio_from); ?>" class="sb-sp-input" placeholder="+15551234567" style="width: 200px;">
                </div>

                <!-- Email Fallback Info -->
                <div class="sb-sp-info-box" style="background: #f0fdf4; border-color: #bbf7d0; margin-top: 24px;">
                    <p class="sb-sp-description" style="margin: 0; color: #166534; font-size: 13px;">
                        <strong><?php _e('Automatic Email Fallback:', 'wpswitchboard'); ?></strong>
                        <?php _e('If SMS delivery fails for any reason (Twilio not configured, API error, or user has no phone number), the verification code will automatically be sent via email instead. This ensures users can always complete their login.', 'wpswitchboard'); ?>
                    </p>
                </div>

                <!-- User Phone Number Required -->
                <div class="sb-sp-alert sb-sp-alert-warning" style="margin-top: 16px; margin-bottom: 16px;">
                    <div class="sb-sp-alert-title"><?php _e('User Phone Number Required', 'wpswitchboard'); ?></div>
                    <div class="sb-sp-alert-text">
                        <?php _e('Users must have a phone number saved in their profile (Users → Profile → Email/SMS Verification section) to receive SMS codes. Without a phone number, the system will automatically fall back to email delivery.', 'wpswitchboard'); ?>
                    </div>
                </div>

                <!-- Phone Format Info -->
                <div class="sb-sp-info-box" style="background: #eff6ff; border-color: #bfdbfe;">
                    <p class="sb-sp-description" style="margin: 0 0 8px; color: #1e40af; font-size: 13px;">
                        <strong><?php _e('Phone Number Format (E.164):', 'wpswitchboard'); ?></strong>
                    </p>
                    <p class="sb-sp-description" style="margin: 0 0 8px; color: #3b82f6; font-size: 13px;">
                        <?php _e('Phone numbers must include the country code. Examples:', 'wpswitchboard'); ?>
                    </p>
                    <ul style="margin: 0; padding-left: 20px; color: #3b82f6; font-size: 12px;">
                        <li><code style="background: #dbeafe; padding: 2px 6px; border-radius: 3px;">+1 555 123 4567</code> <?php _e('(USA/Canada)', 'wpswitchboard'); ?></li>
                        <li><code style="background: #dbeafe; padding: 2px 6px; border-radius: 3px;">+44 7911 123456</code> <?php _e('(UK)', 'wpswitchboard'); ?></li>
                        <li><code style="background: #dbeafe; padding: 2px 6px; border-radius: 3px;">+91 98765 43210</code> <?php _e('(India)', 'wpswitchboard'); ?></li>
                    </ul>
                </div>
            </div>
        </div>

        <script>
        (function() {
            var deliveryRadios = document.querySelectorAll('input[name="setting_delivery_method"]');
            var twilioCard = document.getElementById('twilio-settings-card');

            deliveryRadios.forEach(function(radio) {
                radio.addEventListener('change', function() {
                    if (this.value === 'sms') {
                        twilioCard.style.display = 'block';
                    } else {
                        twilioCard.style.display = 'none';
                    }
                });
            });
        })();
        </script>

        <!-- Info Box -->
        <div class="sb-sp-info-box sb-sp-info-box-info" style="margin-top: 24px;">
            <h4 class="sb-sp-heading-sm"><?php _e('About Email/SMS Verification', 'wpswitchboard'); ?></h4>
            <p class="sb-sp-description" style="margin: 0;">
                <?php _e('Email/SMS verification adds an extra security step after password entry. Users receive a 6-digit code via email or SMS that expires after the configured time. Unlike authenticator app-based 2FA, this doesn\'t require users to install any apps - codes are delivered directly to their email or phone. This provides a familiar experience similar to banking and other services.', 'wpswitchboard'); ?>
            </p>
        </div>
        <?php
    }
}

$switchboard_instance = new SwitchboardOtpLogin();
Switchboard_Data_Registry::get_instance()->auto_register($switchboard_instance, __FILE__);
