<?php
/**
 * Handles the App-Based Two-Factor Authentication (TOTP) feature for Guard Dog.
 *
 * @package GuardDog
 * @since 1.2.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

use GuardDog\Vendor\PragmaRX\Google2FA\Google2FA;
use GuardDog\Vendor\BaconQrCode\Renderer\ImageRenderer;
use GuardDog\Vendor\BaconQrCode\Renderer\Image\SvgImageBackEnd;
use GuardDog\Vendor\BaconQrCode\Renderer\RendererStyle\RendererStyle;
use GuardDog\Vendor\BaconQrCode\Writer;

class Guard_Dog_Two_Factor_Auth {

    /**
     * Canonical shortcode tag for the frontend account-security widget.
     *
     * @var string
     */
    const SHORTCODE = 'guard_dog_2fa';

    /**
     * Standalone shortcode tag for frontend 2FA management.
     *
     * @var string
     */
    const TWO_FACTOR_SHORTCODE = 'guard_dog_two_factor';

    /**
     * Human-friendly shortcode alias for the frontend account-security widget.
     *
     * @var string
     */
	    const SHORTCODE_ALIAS = 'guard_dog_account_security';

	    /**
	     * Standalone shortcode tag for the frontend sessions widget.
	     *
	     * @var string
	     */
	    const SESSIONS_SHORTCODE = 'guard_dog_sessions';

    /**
     * Block name for the frontend account-security widget.
     *
     * @var string
     */
    const BLOCK_NAME = 'guard-dog/account-security';

    /**
     * Editor script handle for the account-security block.
     *
     * @var string
     */
    const BLOCK_EDITOR_SCRIPT = 'guard-dog-account-security-block';

    /**
     * Block name for standalone frontend 2FA management.
     *
     * @var string
     */
    const TWO_FACTOR_BLOCK_NAME = 'guard-dog/two-factor-auth';

    /**
     * Editor script handle for the standalone 2FA block.
     *
     * @var string
     */
    const TWO_FACTOR_BLOCK_EDITOR_SCRIPT = 'guard-dog-two-factor-auth-block';

    /**
     * Block name for standalone frontend session management.
     *
     * @var string
     */
    const SESSIONS_BLOCK_NAME = 'guard-dog/sessions';

    /**
     * Editor script handle for the standalone sessions block.
     *
     * @var string
     */
    const SESSIONS_BLOCK_EDITOR_SCRIPT = 'guard-dog-sessions-block';

    /**
     * User meta key for the 2FA secret.
     * @var string
     */
    const USER_META_SECRET_KEY = '_guard_dog_2fa_secret';

    /**
     * User meta key to indicate if 2FA is enabled for the user.
     * @var string
     */
    const USER_META_ENABLED_KEY = '_guard_dog_2fa_enabled';

     /**
     * User meta key for recovery codes.
     * @var string
     */
    const USER_META_RECOVERY_CODES = '_guard_dog_2fa_recovery_codes';

    // Error codes for 2FA
    const ERROR_CODE_REQUIRED = 'guard_dog_2fa_required';
    const ERROR_CODE_INVALID = 'guard_dog_2fa_invalid_code';
    const ERROR_CODE_INVALID_SESSION = 'guard_dog_2fa_invalid_session';
    const ERROR_CODE_RATE_LIMITED = 'guard_dog_2fa_rate_limited';

    /**
     * Instance of the Google2FA library.
     * @var Google2FA|null
     */
    private $google2fa = null;
    private $activity_log = null;

    /**
     * Constructor. Adds hooks.
     *
     * @since 1.2.0
     */
    public function __construct($activity_log_instance) {
        $this->activity_log = $activity_log_instance;
        // Only proceed if the library class exists (via Composer).
        if ( ! class_exists( '\GuardDog\Vendor\PragmaRX\Google2FA\Google2FA' ) ) {
            // Optionally log an error or show an admin notice.
            return;
        }
        $this->google2fa = new Google2FA();
        $this->add_hooks();
    }

    /**
     * Adds the necessary WordPress hooks.
     *
     * @since 1.2.0
     */
    public function add_hooks() {
        // User Profile actions.
        add_action( 'show_user_profile', array( $this, 'render_user_profile_section' ) ); // Show section on profile page (for others).
        add_action( 'edit_user_profile', array( $this, 'render_user_profile_section' ) ); // Show section on profile page (for self).
        add_action( 'personal_options_update', array( $this, 'save_user_profile_settings' ) ); // Save settings from own profile.
        add_action( 'edit_user_profile_update', array( $this, 'save_user_profile_settings' ) ); // Save settings when admin edits others.

        // Login flow hooks.
        add_filter( 'authenticate', array( $this, 'intercept_login_check' ), 25, 3 ); // Priority 25 to run after CAPTCHA but before final auth.
        add_action( 'login_init', array( $this, 'maybe_render_2fa_setup_interstitial' ), 3 ); // Setup interstitial for enforcement (before 2FA form).
        add_action( 'login_init', array( $this, 'handle_2fa_setup_completion' ), 4 ); // Handle setup completion during login flow.
        add_action( 'login_init', array( $this, 'maybe_render_2fa_form_and_exit' ) ); // Renders the separate 2FA form.
        add_action( 'login_init', array( $this, 'handle_2fa_form_submission' ), 5 ); // Handle 2FA form submission early.
        add_filter( 'wp_login_errors', array( $this, 'add_session_errors' ), 10, 2 ); // Handle session-based errors.

        // Admin area warnings.
        add_action( 'admin_notices', array( $this, 'show_grace_period_warnings' ) );

        // AJAX handlers (both logged-in and frontend usage).
        add_action( 'wp_ajax_guard_dog_generate_2fa_secret', array( $this, 'ajax_generate_secret' ) );
        add_action( 'wp_ajax_guard_dog_verify_2fa_setup', array( $this, 'ajax_verify_setup' ) );

        // AJAX handlers for setup interstitial (unauthenticated users during enforcement).
        // These use transient-based validation instead of get_current_user_id().
        add_action( 'wp_ajax_nopriv_guard_dog_setup_generate_secret', array( $this, 'ajax_setup_generate_secret' ) );
        add_action( 'wp_ajax_nopriv_guard_dog_setup_send_email_code', array( $this, 'ajax_setup_send_email_code' ) );
        add_action( 'wp_ajax_nopriv_guard_dog_setup_register_passkey_options', array( $this, 'ajax_setup_passkey_options' ) );
        add_action( 'wp_ajax_nopriv_guard_dog_setup_register_passkey_verify', array( $this, 'ajax_setup_passkey_verify' ) );

        // Script enqueueing.
        add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_profile_scripts' ) );
        add_action( 'login_enqueue_scripts', array( $this, 'enqueue_login_scripts' ) );
        
	        // Shortcode registration.
	        add_shortcode( self::SHORTCODE, array( $this, 'shortcode_handler' ) );
	        add_shortcode( self::SHORTCODE_ALIAS, array( $this, 'shortcode_handler' ) );
	        add_shortcode( self::TWO_FACTOR_SHORTCODE, array( $this, 'two_factor_shortcode_handler' ) );
	        add_shortcode( self::SESSIONS_SHORTCODE, array( $this, 'sessions_shortcode_handler' ) );
	        add_action( 'init', array( $this, 'register_block' ) );
        
        // Additional AJAX handlers for shortcode functionality (logged-in users only).
        add_action( 'wp_ajax_guard_dog_view_recovery_codes', array( $this, 'ajax_view_recovery_codes' ) );
        add_action( 'wp_ajax_guard_dog_regenerate_recovery_codes', array( $this, 'ajax_regenerate_recovery_codes' ) );
        add_action( 'wp_ajax_guard_dog_disable_2fa', array( $this, 'ajax_disable_2fa' ) );
        add_action( 'wp_ajax_guard_dog_refresh_2fa_nonces', array( $this, 'ajax_refresh_nonces' ) );
    }

    //--------------------------------------------------------------------------
    // User Profile Section
    //--------------------------------------------------------------------------

    /**
     * Renders the 2FA settings section on the user profile page.
     *
     * @since 1.2.0
     * @param WP_User $user The user object being edited.
     */
    public function render_user_profile_section($user) {
        $this->render_user_profile_2fa_content($user);
    }

    /**
     * Saves 2FA settings when the user profile is updated.
     *
     * @since 1.2.0
     * @param int $user_id The ID of the user being updated.
     */
    public function save_user_profile_settings($user_id) {
        // Check permissions - only allow users to edit their own profile
        // or admins to edit others (standard profile.php check)
        if (!current_user_can('edit_user', $user_id)) {
            return;
        }

        // Handle 2FA disable form submission
        if (isset($_POST['guard_dog_disable_2fa']) && isset($_POST['guard_dog_disable_2fa_nonce'])) {
            if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['guard_dog_disable_2fa_nonce'])), 'guard_dog_disable_2fa_' . $user_id)) {
                // Nonce check failed - maybe add an admin notice?
                return;
            }

            // Delete the meta keys to disable
            delete_user_meta($user_id, self::USER_META_ENABLED_KEY);
            delete_user_meta($user_id, self::USER_META_SECRET_KEY);
            delete_user_meta($user_id, self::USER_META_RECOVERY_CODES); // Also remove recovery codes

            // Optional: Add an admin notice for success
            add_action('admin_notices', function() {
                add_settings_error(
                    'guard_dog_2fa',
                    '2fa_disabled',
                    __('Two-Factor Authentication has been disabled.', 'guard-dog'),
                    'success'
                );
            });
        }

        // Note: Recovery codes regeneration is now handled via AJAX in ajax_regenerate_recovery_codes()

        // Handle enabling/verifying 2FA (to be implemented later)

    }

    // --- Other methods for secret generation, code verification, QR code generation, etc. will go here --- 

    /**
     * AJAX handler to generate a new 2FA secret key.
     *
     * @since 1.2.0
     */
    public function ajax_generate_secret() {
        // Debug logging for request start
        if (function_exists('Guard_Dog') && Guard_Dog()->debug) {
            Guard_Dog()->debug->info('2FA Generate request started', 'two_factor', [
                'timestamp' => current_time('mysql'),
                'memory_usage' => memory_get_usage(true),
                'memory_limit' => ini_get('memory_limit'),
                'max_execution_time' => ini_get('max_execution_time'),
                'user_logged_in' => is_user_logged_in(),
                'current_user_id' => get_current_user_id()
            ]);
        }
        
        // Check if 2FA is globally enabled.
        if ( ! get_option( 'guard_dog_two_factor_enabled', false ) ) {
            wp_send_json_error( array( 'message' => __( 'Two-factor authentication is not enabled globally.', 'guard-dog' ) ), 403 );
        }
        
        // Check if user is logged in first.
        if ( ! is_user_logged_in() ) {
            wp_send_json_error( array( 'message' => __( 'You must be logged in to use this feature.', 'guard-dog' ) ), 403 );
        }

        // Get user ID from AJAX request (or could use get_current_user_id).
        $user_id = isset( $_POST['user_id'] ) ? absint( $_POST['user_id'] ) : 0;

        // Security checks.
        if ( $user_id !== get_current_user_id() ) {
            wp_send_json_error( array( 'message' => __( 'Permission denied.', 'guard-dog' ) ), 403 );
        }
        
        // Use WordPress standard AJAX nonce verification
        $expected_action = 'guard_dog_generate_2fa_secret_' . $user_id;
        
        // Check both _wpnonce and nonce for compatibility
        $nonce = isset($_POST['_wpnonce']) ? sanitize_text_field(wp_unslash($_POST['_wpnonce'])) : (isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : '');
        
        // Debug logging for nonce verification.
        if ( function_exists( 'Guard_Dog' ) && Guard_Dog()->debug ) {
            Guard_Dog()->debug->info( '2FA Generate nonce verification', 'two_factor', array(
                'user_id' => $user_id,
                'expected_action' => $expected_action,
                'nonce_length' => strlen( $nonce ),
                'has_wpnonce' => isset( $_POST['_wpnonce'] ),
                'has_nonce' => isset( $_POST['nonce'] ),
                'relevant_post_keys' => $this->get_relevant_post_keys()
            ) );
        }
        
        // Try WordPress's built-in AJAX nonce check first.
        $verify_result = false;
        if ( isset( $_POST['_wpnonce'] ) ) {
            $verify_result = wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ), $expected_action );
        } elseif ( isset( $_POST['nonce'] ) ) {
            $verify_result = wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), $expected_action );
        }
        
        if ( ! $verify_result ) {
            if ( function_exists( 'Guard_Dog' ) && Guard_Dog()->debug ) {
                Guard_Dog()->debug->error( '2FA Generate nonce verification failed', 'two_factor', array(
                    'user_id' => $user_id,
                    'expected_action' => $expected_action,
                    'nonce_length' => strlen( $nonce ),
                    'verify_result' => $verify_result
                ) );
            }
            wp_send_json_error( array( 'message' => __( 'Security verification failed.', 'guard-dog' ) ), 403 );
        }

        // Prevent generating a new secret if already enabled or setup partially completed.
        if ( get_user_meta( $user_id, self::USER_META_ENABLED_KEY, true ) ) {
             wp_send_json_error( array( 'message' => __( '2FA is already enabled.', 'guard-dog' ) ), 400 );
        }
        // Consider if we need to clear a previous temporary secret if they restart setup
        // delete_user_meta($user_id, '_guard_dog_2fa_temp_secret');

        try {
            // Debug logging for library check.
            if ( function_exists( 'Guard_Dog' ) && Guard_Dog()->debug ) {
                Guard_Dog()->debug->info( '2FA Library check', 'two_factor', array(
                    'google2fa_class_exists' => class_exists( '\GuardDog\Vendor\PragmaRX\Google2FA\Google2FA' ),
                    'google2fa_instance_exists' => $this->google2fa !== null,
                    'memory_usage' => memory_get_usage( true )
                ) );
            }
            
            // Generate new secret
            $secret = $this->google2fa->generateSecretKey();

            // Store temporarily (e.g., until verified)
            // Using a separate key to avoid confusion with the final key
            update_user_meta($user_id, '_guard_dog_2fa_temp_secret', $secret);

            // Generate QR Code data URI
            $user_info = get_userdata($user_id);
            $site_name = get_bloginfo('name');

            // Create the OTPAuth URI
            $otpauth_uri = $this->google2fa->getQRCodeUrl(
                sanitize_title($site_name), // Issuer
                $user_info->user_email,      // Account Name
                $secret
            );

            // Use BaconQrCode to generate SVG data
            $qr_code_data_uri = '';

            // Debug logging for QR code generation start.
            if ( function_exists( 'Guard_Dog' ) && Guard_Dog()->debug ) {
                Guard_Dog()->debug->info( '2FA QR Code generation started', 'two_factor', array(
                    'memory_usage' => memory_get_usage( true ),
                    'otpauth_uri_length' => strlen( $otpauth_uri ),
                    'bacon_qr_exists' => class_exists('GuardDog\Vendor\BaconQrCode\Writer')
                ));
            }

            // Remove the class_exists check and rely on try/catch
            // if (class_exists('GuardDog\Vendor\BaconQrCode\Writer')) {
                try {
                    $renderer = new ImageRenderer(
                        new RendererStyle(200), // Size in pixels
                        new SvgImageBackEnd()   // Output as SVG
                    );
                    $writer = new Writer($renderer);
                    // Get SVG image data as a string
                    $svg_data = $writer->writeString($otpauth_uri);
                    // Create a base64 data URI
                    $qr_code_data_uri = 'data:image/svg+xml;base64,' . base64_encode($svg_data);
                    
                    // Debug logging for successful QR generation.
                    if ( function_exists( 'Guard_Dog' ) && Guard_Dog()->debug ) {
                        Guard_Dog()->debug->info( '2FA QR Code generation completed', 'two_factor', array(
                            'memory_usage' => memory_get_usage( true ),
                            'svg_data_length' => strlen( $svg_data ),
                            'qr_code_data_uri_length' => strlen( $qr_code_data_uri )
                        ) );
                    }
                } catch (\Throwable $qr_exception) { // Catch any throwable error/exception
                    // Debug logging for QR generation error.
                    if ( function_exists( 'Guard_Dog' ) && Guard_Dog()->debug ) {
                        Guard_Dog()->debug->error( '2FA QR Generation Error', 'two_factor', array(
                            'error_message' => $qr_exception->getMessage(),
                            'error_code' => $qr_exception->getCode(),
                            'memory_usage' => memory_get_usage( true )
                        ) );
                    }
                    // Keep $qr_code_data_uri empty, the frontend won't display the image.
                }
            // } else {
            //     // Fallback or error - BaconQrCode not found
            //     error_log('Guard Dog Error: BaconQrCode\Writer class not found (class_exists check failed).');
            // }

            // Debug logging for successful generation.
            if ( function_exists( 'Guard_Dog' ) && Guard_Dog()->debug ) {
                Guard_Dog()->debug->info( '2FA Generate request completed successfully', 'two_factor', array(
                    'timestamp' => current_time( 'mysql' ),
                    'memory_usage' => memory_get_usage( true ),
                    'secret_length' => strlen( $secret ),
                    'qr_code_generated' => ! empty( $qr_code_data_uri )
                ) );
            }
            
            // Send back the secret and QR code data URI.
            wp_send_json_success( array(
                'secret' => $secret,
                'qr_data_uri' => $qr_code_data_uri,
            ) );

        } catch (\Exception $e) {
            // Debug logging for errors.
            if ( function_exists( 'Guard_Dog' ) && Guard_Dog()->debug ) {
                Guard_Dog()->debug->error( '2FA Generate request failed with exception', 'two_factor', array(
                    'timestamp' => current_time( 'mysql' ),
                    'memory_usage' => memory_get_usage( true ),
                    'error_message' => $e->getMessage(),
                    'error_code' => $e->getCode(),
                    'error_file' => $e->getFile(),
                    'error_line' => $e->getLine()
                ) );
            }
            
            // Log the error if needed.
            // error_log('Guard Dog 2FA Secret Generation Error: ' . $e->getMessage());
            wp_send_json_error( array( 'message' => __( 'Could not generate secret key. Please try again.', 'guard-dog' ) ), 500 );
        }
    }

    /**
     * AJAX handler to verify the 2FA code during setup and enable 2FA.
     *
     * @since 1.2.0
     */
    public function ajax_verify_setup() {
        // Check if 2FA is globally enabled
        if (!get_option('guard_dog_two_factor_enabled', false)) {
            wp_send_json_error(array('message' => __('Two-factor authentication is not enabled globally.', 'guard-dog')), 403);
        }
        
        // Check if user is logged in first
        if (!is_user_logged_in()) {
            wp_send_json_error(array('message' => __('You must be logged in to use this feature.', 'guard-dog')), 403);
        }

        // Get current user ID first (no POST data access needed)
        $current_user_id = get_current_user_id();
        $expected_action = 'guard_dog_verify_2fa_setup_' . $current_user_id;
        
        // Fail early if nonce is missing or invalid - use WordPress recommended pattern
        if ((!isset($_POST['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_wpnonce'])), $expected_action)) &&
            (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), $expected_action))) {
            wp_send_json_error(array('message' => __('Security verification failed.', 'guard-dog')), 403);
        }

        // Now safe to access POST data after nonce validation
        $user_id = isset($_POST['user_id']) ? absint($_POST['user_id']) : 0;
        if ($user_id !== $current_user_id) {
            wp_send_json_error(array('message' => __('Permission denied.', 'guard-dog')), 403);
        }

        // Get submitted code and temporary secret
        $code = isset($_POST['code']) ? sanitize_text_field(wp_unslash($_POST['code'])) : '';
        $temp_secret = get_user_meta($user_id, '_guard_dog_2fa_temp_secret', true);

        if (empty($temp_secret)) {
            wp_send_json_error(array('message' => __('No temporary secret found. Please start setup again.', 'guard-dog')), 400);
        }
        if (empty($code) || !ctype_digit($code) || strlen($code) !== 6) {
             wp_send_json_error(array('message' => __('Please enter a valid 6-digit code.', 'guard-dog')), 400);
        }

        try {
            // Verify the code against the temporary secret
            $is_valid = $this->google2fa->verifyKey($temp_secret, $code);

            if ($is_valid) {
                // --- Verification successful --- 
                // Move secret to permanent meta
                update_user_meta($user_id, self::USER_META_SECRET_KEY, $temp_secret);
                // Mark as enabled
                update_user_meta($user_id, self::USER_META_ENABLED_KEY, true);
                // Delete temporary secret
                delete_user_meta($user_id, '_guard_dog_2fa_temp_secret');

                // Generate and store recovery codes
                $recovery_codes = $this->generate_recovery_codes();
                $encrypted_codes = $this->encode_recovery_codes($recovery_codes);
                update_user_meta($user_id, self::USER_META_RECOVERY_CODES, $encrypted_codes);

                // Store codes in a transient to display ONCE after successful setup
                set_transient('_guard_dog_show_recovery_codes_' . $user_id, $recovery_codes, MINUTE_IN_SECONDS * 10); // Store for 10 mins

                wp_send_json_success(array('message' => __('Two-Factor Authentication enabled successfully!', 'guard-dog')));
            } else {
                // --- Verification failed --- 
                wp_send_json_error(array('message' => __('Verification code is incorrect. Please try again.', 'guard-dog')), 400);
            }
        } catch (\Exception $e) {
             if (function_exists('Guard_Dog') && Guard_Dog()->debug) {
                 Guard_Dog()->debug->error('2FA Verification Error: ' . $e->getMessage());
             }
             wp_send_json_error(array('message' => __('An error occurred during verification. Please try again.', 'guard-dog')), 500);
        }
    }

    /**
     * Handle the frontend account-security shortcodes.
     *
     * @since 1.6.0
     * @param array $atts Shortcode attributes.
     * @return string HTML output for the 2FA interface.
     */
    public function shortcode_handler($atts) {
        // Only allow logged-in users
        if (!is_user_logged_in()) {
            return '<div class="guard-dog-2fa-notice guard-dog-2fa-error">' . 
                   esc_html__('You must be logged in to manage two-factor authentication.', 'guard-dog') . 
                   '</div>';
        }

        $atts = $this->normalize_shortcode_atts( $atts );

        // Enqueue CSS and JavaScript assets
        $this->enqueue_shortcode_assets();

	        return Guard_Dog_View_Loader::load_partial('shortcode-2fa', [
	            'atts' => $atts,
	            'passkeys_markup' => $this->get_shortcode_passkeys_markup( get_current_user_id(), $atts ),
	            'sessions_markup' => $this->get_shortcode_sessions_markup( get_current_user_id(), $atts ),
	            'this' => $this
	        ]);
	    }

	    /**
	     * Handle the standalone frontend 2FA management shortcode.
	     *
	     * @since 1.9.40
	     * @param array|string $atts Shortcode attributes.
	     * @return string HTML output for the 2FA-only interface.
	     */
	    public function two_factor_shortcode_handler( $atts ) {
	        if ( ! is_user_logged_in() ) {
	            return '<div class="guard-dog-2fa-notice guard-dog-2fa-error">' .
	                   esc_html__( 'You must be logged in to manage two-factor authentication.', 'guard-dog' ) .
	                   '</div>';
	        }

	        $atts = $this->normalize_two_factor_shortcode_atts( $atts );

	        $this->enqueue_shortcode_assets();

	        return Guard_Dog_View_Loader::load_partial(
	            'shortcode-2fa',
	            array(
	                'atts'            => $atts,
	                'passkeys_markup' => '',
	                'sessions_markup' => '',
	                'this'            => $this,
	            )
	        );
	    }

	    /**
	     * Handle the standalone frontend sessions shortcode.
	     *
	     * @since 1.9.40
	     * @param array|string $atts Shortcode attributes.
	     * @return string HTML output for the sessions interface.
	     */
	    public function sessions_shortcode_handler( $atts ) {
	        if ( ! is_user_logged_in() ) {
	            return '<div class="guard-dog-2fa-notice guard-dog-2fa-error">' .
	                   esc_html__( 'You must be logged in to manage active sessions.', 'guard-dog' ) .
	                   '</div>';
	        }

	        $atts = $this->normalize_sessions_shortcode_atts( $atts );

	        return $this->get_shortcode_sessions_markup(
	            get_current_user_id(),
	            array(
	                'style'         => $atts['style'],
	                'show_sessions' => true,
	            )
	        );
	    }

	    /**
	     * Prime frontend assets for an account-security surface before rendering.
	     *
	     * Useful for templates that render shortcodes after wp_head has already run.
	     *
	     * @since 1.9.40
	     * @param array|string $atts Account-security shortcode attributes.
	     * @return void
	     */
	    public function enqueue_account_security_assets( $atts = array() ) {
	        $atts = $this->normalize_shortcode_atts( $atts );

	        $this->enqueue_shortcode_assets();

	        if ( ! is_user_logged_in() ) {
	            return;
	        }

	        if ( ! empty( $atts['show_passkeys'] ) ) {
	            $this->get_shortcode_passkeys_markup( get_current_user_id(), $atts );
	        }

	        if ( ! empty( $atts['show_sessions'] ) && function_exists( 'Guard_Dog' ) && isset( Guard_Dog()->features['session_manager'] ) ) {
	            $session_manager = Guard_Dog()->features['session_manager'];
	            if ( is_object( $session_manager ) && method_exists( $session_manager, 'enqueue_frontend_assets' ) ) {
	                $session_manager->enqueue_frontend_assets();
	            }
	        }
	    }

	    /**
	     * Register the Gutenberg block wrapper for the account-security widget.
     *
     * @since 1.9.40
     */
    public function register_block() {
        if ( ! function_exists( 'register_block_type' ) || ! class_exists( '\WP_Block_Type_Registry' ) ) {
            return;
        }

        $registry = \WP_Block_Type_Registry::get_instance();

        if ( ! $registry->is_registered( self::BLOCK_NAME ) ) {
            $this->register_block_editor_assets();

            register_block_type(
                self::BLOCK_NAME,
                array(
                    'api_version'     => 2,
                    'editor_script'   => self::BLOCK_EDITOR_SCRIPT,
                    'render_callback' => array( $this, 'render_block' ),
                    'attributes'      => $this->get_block_attributes_schema(),
                )
            );
        }

        if ( ! $registry->is_registered( self::TWO_FACTOR_BLOCK_NAME ) ) {
            $this->register_two_factor_block_editor_assets();

            register_block_type(
                self::TWO_FACTOR_BLOCK_NAME,
                array(
                    'api_version'     => 2,
                    'editor_script'   => self::TWO_FACTOR_BLOCK_EDITOR_SCRIPT,
                    'render_callback' => array( $this, 'render_two_factor_block' ),
                    'attributes'      => $this->get_style_only_block_attributes_schema(),
                )
            );
        }

        if ( ! $registry->is_registered( self::SESSIONS_BLOCK_NAME ) ) {
            $this->register_sessions_block_editor_assets();

            register_block_type(
                self::SESSIONS_BLOCK_NAME,
                array(
                    'api_version'     => 2,
                    'editor_script'   => self::SESSIONS_BLOCK_EDITOR_SCRIPT,
                    'render_callback' => array( $this, 'render_sessions_block' ),
                    'attributes'      => $this->get_style_only_block_attributes_schema(),
                )
            );
        }
    }

    /**
     * Register the editor-side block assets.
     *
     * @since 1.9.40
     */
    public function register_block_editor_assets() {
        if ( wp_script_is( self::BLOCK_EDITOR_SCRIPT, 'registered' ) ) {
            return;
        }

        wp_register_script(
            self::BLOCK_EDITOR_SCRIPT,
            GUARD_DOG_URL . 'assets/js/guard-dog-account-security-block.js',
            array( 'wp-blocks', 'wp-element', 'wp-components', 'wp-i18n', 'wp-block-editor' ),
            GUARD_DOG_VERSION,
            true
        );
    }

    /**
     * Register the editor-side standalone 2FA block assets.
     *
     * @since 1.9.40
     */
    public function register_two_factor_block_editor_assets() {
        if ( wp_script_is( self::TWO_FACTOR_BLOCK_EDITOR_SCRIPT, 'registered' ) ) {
            return;
        }

        wp_register_script(
            self::TWO_FACTOR_BLOCK_EDITOR_SCRIPT,
            GUARD_DOG_URL . 'assets/js/guard-dog-two-factor-auth-block.js',
            array( 'wp-blocks', 'wp-element', 'wp-components', 'wp-i18n', 'wp-block-editor' ),
            GUARD_DOG_VERSION,
            true
        );
    }

    /**
     * Register the editor-side standalone sessions block assets.
     *
     * @since 1.9.40
     */
    public function register_sessions_block_editor_assets() {
        if ( wp_script_is( self::SESSIONS_BLOCK_EDITOR_SCRIPT, 'registered' ) ) {
            return;
        }

        wp_register_script(
            self::SESSIONS_BLOCK_EDITOR_SCRIPT,
            GUARD_DOG_URL . 'assets/js/guard-dog-sessions-block.js',
            array( 'wp-blocks', 'wp-element', 'wp-components', 'wp-i18n', 'wp-block-editor' ),
            GUARD_DOG_VERSION,
            true
        );
    }

    /**
     * Render the account-security block using the canonical shortcode path.
     *
     * @since 1.9.40
     * @param array  $attributes Block attributes.
     * @param string $content    Block content.
     * @return string
     */
    public function render_block( $attributes = array(), $content = '' ) {
        unset( $content );

        return $this->shortcode_handler( (array) $attributes );
    }

    /**
     * Render the standalone 2FA block using the canonical primitive shortcode path.
     *
     * @since 1.9.40
     * @param array  $attributes Block attributes.
     * @param string $content    Block content.
     * @return string
     */
    public function render_two_factor_block( $attributes = array(), $content = '' ) {
        unset( $content );

        return $this->two_factor_shortcode_handler( (array) $attributes );
    }

    /**
     * Render the standalone sessions block using the canonical shortcode path.
     *
     * @since 1.9.40
     * @param array  $attributes Block attributes.
     * @param string $content    Block content.
     * @return string
     */
    public function render_sessions_block( $attributes = array(), $content = '' ) {
        unset( $content );

        return $this->sessions_shortcode_handler( (array) $attributes );
    }

    /**
     * Return the block attribute schema for the account-security block.
     *
     * @since 1.9.40
     * @return array
     */
    private function get_block_attributes_schema() {
        return array(
            'style'         => array(
                'type'    => 'string',
                'default' => 'default',
            ),
	            'show_2fa'      => array(
	                'type'    => 'boolean',
	                'default' => true,
	            ),
	            'show_passkeys' => array(
	                'type'    => 'boolean',
	                'default' => true,
	            ),
	            'show_sessions' => array(
	                'type'    => 'boolean',
	                'default' => false,
	            ),
	        );
	    }

    /**
     * Return the block attribute schema for primitive style-only blocks.
     *
     * @since 1.9.40
     * @return array
     */
    private function get_style_only_block_attributes_schema() {
        return array(
            'style' => array(
                'type'    => 'string',
                'default' => 'default',
            ),
        );
    }

    /**
     * Normalize shortcode attributes for the frontend account-security widget.
     *
     * @since 1.9.40
     * @param array|string $atts Raw shortcode attributes.
     * @return array
     */
    private function normalize_shortcode_atts( $atts ) {
        $atts = shortcode_atts(
	            array(
	                'style'         => 'default',
	                'show_2fa'      => 'true',
	                'show_passkeys' => 'true',
	                'show_sessions' => 'false',
	            ),
            (array) $atts,
            self::SHORTCODE
        );

	        return array(
	            'style'         => $this->normalize_frontend_style( $atts['style'] ),
	            'show_2fa'      => $this->to_bool( $atts['show_2fa'] ),
	            'show_passkeys' => $this->to_bool( $atts['show_passkeys'] ),
	            'show_sessions' => $this->to_bool( $atts['show_sessions'] ),
	        );
	    }

    /**
     * Normalize shortcode attributes for the standalone 2FA widget.
     *
     * @since 1.9.40
     * @param array|string $atts Raw shortcode attributes.
     * @return array
     */
    private function normalize_two_factor_shortcode_atts( $atts ) {
        $atts = shortcode_atts(
            array(
                'style' => 'default',
            ),
            (array) $atts,
            self::TWO_FACTOR_SHORTCODE
        );

        return array(
            'style'         => $this->normalize_frontend_style( $atts['style'] ),
            'show_2fa'      => true,
            'show_passkeys' => false,
            'show_sessions' => false,
        );
    }

    /**
     * Normalize shortcode attributes for the standalone sessions widget.
     *
     * @since 1.9.40
     * @param array|string $atts Raw shortcode attributes.
     * @return array
     */
    private function normalize_sessions_shortcode_atts( $atts ) {
        $atts = shortcode_atts(
            array(
                'style' => 'default',
            ),
            (array) $atts,
            self::SESSIONS_SHORTCODE
        );

        return array(
            'style' => $this->normalize_frontend_style( $atts['style'] ),
        );
    }

    /**
     * Normalize the frontend visual style attribute.
     *
     * @since 1.9.40
     * @param mixed $style Raw style value.
     * @return string
     */
    private function normalize_frontend_style( $style ) {
        $style = is_string( $style ) ? sanitize_key( $style ) : 'default';

        if ( ! in_array( $style, array( 'default', 'minimal', 'compact' ), true ) ) {
            return 'default';
        }

        return $style;
    }

    /**
     * Convert shortcode-ish booleans to true booleans.
     *
     * @since 1.9.40
     * @param mixed $value Raw attribute value.
     * @return bool
     */
    private function to_bool( $value ) {
        if ( is_bool( $value ) ) {
            return $value;
        }

        $normalized = strtolower( trim( (string) $value ) );

        return ! in_array( $normalized, array( '', '0', 'false', 'no', 'off' ), true );
    }

    /**
     * Get passkey management markup for the frontend account-security widget.
     *
     * @since 1.9.40
     * @param int   $user_id Current user ID.
     * @param array $atts    Normalized shortcode attributes.
     * @return string
     */
    private function get_shortcode_passkeys_markup( $user_id, $atts ) {
        if ( empty( $atts['show_passkeys'] ) || ! function_exists( 'Guard_Dog' ) || ! isset( Guard_Dog()->features['passkeys'] ) ) {
            return '';
        }

        $passkeys = Guard_Dog()->features['passkeys'];
        if ( ! is_object( $passkeys ) || ! method_exists( $passkeys, 'get_management_markup' ) ) {
            return '';
        }

        if ( method_exists( $passkeys, 'is_enabled' ) && ! $passkeys->is_enabled() ) {
            return '';
        }

        $user = get_userdata( $user_id );
        if ( ! $user ) {
            return '';
        }

        $markup = (string) $passkeys->get_management_markup(
            $user,
            array(
                'context' => 'shortcode',
                'style'   => $atts['style'] ?? 'default',
            )
        );

        if ( '' !== $markup && method_exists( $passkeys, 'enqueue_management_assets' ) ) {
            $passkeys->enqueue_management_assets();
        }

	        return $markup;
	    }

	    /**
	     * Get active-session management markup for the frontend account-security widget.
	     *
	     * @since 1.9.40
	     * @param int   $user_id Current user ID.
	     * @param array $atts    Normalized shortcode attributes.
	     * @return string
	     */
	    private function get_shortcode_sessions_markup( $user_id, $atts ) {
	        if ( empty( $atts['show_sessions'] ) || ! function_exists( 'Guard_Dog' ) || ! isset( Guard_Dog()->features['session_manager'] ) ) {
	            return '';
	        }

	        $session_manager = Guard_Dog()->features['session_manager'];
	        if ( ! is_object( $session_manager ) || ! method_exists( $session_manager, 'get_frontend_management_markup' ) ) {
	            return '';
	        }

	        if ( method_exists( $session_manager, 'is_enabled' ) && ! $session_manager->is_enabled() ) {
	            return '';
	        }

	        $user = get_userdata( $user_id );
	        if ( ! $user ) {
	            return '';
	        }

	        $markup = (string) $session_manager->get_frontend_management_markup(
	            $user,
	            array(
	                'context' => 'shortcode',
	                'style'   => $atts['style'] ?? 'default',
	            )
	        );

	        if ( '' !== $markup && method_exists( $session_manager, 'enqueue_frontend_assets' ) ) {
	            $session_manager->enqueue_frontend_assets();
	        }

	        return $markup;
	    }

    //--------------------------------------------------------------------------
    // Additional AJAX Handlers for Shortcode
    //--------------------------------------------------------------------------

    /**
     * AJAX handler to view recovery codes.
     *
     * @since 1.7.0
     */
    public function ajax_view_recovery_codes() {
        // Check if user is logged in first
        if (!is_user_logged_in()) {
            wp_send_json_error(array('message' => __('You must be logged in to use this feature.', 'guard-dog')), 403);
        }

        // Get current user ID first (no POST data access needed)
        $current_user_id = get_current_user_id();
        $expected_action = 'guard_dog_view_recovery_codes_' . $current_user_id;
        
        // Fail early if nonce is missing or invalid - use WordPress recommended pattern
        if ((!isset($_POST['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_wpnonce'])), $expected_action)) &&
            (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), $expected_action))) {
            wp_send_json_error(array('message' => __('Security verification failed.', 'guard-dog')), 403);
        }

        // Now safe to access POST data after nonce validation
        $user_id = isset($_POST['user_id']) ? absint($_POST['user_id']) : 0;

        // Security checks
        if ($user_id !== $current_user_id) {
            wp_send_json_error(array('message' => __('Permission denied.', 'guard-dog')), 403);
        }

        // Check if 2FA is enabled
        if (!$this->is_2fa_enabled_for_user($user_id)) {
            wp_send_json_error(array('message' => __('Two-factor authentication is not enabled.', 'guard-dog')), 400);
        }

        // Get recovery codes
        $encrypted_codes = get_user_meta($user_id, self::USER_META_RECOVERY_CODES, true);
        $recovery_codes = $this->decode_recovery_codes($encrypted_codes);
        if (empty($recovery_codes)) {
            wp_send_json_error(array('message' => __('No recovery codes found.', 'guard-dog')), 400);
        }

        wp_send_json_error([
            'message' => __('Recovery codes cannot be displayed for security reasons. Please generate new codes if needed.', 'guard-dog')
        ], 400);
    }

    /**
     * AJAX handler to regenerate recovery codes.
     *
     * @since 1.7.0
     */
    public function ajax_regenerate_recovery_codes() {
        // Check if user is logged in first
        if (!is_user_logged_in()) {
            wp_send_json_error(array('message' => __('You must be logged in to use this feature.', 'guard-dog')), 403);
        }

        // Get current user ID first (no POST data access needed)
        $current_user_id = get_current_user_id();
        $expected_action = 'guard_dog_regenerate_recovery_codes_' . $current_user_id;
        
        // Fail early if nonce is missing or invalid - use WordPress recommended pattern
        if ((!isset($_POST['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_wpnonce'])), $expected_action)) &&
            (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), $expected_action))) {
            wp_send_json_error(array('message' => __('Security verification failed.', 'guard-dog')), 403);
        }

        // Now safe to access POST data after nonce validation
        $user_id = isset($_POST['user_id']) ? absint($_POST['user_id']) : 0;
        if ($user_id !== $current_user_id) {
            wp_send_json_error(array('message' => __('Permission denied.', 'guard-dog')), 403);
        }

        // Check if 2FA is enabled
        if (!$this->is_2fa_enabled_for_user($user_id)) {
            wp_send_json_error(array('message' => __('Two-factor authentication is not enabled.', 'guard-dog')), 400);
        }

        // Generate new recovery codes
        $new_recovery_codes = $this->generate_recovery_codes();
        
        // Store codes encrypted in user meta for validation during login
        // This prevents them from being displayed again but allows validation
        $encrypted_codes = $this->encode_recovery_codes($new_recovery_codes);
        update_user_meta($user_id, self::USER_META_RECOVERY_CODES, $encrypted_codes);

        wp_send_json_success([
            'message' => __('New recovery codes generated successfully. Save them now - they will only be shown once.', 'guard-dog'),
            'codes' => $new_recovery_codes,
            'count' => count($new_recovery_codes)
        ]);
    }

    /**
     * AJAX handler to disable 2FA.
     *
     * @since 1.7.0
     */
    public function ajax_disable_2fa() {
        // Check if user is logged in first
        if (!is_user_logged_in()) {
            wp_send_json_error(array('message' => __('You must be logged in to use this feature.', 'guard-dog')), 403);
        }

        // Get current user ID first (no POST data access needed)
        $current_user_id = get_current_user_id();
        $expected_action = 'guard_dog_disable_2fa_' . $current_user_id;
        
        // Fail early if nonce is missing or invalid - use WordPress recommended pattern
        if ((!isset($_POST['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_wpnonce'])), $expected_action)) &&
            (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), $expected_action))) {
            wp_send_json_error(array('message' => __('Security verification failed.', 'guard-dog')), 403);
        }

        // Now safe to access POST data after nonce validation
        $user_id = isset($_POST['user_id']) ? absint($_POST['user_id']) : 0;
        if ($user_id !== $current_user_id) {
            wp_send_json_error(array('message' => __('Permission denied.', 'guard-dog')), 403);
        }

        // Check if 2FA is enabled
        if (!$this->is_2fa_enabled_for_user($user_id)) {
            wp_send_json_error(array('message' => __('Two-factor authentication is already disabled.', 'guard-dog')), 400);
        }

        // Delete all 2FA data
        delete_user_meta($user_id, self::USER_META_ENABLED_KEY);
        delete_user_meta($user_id, self::USER_META_SECRET_KEY);
        delete_user_meta($user_id, self::USER_META_RECOVERY_CODES);
        delete_user_meta($user_id, '_guard_dog_2fa_temp_secret');

        wp_send_json_success([
            'message' => __('Two-factor authentication has been disabled successfully.', 'guard-dog')
        ]);
    }

    /**
     * Get relevant POST keys for debug logging (only plugin-specific fields)
     *
     * @return array Array of relevant POST keys
     */
    private function get_relevant_post_keys(): array {
        $relevant_keys = [];
        $expected_keys = ['_wpnonce', 'nonce', 'user_id', 'guard_dog_2fa_nonce', 'guard_dog_2fa_code', 'action'];
        
        foreach ($expected_keys as $key) {
            if (isset($_POST[$key])) {
                $relevant_keys[] = $key;
            }
        }
        
        return $relevant_keys;
    }

    /**
     * Enqueue shortcode assets (CSS and JavaScript).
     *
     * @since 1.7.0
     */
    private function enqueue_shortcode_assets() {
        // Only enqueue once per page
        static $assets_enqueued = false;
        if ($assets_enqueued) {
            return;
        }
        $assets_enqueued = true;

        // Enqueue CSS
        wp_enqueue_style(
            'guard-dog-shortcode-2fa',
            GUARD_DOG_URL . 'assets/css/guard-dog-shortcode-2fa.css',
            [],
            GUARD_DOG_VERSION
        );

        // Enqueue JavaScript
        wp_enqueue_script(
            'guard-dog-shortcode-2fa',
            GUARD_DOG_URL . 'assets/js/guard-dog-shortcode-2fa.js',
            ['jquery'],
            GUARD_DOG_VERSION,
            true
        );

        // Localize script with AJAX URL and other data
        $user_id = get_current_user_id();
        
        // Check if email 2FA is available
        $email_2fa_enabled = get_option('guard_dog_email_2fa_enabled', false);
        $email_provider_configured = false;
        
        if ( $email_2fa_enabled && function_exists( 'Guard_Dog' ) && isset( Guard_Dog()->features['email_two_factor_auth'] ) ) {
            $email_2fa_instance = Guard_Dog()->features['email_two_factor_auth'];
            $email_provider_configured = $email_2fa_instance->is_email_provider_configured();
        }
        
        // Create nonces with correct action names.
        $nonces = array(
            'generate' => wp_create_nonce( 'guard_dog_generate_2fa_secret_' . $user_id ),
            'verify' => wp_create_nonce( 'guard_dog_verify_2fa_setup_' . $user_id ),
            'regenerateRecovery' => wp_create_nonce( 'guard_dog_regenerate_recovery_codes_' . $user_id ),
            'disable' => wp_create_nonce( 'guard_dog_disable_2fa_' . $user_id ),
            'refresh' => wp_create_nonce( 'guard_dog_refresh_2fa_nonces_' . $user_id ),
        );
        
        // Add email 2FA nonces if available.
        if ( $email_2fa_enabled && $email_provider_configured ) {
            $nonces = array_merge( $nonces, array(
                'emailEnable' => wp_create_nonce( 'guard_dog_enable_email_2fa_' . $user_id ),
                'emailVerify' => wp_create_nonce( 'guard_dog_verify_email_2fa_code_' . $user_id ),
                'emailSendCode' => wp_create_nonce( 'guard_dog_send_email_2fa_code_' . $user_id ),
                'emailDisable' => wp_create_nonce( 'guard_dog_disable_email_2fa_' . $user_id ),
            ) );
        }
        
        // Debug logging for nonce creation.
        if ( function_exists( 'Guard_Dog' ) && Guard_Dog()->debug ) {
            Guard_Dog()->debug->info( 'Unified 2FA Shortcode nonces created', 'two_factor', array(
                'user_id' => $user_id,
                'nonce_actions' => array_keys( $nonces ),
                'email_2fa_available' => $email_2fa_enabled && $email_provider_configured,
                'nonce_lengths' => array_map( 'strlen', $nonces )
            ) );
        }
        
        wp_localize_script( 'guard-dog-shortcode-2fa', 'guard_dog_2fa_shortcode', array(
            'ajaxUrl' => admin_url( 'admin-ajax.php' ),
            'userId' => $user_id,
            'nonces' => $nonces,
            'strings' => array(
                'loading' => __( 'Loading...', 'guard-dog' ),
                'error' => __( 'An error occurred.', 'guard-dog' ),
                'networkError' => __( 'Network error. Please try again.', 'guard-dog' ),
                'confirmRegenerate' => __( 'Are you sure you want to regenerate recovery codes? This will invalidate all existing codes.', 'guard-dog' ),
                'confirmDisable' => __( 'Are you sure you want to disable two-factor authentication? This will remove all security codes and make your account less secure.', 'guard-dog' ),
            )
        ) );
    }

    /**
     * Encode recovery codes for storage (encoding only, not encryption or confidentiality).
     *
     * @since 1.8.1
     * @param array $codes Array of recovery codes to encode.
     * @return string Encoded codes string.
     * @todo Encrypt recovery codes (e.g. libsodium/OpenSSL) with a wp_salt()-derived key
     *       for confidentiality. Anyone with DB/user_meta access can currently decode.
     *       Requires key storage and rotation design. See docs/reviews/CODE_REVIEW_2FA_PASSWORD_POLICY_2025-01.md H2.
     */
    private function encode_recovery_codes($codes) {
        if (!is_array($codes)) {
            return '';
        }
        
        $codes_string = implode('|', $codes);
        return base64_encode($codes_string);
    }

    /**
     * Decode recovery codes from the stored format (encoding only, not decryption).
     *
     * @since 1.2.0
     * @param string $encoded_codes The encoded recovery codes.
     * @return array Array of recovery codes.
     */
    public function decode_recovery_codes($encoded_codes) {
        if ( empty( $encoded_codes ) ) {
            return array();
        }
        // If already an array, return as-is (for legacy or error cases).
        if ( is_array( $encoded_codes ) ) {
            return $encoded_codes;
        }
        $decoded = base64_decode( $encoded_codes );
        return $decoded ? explode( '|', $decoded ) : array();
    }

    /**
     * Generate a set of unique recovery codes.
     *
     * @since 1.2.0
     * @param int $count Number of codes to generate.
     * @return array Array of generated recovery codes.
     */
    private function generate_recovery_codes($count = 10) {
        $codes = array();
        $charset = 'abcdefghjkmnpqrstuvwxyz23456789'; // User-friendly charset (no confusing chars like 1/l/I, 0/O).
        $charset_max = strlen( $charset ) - 1;

        for ( $i = 0; $i < $count; $i++ ) {
            $code = '';
            for ( $j = 0; $j < 12; $j++ ) { // e.g., 12 characters long.
                $code .= $charset[random_int( 0, $charset_max )];
                if ( ( $j + 1 ) % 4 === 0 && $j !== 11 ) { // Add dashes for readability.
                    $code .= '-';
                }
            }
            // Ensure uniqueness (highly unlikely collision, but check anyway).
            if ( ! in_array( $code, $codes ) ) {
                $codes[] = $code;
            } else {
                $i--; // Decrement to retry generating a unique code.
            }
        }
        return $codes;
    }

    //--------------------------------------------------------------------------
    // Login Flow Integration
    //--------------------------------------------------------------------------

    /**
     * Intercepts the login check to add 2FA verification.
     *
     * @since 1.2.0
     * @param WP_User|WP_Error|null $user The user object or error.
     * @param string $username The username.
     * @param string $password The password.
     * @return WP_User|WP_Error|null The user object, error, or null.
     */
    public function intercept_login_check($user, $username, $password) {
        // Debug logging to see if this hook is being called
        if (function_exists('Guard_Dog') && Guard_Dog()->debug) {
            Guard_Dog()->debug->info('2FA intercept_login_check called', 'two_factor', [
                'user_id' => ($user && !is_wp_error($user)) ? $user->ID : 'null',
                'username' => ($user && !is_wp_error($user)) ? $user->user_login : 'null',
                'is_wp_error' => is_wp_error($user),
                'relevant_post_keys' => $this->get_relevant_post_keys()
            ]);
        }

        // If there's already an error, return it
        if (is_wp_error($user)) {
            return $user;
        }

        // If no user found, return null (let WordPress handle it)
        if (!$user) {
            return null;
        }

        // Check if user was authenticated via passkey (WebAuthn)
        // Passkeys are inherently MFA (possession + biometric), so 2FA is redundant
        if (class_exists('Guard_Dog_Passkeys') && Guard_Dog_Passkeys::was_passkey_authenticated($user->ID)) {
            $passkey_bypass_2fa = get_option('guard_dog_passkeys_bypass_2fa', true);
            if ($passkey_bypass_2fa) {
                if (function_exists('Guard_Dog') && Guard_Dog()->debug) {
                    Guard_Dog()->debug->info('2FA bypassed - user authenticated via passkey', 'two_factor', [
                        'user_id' => $user->ID,
                        'username' => $user->user_login
                    ]);
                }
                // Clear the passkey flag after use
                Guard_Dog_Passkeys::clear_passkey_flag($user->ID);
                return $user;
            }
        }

        // Check if user was authenticated via OAuth social login
        // OAuth providers verify identity through their own authentication, so 2FA can be bypassed
        $social_bypass_enabled = get_option('guard_dog_social_bypass_2fa', false);
        $oauth_transient_key = Guard_Dog_Social_Login::OAUTH_AUTH_TRANSIENT_PREFIX . $user->ID;
        if ($social_bypass_enabled && get_transient($oauth_transient_key)) {
            if (function_exists('Guard_Dog') && Guard_Dog()->debug) {
                Guard_Dog()->debug->info('2FA bypassed - user authenticated via social login', 'two_factor', [
                    'user_id' => $user->ID,
                    'username' => $user->user_login
                ]);
            }
            delete_transient($oauth_transient_key);
            return $user;
        }

        // 2FA form submissions are now handled by handle_2fa_form_submission() method
        // This method only handles the initial login check for 2FA requirement

        // Check if 2FA is enabled globally
        $tfa_enabled = get_option('guard_dog_two_factor_enabled', false);
        
        // Debug logging for 2FA global check
        if (function_exists('Guard_Dog') && Guard_Dog()->debug) {
            Guard_Dog()->debug->info('2FA global check', 'two_factor', [
                'tfa_enabled' => $tfa_enabled,
                'user_id' => $user->ID,
                'username' => $user->user_login
            ]);
        }
        
        if (!$tfa_enabled) {
            if (function_exists('Guard_Dog') && Guard_Dog()->debug) {
                Guard_Dog()->debug->info('2FA globally disabled, allowing login', 'two_factor');
            }
            return $user;
        }

        // Check if 2FA is enforced and if this user needs to be required to set it up
        $tfa_enforced = get_option('guard_dog_two_factor_enforced', false);
        $user_has_any_2fa = $this->user_has_any_2fa_enabled($user);
        
        // Debug logging for user 2FA check
        if (function_exists('Guard_Dog') && Guard_Dog()->debug) {
            Guard_Dog()->debug->info('2FA user check', 'two_factor', [
                'tfa_enforced' => $tfa_enforced,
                'user_has_any_2fa' => $user_has_any_2fa,
                'user_id' => $user->ID,
                'username' => $user->user_login
            ]);
        }
        
        if ($tfa_enforced && !$user_has_any_2fa) {
            // Check if grace period has expired
            $enforcement_date = get_option('guard_dog_two_factor_enforcement_date', '');
            $grace_period = get_option('guard_dog_two_factor_grace_period', 7);
            
            if ($enforcement_date) {
                $enforcement_timestamp = intval($enforcement_date) + ($grace_period * DAY_IN_SECONDS);
                $current_time = time();
                
                if ($current_time >= $enforcement_timestamp) {
                    // Grace period has expired - redirect to 2FA setup interstitial
                    // phpcs:disable WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended
                    // Reading redirect_to from login form submission; actual form processing happens after redirect
                    $redirect_to = '';
                    if (isset($_POST['redirect_to']) && is_string($_POST['redirect_to'])) {
                        $redirect_to = esc_url_raw(wp_unslash($_POST['redirect_to']));
                    } elseif (isset($_GET['redirect_to']) && is_string($_GET['redirect_to'])) {
                        $redirect_to = esc_url_raw(wp_unslash($_GET['redirect_to']));
                    }
                    // phpcs:enable WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended

                    if (empty($redirect_to)) {
                        $redirect_to = admin_url();
                    }

                    // Redirect to setup interstitial instead of blocking
                    $this->redirect_to_2fa_setup($user, $redirect_to);

                    // Fallback error if redirect fails (should not reach here)
                    return new WP_Error(
                        self::ERROR_CODE_REQUIRED,
                        __('Two-factor authentication is required. Please set up 2FA to continue.', 'guard-dog')
                    );
                } else {
                    // Still in grace period - allow login but show warning
                    $days_remaining = ceil(($enforcement_timestamp - $current_time) / DAY_IN_SECONDS);
                    $this->activity_log->add_log_entry('login_warning_2fa_required', [
                        'user_id' => $user->ID,
                        'username' => $user->user_login,
                        'days_remaining' => $days_remaining,
                        // Translators: %d is the number of days remaining for the user to set up 2FA.
                        'message' => sprintf(esc_html__('User logged in but 2FA setup required within %d days', 'guard-dog'), esc_html($days_remaining))
                    ]);
                    
                    // Add a transient to show warning on next page load
                    set_transient('guard_dog_2fa_grace_warning_' . $user->ID, $days_remaining, 3600);

                    // User is in grace period - allow login without 2FA prompt
                    // They will see a warning in the admin to set up 2FA
                    return $user;
                }
            }
        }

        // Check if user has app-based (TOTP) 2FA enabled
        $user_has_2fa = $this->is_2fa_enabled_for_user($user);

        // Check if user has email 2FA enabled
        $email_2fa_enabled = get_option('guard_dog_email_2fa_enabled', false);
        $user_has_email_2fa = false;
        
        if ($email_2fa_enabled && function_exists('Guard_Dog') && isset(Guard_Dog()->features['email_two_factor_auth'])) {
            $user_has_email_2fa = Guard_Dog()->features['email_two_factor_auth']->is_email_2fa_enabled_for_user($user);
        }
        
        // If user has either app-based or email 2FA enabled, require verification
        if ($user_has_2fa || $user_has_email_2fa) {
            // Debug logging for 2FA requirement
            if (function_exists('Guard_Dog') && Guard_Dog()->debug) {
                Guard_Dog()->debug->info('2FA required for user, redirecting to 2FA form', 'two_factor', [
                    'user_id' => $user->ID,
                    'username' => $user->user_login,
                    'tfa_enabled' => $tfa_enabled,
                    'tfa_enforced' => $tfa_enforced,
                    'user_has_2fa' => $user_has_2fa,
                    'user_has_email_2fa' => $user_has_email_2fa
                ]);
            }
            // Store user info in transient for 2FA verification
            $transient_key = $this->get_2fa_transient_key( $user->user_login );
            $flow_token = wp_generate_password(32, false);
            $remember_me = filter_input(INPUT_POST, 'rememberme', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
            $redirect_to_param = filter_input(INPUT_POST, 'redirect_to', FILTER_SANITIZE_URL);
            $redirect_to = wp_validate_redirect($redirect_to_param ?: admin_url(), admin_url());
            
            // Store CAPTCHA token if present (for CAPTCHA + 2FA integration)
            $captcha_token = '';
            if (isset($_POST['g-recaptcha-response'])) {
                $captcha_token = sanitize_text_field(wp_unslash($_POST['g-recaptcha-response']));
            } elseif (isset($_POST['h-captcha-response'])) {
                $captcha_token = sanitize_text_field(wp_unslash($_POST['h-captcha-response']));
            } elseif (isset($_POST['cf-turnstile-response'])) {
                $captcha_token = sanitize_text_field(wp_unslash($_POST['cf-turnstile-response']));
            }
            
            $transient_data = [
                'user_id' => $user->ID,
                'username' => $user->user_login,
                'remember' => ($remember_me !== null),
                'redirect_to' => $redirect_to,
                'flow_token' => $flow_token,
                'captcha_token' => $captcha_token, // Store CAPTCHA token for 2FA step
                'passkey_setup_intent' => $this->has_passkey_setup_intent_from_post(),
            ];
            
            // Store for 10 minutes
            set_transient($transient_key, $transient_data, 600);
            
            // Redirect to 2FA form using state token to identify 2FA flow
            $redirect_url = Guard_Dog_Utils::create_login_state_url(
                Guard_Dog_Utils::get_login_base_url(),
                array(
                    'type'       => '2fa',
                    'username'   => $user->user_login,
                    'flow_token' => $flow_token,
                )
            );

            wp_safe_redirect($redirect_url);
            exit;
        }

        // User doesn't have 2FA enabled and it's not enforced, allow login
        return $user;
    }

    /**
     * Handle 2FA form submission early in the login process.
     *
     * @since 1.8.42
     */
    public function handle_2fa_form_submission() {
        // Check if this is a 2FA form submission
        $interim_login = filter_input(INPUT_POST, 'guard_dog_2fa_interim_login', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
        if ($interim_login !== '1') {
            return; // Not a 2FA form submission
        }

        // Get the username from the form
        $username = filter_input(INPUT_POST, 'log', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
        if (empty($username)) {
            return;
        }

        // Get the user object
        $user = get_user_by('login', $username);
        if (!$user) {
            return;
        }

        // Get the 2FA code and method
        $guard_dog_2fa_code = filter_input(INPUT_POST, 'guard_dog_2fa_code', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
        $guard_dog_2fa_nonce = filter_input(INPUT_POST, 'guard_dog_2fa_nonce', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
        $guard_dog_2fa_method = filter_input(INPUT_POST, 'guard_dog_2fa_method', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
        $guard_dog_2fa_flow_token = filter_input(INPUT_POST, 'guard_dog_2fa_flow_token', FILTER_SANITIZE_FULL_SPECIAL_CHARS);

        if (empty($guard_dog_2fa_code) || empty($guard_dog_2fa_nonce) || empty($guard_dog_2fa_flow_token)) {
            return;
        }

        $transient_key = $this->get_2fa_transient_key( $username );
        $transient_data = get_transient( $transient_key );
        if (
            ! is_array( $transient_data )
            || (int) ( $transient_data['user_id'] ?? 0 ) !== (int) $user->ID
            || empty( $transient_data['flow_token'] )
            || ! hash_equals( (string) $transient_data['flow_token'], (string) $guard_dog_2fa_flow_token )
        ) {
            wp_safe_redirect( wp_login_url() );
            exit;
        }

        if ( $this->is_2fa_rate_limited( $user->ID ) ) {
            wp_safe_redirect( $this->create_2fa_state_url( $username, $guard_dog_2fa_flow_token, self::ERROR_CODE_RATE_LIMITED ) );
            exit;
        }

        // Prepare sanitized post data
        $sanitized_post = array(
            'guard_dog_2fa_code' => $guard_dog_2fa_code,
            'guard_dog_2fa_nonce' => $guard_dog_2fa_nonce,
            'guard_dog_2fa_method' => $guard_dog_2fa_method ?: 'app'
        );

        // Validate the 2FA code
        $validation_result = $this->validate_2fa_code($user, $sanitized_post);
        
        if (is_wp_error($validation_result)) {
            // 2FA validation failed - redirect back to 2FA form with error code
            $error_code = $validation_result->get_error_code();
            
            $this->record_failed_2fa_attempt( $user->ID );
            $redirect_url = $this->create_2fa_state_url( $username, $guard_dog_2fa_flow_token, $error_code );
            wp_safe_redirect($redirect_url);
            exit;
        }

        // 2FA validation successful - complete the login process and exit
        $this->clear_failed_2fa_attempts( $user->ID );
        $this->complete_2fa_login($user);
        // This should never be reached due to exit in complete_2fa_login
    }

    /**
     * Renders the custom 2FA form if the action is correct, then exits.
     *
     * @since 1.8.0
     */
    public function maybe_render_2fa_form_and_exit() {
        // Check for 2FA flow via state token.
        $login_state = Guard_Dog_Utils::consume_login_state();
        $state_username = null;
        $state_error    = null;
        $state_flow_token = null;

        if ( is_array( $login_state ) && isset( $login_state['type'] ) && $login_state['type'] === '2fa' ) {
            $state_username = isset( $login_state['username'] ) ? sanitize_user( $login_state['username'] ) : null;
            $state_error    = isset( $login_state['error'] ) ? sanitize_text_field( $login_state['error'] ) : null;
            $state_flow_token = isset( $login_state['flow_token'] ) ? sanitize_text_field( $login_state['flow_token'] ) : null;
        }

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

        $username = $state_username;
        $username = $username ? sanitize_user($username) : '';
        if (empty($username) || empty($state_flow_token)) {
            wp_safe_redirect(wp_login_url());
            exit;
        }

        $transient_key = $this->get_2fa_transient_key( $username );
        $transient_data = get_transient($transient_key);

        if (false === $transient_data) {
            // Transient expired - redirect with error via state token
            $login_url = Guard_Dog_Utils::create_login_state_url(
                Guard_Dog_Utils::get_login_base_url(),
                array( 'type' => 'error', 'error' => 'session_expired' ),
                60
            );
            wp_safe_redirect($login_url);
            exit;
        }

        if (
            ! is_array( $transient_data )
            || empty( $transient_data['flow_token'] )
            || ! hash_equals( (string) $transient_data['flow_token'], (string) $state_flow_token )
        ) {
            wp_safe_redirect(wp_login_url());
            exit;
        }

        // Set up proper WordPress login page environment
        if (!function_exists('login_header')) {
            // This file is not loaded by default, so we need to include it.
            require_once(ABSPATH . 'wp-login.php');
        }

        // Set up global variables that login_header() expects
        global $error, $interim_login, $action;
        $error = null;
        $interim_login = false;
        $action = 'guard_dog_2fa';

        $user_id = $transient_data['user_id'];

        // Validate that user exists and actually has 2FA configured
        // This prevents showing the 2FA form if state is stale
        $user = get_user_by('ID', $user_id);
        if (!$user || !$this->user_has_any_2fa_enabled($user)) {
            delete_transient($transient_key);

            // Check if user needs to set up 2FA (enforcement active, grace period expired)
            if ($user && $this->should_require_2fa_setup($user)) {
                $this->redirect_to_2fa_setup($user, $transient_data['redirect_to'] ?? admin_url());
            }

            // Otherwise, redirect back to login
            wp_safe_redirect(wp_login_url());
            exit;
        }

        // Manually trigger script enqueueing since we're in a custom context
        $this->enqueue_login_scripts();

        // Also enqueue email 2FA scripts if available, passing the user ID
        if (function_exists('Guard_Dog') && isset(Guard_Dog()->features['email_two_factor_auth'])) {
            Guard_Dog()->features['email_two_factor_auth']->enqueue_login_scripts($user_id);
        }
        $rememberme = $transient_data['remember'];
        $redirect_to = $transient_data['redirect_to'];
        $captcha_token = $transient_data['captcha_token'] ?? '';

        // Determine CAPTCHA provider for token preservation
        $captcha_provider = '';
        if (!empty($captcha_token)) {
            // Check which CAPTCHA provider is active
            $active_provider = get_option('guard_dog_captcha_provider', 'none');
            switch ($active_provider) {
                case 'recaptcha_v2':
                case 'recaptcha_v3':
                    $captcha_provider = 'recaptcha';
                    break;
                case 'hcaptcha':
                    $captcha_provider = 'hcaptcha';
                    break;
                case 'turnstile':
                    $captcha_provider = 'turnstile';
                    break;
            }
        }

        // Get recovery code count for the user
        $encrypted_codes = get_user_meta($user_id, self::USER_META_RECOVERY_CODES, true);
        $recovery_codes = $this->decode_recovery_codes($encrypted_codes);
        $recovery_count = count($recovery_codes);

        // Check for error from state token
        $error_message = '';
        if ($state_error === 'invalid_code' || $state_error === self::ERROR_CODE_INVALID) {
            $error_message = '<strong>' . esc_html__('ERROR', 'guard-dog') . ':</strong> ' . esc_html__('The verification code is incorrect. Please try again.', 'guard-dog');
        } elseif ($state_error === self::ERROR_CODE_RATE_LIMITED) {
            $error_message = '<strong>' . esc_html__('ERROR', 'guard-dog') . ':</strong> ' . esc_html__('Too many failed attempts. Please try again later.', 'guard-dog');
        } elseif ($state_error === self::ERROR_CODE_INVALID_SESSION) {
            $error_message = '<strong>' . esc_html__('ERROR', 'guard-dog') . ':</strong> ' . esc_html__('Your verification session is invalid. Please sign in again.', 'guard-dog');
        }

        $error = null;
        if (!empty($error_message)) {
            $error = new WP_Error('guard_dog_2fa_error', $error_message);
        }

        $GLOBALS['guard_dog_login_screen_state'] = 'two_factor';

        // Call login_header() to get proper WordPress login page structure
        login_header(__('Two-Factor Authentication', 'guard-dog'), '', $error);
        
        // Render the login form partial
        Guard_Dog_View_Loader::render_partial('login-2fa-form', [
            'user_id' => $user_id,
            'username' => $username,
            'redirect_to' => $redirect_to,
            'rememberme' => $rememberme,
            'flow_token' => $state_flow_token,
            'captcha_token' => $captcha_token,
            'captcha_provider' => $captcha_provider,
            'this' => $this
        ]);

        login_footer();
        exit;
    }

    //--------------------------------------------------------------------------
    // 2FA Setup Interstitial (for enforcement when grace period expires)
    //--------------------------------------------------------------------------

    /**
     * Check if user needs to set up 2FA to continue login.
     *
     * Returns true when:
     * - 2FA enforcement is enabled
     * - User has no 2FA method configured
     * - Grace period has expired
     *
     * @since 1.9.1
     * @param WP_User|int $user The user object or user ID.
     * @return bool True if 2FA setup is required.
     */
    public function should_require_2fa_setup($user) {
        // Get user ID
        if ($user instanceof WP_User) {
            $user_id = $user->ID;
        } else {
            $user_id = absint($user);
        }

        if ($user_id <= 0) {
            return false;
        }

        // Check if 2FA enforcement is enabled
        $tfa_enforced = get_option('guard_dog_two_factor_enforced', false);
        if (!$tfa_enforced) {
            return false;
        }

        // Check if user already has 2FA configured
        if ($this->user_has_any_2fa_enabled($user_id)) {
            return false;
        }

        // Check if grace period has expired
        $enforcement_date = get_option('guard_dog_two_factor_enforcement_date', '');
        $grace_period = get_option('guard_dog_two_factor_grace_period', 7);

        if (empty($enforcement_date)) {
            // Enforcement date not set - shouldn't require setup yet
            return false;
        }

        $enforcement_timestamp = intval($enforcement_date) + ($grace_period * DAY_IN_SECONDS);
        $current_time = time();

        // Grace period expired if current time is past enforcement timestamp
        return ($current_time >= $enforcement_timestamp);
    }

    /**
     * Get available 2FA methods for the setup interstitial.
     *
     * @since 1.9.1
     * @return array List of available methods with their configurations.
     */
    public function get_available_2fa_methods_for_setup() {
        $methods = [];

        // TOTP (always available if 2FA is globally enabled)
        if (get_option('guard_dog_two_factor_enabled', false)) {
            $methods['totp'] = [
                'name'        => __('Authenticator App', 'guard-dog'),
                'description' => __('Use an authenticator app like Google Authenticator, Authy, or Microsoft Authenticator. Codes change every 30 seconds.', 'guard-dog'),
                'icon'        => 'dashicons-smartphone',
            ];
        }

        // Email 2FA (if globally enabled and email provider configured)
        $email_2fa_enabled = get_option('guard_dog_email_2fa_enabled', false);
        if ($email_2fa_enabled && function_exists('Guard_Dog') && isset(Guard_Dog()->features['email_two_factor_auth'])) {
            $email_2fa = Guard_Dog()->features['email_two_factor_auth'];
            if ($email_2fa->is_email_provider_configured()) {
                $methods['email'] = [
                    'name'        => __('Email Verification', 'guard-dog'),
                    'description' => __('Receive a 6-digit verification code via email when you log in.', 'guard-dog'),
                    'icon'        => 'dashicons-email-alt',
                ];
            }
        }

        // Passkeys (only if enabled AND bypass_2fa is true - meaning passkeys satisfy 2FA)
        $passkeys_enabled = get_option('guard_dog_passkeys_enabled', false);
        $passkeys_bypass_2fa = get_option('guard_dog_passkeys_bypass_2fa', true);

        if ($passkeys_enabled && $passkeys_bypass_2fa) {
            $methods['passkey'] = [
                'name'        => __('Passkey', 'guard-dog'),
                'description' => __('Use biometrics (Face ID, Touch ID, fingerprint) or a security key for passwordless authentication.', 'guard-dog'),
                'icon'        => 'dashicons-admin-network',
            ];
        }

        return $methods;
    }

    /**
     * Redirect to 2FA setup interstitial.
     *
     * Stores user info in a transient and sets a session flag,
     * then redirects to the login page which will render the setup interstitial.
     *
     * @since 1.9.1
     * @param WP_User $user        The user object.
     * @param string  $redirect_to Where to redirect after setup.
     */
    public function redirect_to_2fa_setup($user, $redirect_to = '') {
        if (empty($redirect_to)) {
            $redirect_to = admin_url();
        } else {
            $validated = wp_validate_redirect($redirect_to, admin_url());
            $redirect_to = ($validated !== false && $validated !== '') ? $validated : admin_url();
        }

        // Store user info in transient for setup flow (30 minute expiration)
        $transient_key = 'guard_dog_2fa_setup_' . hash('sha256', $user->user_login);
        set_transient($transient_key, [
            'user_id'     => $user->ID,
            'username'    => $user->user_login,
            'redirect_to' => $redirect_to,
            'timestamp'   => time(),
        ], 1800);

        // Redirect to login page with state token (setup interstitial will render)
        $redirect_url = Guard_Dog_Utils::create_login_state_url(
            Guard_Dog_Utils::get_login_base_url(),
            array(
                'type'     => '2fa_setup',
                'username' => $user->user_login,
            )
        );

        wp_safe_redirect($redirect_url);
        exit;
    }

    /**
     * Render the 2FA setup interstitial if needed.
     *
     * Called on login_init at priority 3 (before 2FA form rendering).
     *
     * @since 1.9.1
     */
    public function maybe_render_2fa_setup_interstitial() {
        // Check for state-token-based setup flow
        $login_state = Guard_Dog_Utils::consume_login_state();
        $state_username = null;
        $state_error    = null;

        if ( is_array( $login_state ) && isset( $login_state['type'] ) && $login_state['type'] === '2fa_setup' ) {
            $state_username = isset( $login_state['username'] ) ? sanitize_user( $login_state['username'] ) : null;
            $state_error    = isset( $login_state['error'] ) ? sanitize_text_field( $login_state['error'] ) : null;
        }

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

        // Get transient data
        $transient_key = 'guard_dog_2fa_setup_' . hash('sha256', $state_username);
        $transient_data = get_transient($transient_key);

        if (false === $transient_data) {
            // Transient expired - redirect to login
            wp_safe_redirect(wp_login_url());
            exit;
        }

        // Get user
        $user = get_user_by('ID', $transient_data['user_id']);
        if (!$user) {
            delete_transient($transient_key);
            wp_safe_redirect(wp_login_url());
            exit;
        }

        // Verify user still needs setup (hasn't completed it via another path)
        if ($this->user_has_any_2fa_enabled($user)) {
            // User already has 2FA - complete login
            delete_transient($transient_key);

            wp_set_auth_cookie($user->ID, false);
            wp_safe_redirect($transient_data['redirect_to'] ?? admin_url());
            exit;
        }

        // Get available 2FA methods
        $available_methods = $this->get_available_2fa_methods_for_setup();

        if (empty($available_methods)) {
            // No 2FA methods available - this is a configuration error
            // Allow login but show admin notice
            wp_set_auth_cookie($user->ID, false);
            set_transient('guard_dog_2fa_config_error_' . $user->ID, true, 3600);
            wp_safe_redirect($transient_data['redirect_to'] ?? admin_url());
            exit;
        }

        // Set up WordPress login page environment
        if (!function_exists('login_header')) {
            require_once ABSPATH . 'wp-login.php';
        }

        global $error, $interim_login, $action;
        $error = null;
        $interim_login = false;
        $action = 'guard_dog_2fa_setup';

        // Enqueue scripts
        $this->enqueue_2fa_setup_scripts($user);

        $GLOBALS['guard_dog_login_screen_state'] = 'two_factor_setup';

        // Render the setup interstitial
        login_header(__('Two-Factor Authentication Setup Required', 'guard-dog'), '', null);

        Guard_Dog_View_Loader::render_partial('login-2fa-setup-interstitial', [
            'user_id'           => $user->ID,
            'username'          => $user->user_login,
            'available_methods' => $available_methods,
            'redirect_to'       => $transient_data['redirect_to'] ?? admin_url(),
            'transient_key'     => $transient_key,
            'setup_error'       => $state_error,
            'this'              => $this,
        ]);

        login_footer();
        exit;
    }

    /**
     * Handle 2FA setup completion during login flow.
     *
     * Called on login_init at priority 4.
     * Processes AJAX-like form submissions for completing 2FA setup.
     *
     * @since 1.9.1
     */
    public function handle_2fa_setup_completion() {
        // phpcs:disable WordPress.Security.NonceVerification.Missing
        // Nonce verification happens within the specific handlers below
        // This method just routes to the appropriate handler
        if (!isset($_POST['guard_dog_2fa_setup_action'])) {
            return;
        }

        $action = sanitize_text_field(wp_unslash($_POST['guard_dog_2fa_setup_action']));
        // phpcs:enable WordPress.Security.NonceVerification.Missing

        // Route to appropriate handler based on action
        switch ($action) {
            case 'complete_totp':
                $this->handle_setup_totp_completion();
                break;
            case 'complete_email':
                $this->handle_setup_email_completion();
                break;
            case 'complete_passkey':
                // Passkey completion is handled via AJAX, not form submission
                break;
        }
    }

    /**
     * Redirect to 2FA setup interstitial with an error message via state token.
     *
     * @since 1.9.20
     * @param string $login_url     Base login URL.
     * @param string $error_message Error message to display.
     * @param string $username      Username for the setup flow.
     */
    private function redirect_with_setup_error( $login_url, $error_message, $username ) {
        $redirect_url = Guard_Dog_Utils::create_login_state_url(
            $login_url,
            array(
                'type'     => '2fa_setup',
                'username' => $username,
                'error'    => $error_message,
            ),
            60
        );
        wp_safe_redirect( $redirect_url );
        exit;
    }

    /**
     * Handle TOTP setup completion during login flow.
     *
     * @since 1.9.1
     */
    private function handle_setup_totp_completion() {
        $login_url = Guard_Dog_Utils::get_login_base_url();

        // phpcs:disable WordPress.Security.NonceVerification.Missing
        // Nonce verified below with wp_verify_nonce
        if (!isset($_POST['guard_dog_2fa_setup_nonce']) || !isset($_POST['user_id'])) {
            wp_safe_redirect($login_url);
            exit;
        }

        $user_id = absint($_POST['user_id']);
        $nonce = sanitize_text_field(wp_unslash($_POST['guard_dog_2fa_setup_nonce']));
        // phpcs:enable WordPress.Security.NonceVerification.Missing

        if (!wp_verify_nonce($nonce, 'guard_dog_2fa_setup_' . $user_id)) {
            wp_safe_redirect($login_url);
            exit;
        }

        // Get user data with null check
        $user_data = get_userdata($user_id);
        if (!$user_data) {
            wp_safe_redirect($login_url);
            exit;
        }

        // Check rate limiting before processing
        if (!$this->check_setup_rate_limit($user_id, 'totp_verify')) {
            $this->redirect_with_setup_error(
                $login_url,
                __('Too many verification attempts. Please wait a few minutes before trying again.', 'guard-dog'),
                $user_data->user_login
            );
        }

        // phpcs:disable WordPress.Security.NonceVerification.Missing
        $code = isset($_POST['guard_dog_2fa_setup_code'])
            ? sanitize_text_field(wp_unslash($_POST['guard_dog_2fa_setup_code']))
            : '';
        // phpcs:enable WordPress.Security.NonceVerification.Missing

        // Get secret from server-side transient (not from form field for security)
        $transient_key = 'guard_dog_2fa_setup_' . hash('sha256', $user_data->user_login);
        $transient_data = get_transient($transient_key);
        $secret = isset($transient_data['totp_secret']) ? $transient_data['totp_secret'] : '';

        $redirect_to = (isset($transient_data['redirect_to']) && is_string($transient_data['redirect_to']))
            ? wp_validate_redirect($transient_data['redirect_to'], admin_url())
            : admin_url();
        $redirect_to = ($redirect_to !== false && $redirect_to !== '') ? $redirect_to : admin_url();

        if (empty($code) || empty($secret)) {
            $this->increment_setup_rate_limit($user_id, 'totp_verify');
            $this->redirect_with_setup_error(
                $login_url,
                __('Please enter the verification code. You may need to regenerate the QR code.', 'guard-dog'),
                $user_data->user_login
            );
        }

        // Verify the code
        $google2fa = new Google2FA();
        $valid = $google2fa->verifyKey($secret, $code, 1);

        if (!$valid) {
            $this->increment_setup_rate_limit($user_id, 'totp_verify');
            $this->redirect_with_setup_error(
                $login_url,
                __('Invalid verification code. Please try again.', 'guard-dog'),
                $user_data->user_login
            );
        }

        // Clear rate limit on successful verification
        $this->clear_setup_rate_limit($user_id, 'totp_verify');
        $this->clear_setup_rate_limit($user_id, 'totp_generate');

        // Save 2FA settings for user
        update_user_meta($user_id, self::USER_META_SECRET_KEY, $secret);
        update_user_meta($user_id, self::USER_META_ENABLED_KEY, '1');

        // Generate recovery codes
        $recovery_codes = $this->generate_recovery_codes();
        $this->store_recovery_codes($user_id, $recovery_codes);

        // Clean up setup transient
        delete_transient($transient_key);

        // Log the setup
        $this->activity_log->add_log_entry('user_2fa_setup_completed', [
            'user_id'  => $user_id,
            'method'   => 'totp',
            'context'  => 'enforcement_interstitial',
        ]);

        // Complete login
        wp_set_auth_cookie($user_id, false);

        // Store recovery codes for display after login
        set_transient('guard_dog_show_recovery_codes_' . $user_id, $recovery_codes, 300);

        wp_safe_redirect($redirect_to);
        exit;
    }

    /**
     * Handle Email 2FA setup completion during login flow.
     *
     * @since 1.9.1
     */
    private function handle_setup_email_completion() {
        $login_url = Guard_Dog_Utils::get_login_base_url();

        // phpcs:disable WordPress.Security.NonceVerification.Missing
        if (!isset($_POST['guard_dog_2fa_setup_nonce']) || !isset($_POST['user_id'])) {
            wp_safe_redirect($login_url);
            exit;
        }

        $user_id = absint($_POST['user_id']);
        $nonce = sanitize_text_field(wp_unslash($_POST['guard_dog_2fa_setup_nonce']));
        // phpcs:enable WordPress.Security.NonceVerification.Missing

        if (!wp_verify_nonce($nonce, 'guard_dog_2fa_setup_' . $user_id)) {
            wp_safe_redirect($login_url);
            exit;
        }

        // Get user data with null check
        $user_data = get_userdata($user_id);
        if (!$user_data) {
            wp_safe_redirect($login_url);
            exit;
        }

        // Check rate limiting before processing
        if (!$this->check_setup_rate_limit($user_id, 'email_verify')) {
            $this->redirect_with_setup_error(
                $login_url,
                __('Too many verification attempts. Please wait a few minutes before trying again.', 'guard-dog'),
                $user_data->user_login
            );
        }

        $transient_key = 'guard_dog_2fa_setup_' . hash('sha256', $user_data->user_login);
        $transient_data = get_transient($transient_key);
        if (false === $transient_data || !is_array($transient_data)) {
            $this->redirect_with_setup_error(
                $login_url,
                __('Invalid or expired session. Please start over.', 'guard-dog'),
                $user_data->user_login
            );
        }
        $redirect_to = (isset($transient_data['redirect_to']) && is_string($transient_data['redirect_to']))
            ? wp_validate_redirect($transient_data['redirect_to'], admin_url())
            : admin_url();
        $redirect_to = ($redirect_to !== false && $redirect_to !== '') ? $redirect_to : admin_url();

        // phpcs:disable WordPress.Security.NonceVerification.Missing
        $code = isset($_POST['guard_dog_2fa_setup_code'])
            ? sanitize_text_field(wp_unslash($_POST['guard_dog_2fa_setup_code']))
            : '';
        // phpcs:enable WordPress.Security.NonceVerification.Missing

        if (empty($code)) {
            $this->increment_setup_rate_limit($user_id, 'email_verify');
            $this->redirect_with_setup_error(
                $login_url,
                __('Please enter the verification code.', 'guard-dog'),
                $user_data->user_login
            );
        }

        // Verify with email 2FA feature
        if (!function_exists('Guard_Dog') || !isset(Guard_Dog()->features['email_two_factor_auth'])) {
            $this->redirect_with_setup_error(
                $login_url,
                __('Email 2FA is not available.', 'guard-dog'),
                $user_data->user_login
            );
        }

        $email_2fa = Guard_Dog()->features['email_two_factor_auth'];
        $verified = $email_2fa->verify_email_code($user_id, $code);

        if (!$verified) {
            $this->increment_setup_rate_limit($user_id, 'email_verify');
            $this->redirect_with_setup_error(
                $login_url,
                __('Invalid or expired verification code. Please try again.', 'guard-dog'),
                $user_data->user_login
            );
        }

        // Clear rate limit on successful verification
        $this->clear_setup_rate_limit($user_id, 'email_verify');

        // Enable email 2FA for user
        $email_2fa->enable_for_user($user_id);

        // Clean up setup transient
        delete_transient($transient_key);

        // Log the setup
        $this->activity_log->add_log_entry('user_2fa_setup_completed', [
            'user_id'  => $user_id,
            'method'   => 'email',
            'context'  => 'enforcement_interstitial',
        ]);

        // Complete login
        wp_set_auth_cookie($user_id, false);
        wp_safe_redirect($redirect_to);
        exit;
    }

    /**
     * Enqueue scripts for the 2FA setup interstitial.
     *
     * @since 1.9.1
     */
    private function enqueue_2fa_setup_scripts( WP_User $user ) {
        // Enqueue dashicons for icons
        wp_enqueue_style('dashicons');

        // Enqueue setup interstitial CSS
        wp_enqueue_style(
            'guard-dog-2fa-setup-interstitial',
            GUARD_DOG_URL . 'assets/css/guard-dog-2fa-setup-interstitial.css',
            [],
            GUARD_DOG_VERSION
        );

        // Enqueue setup interstitial JS
        wp_enqueue_script(
            'guard-dog-2fa-setup-interstitial',
            GUARD_DOG_URL . 'assets/js/guard-dog-2fa-setup-interstitial.js',
            ['jquery'],
            GUARD_DOG_VERSION,
            true
        );

        // Localize script
        wp_localize_script('guard-dog-2fa-setup-interstitial', 'guardDog2FASetup', [
            'ajaxUrl'    => admin_url('admin-ajax.php'),
            'nonce'      => wp_create_nonce('guard_dog_2fa_setup_' . $user->ID),
            'i18n'       => [
                'loading'           => __('Loading...', 'guard-dog'),
                'error'             => __('An error occurred. Please try again.', 'guard-dog'),
                'qrUnavailable'     => __('QR code unavailable. Use the secret key below to complete setup in your authenticator app.', 'guard-dog'),
                'codeSent'          => __('Verification code sent to your email.', 'guard-dog'),
                'verifying'         => __('Verifying...', 'guard-dog'),
                'invalidCode'       => __('Invalid code. Please try again.', 'guard-dog'),
                'setupComplete'     => __('Setup complete! Redirecting...', 'guard-dog'),
                'scanQrCode'        => __('Scan this QR code with your authenticator app', 'guard-dog'),
                'manualEntry'       => __('Or enter this code manually:', 'guard-dog'),
                'passkeyPrompt'     => __('Please follow the prompts to register your passkey.', 'guard-dog'),
                'passkeyError'      => __('Passkey registration failed. Please try again.', 'guard-dog'),
                'passkeySuccess'    => __('Passkey registered successfully!', 'guard-dog'),
                'passkeyNotAllowed' => __('Registration was cancelled or not allowed.', 'guard-dog'),
                'passkeyExists'     => __('A passkey for this account already exists on this device.', 'guard-dog'),
                'tooManyAttempts'   => __('Too many failed attempts. Please try again later.', 'guard-dog'),
            ],
        ]);

        // Also enqueue passkey scripts if passkeys are available
        $passkeys_enabled = get_option('guard_dog_passkeys_enabled', false);
        $passkeys_bypass_2fa = get_option('guard_dog_passkeys_bypass_2fa', true);

        if ($passkeys_enabled && $passkeys_bypass_2fa && function_exists('Guard_Dog') && isset(Guard_Dog()->features['passkeys'])) {
            Guard_Dog()->features['passkeys']->enqueue_login_scripts();
        }
    }

    //--------------------------------------------------------------------------
    // AJAX Handlers for Setup Interstitial (Unauthenticated Users)
    //--------------------------------------------------------------------------

    /**
     * Validate setup interstitial request via transient.
     *
     * Since the user is not authenticated during the setup interstitial,
     * we validate them via the transient created during the login flow.
     *
     * @since 1.9.1
     * @param int $user_id The user ID from the request.
     * @return array|false Array with user data if valid, false otherwise.
     */
    private function validate_setup_request($user_id) {
        $user_id = absint($user_id);
        if ($user_id <= 0) {
            return false;
        }

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

        // Check for valid setup transient
        $transient_key = 'guard_dog_2fa_setup_' . hash('sha256', $user->user_login);
        $transient_data = get_transient($transient_key);

        if (false === $transient_data || !isset($transient_data['user_id'])) {
            return false;
        }

        // Verify user ID matches transient
        if (absint($transient_data['user_id']) !== $user_id) {
            return false;
        }

        return [
            'user'          => $user,
            'transient_key' => $transient_key,
            'transient_data' => $transient_data,
        ];
    }

    /**
     * Check rate limiting for setup attempts.
     *
     * @since 1.9.1
     * @param int    $user_id      The user ID.
     * @param string $action       The action type (totp, email, passkey).
     * @param int    $max_attempts Maximum attempts allowed in the window. Default 5.
     * @return bool True if within rate limit, false if exceeded.
     */
    private function check_setup_rate_limit($user_id, $action, $max_attempts = 5) {
        $rate_key = 'guard_dog_2fa_setup_attempts_' . $user_id . '_' . $action;
        $attempts = get_transient($rate_key);

        if ($attempts === false) {
            $attempts = 0;
        }

        if ($attempts >= $max_attempts) {
            return false;
        }

        return true;
    }

    /**
     * Increment rate limit counter for setup attempts.
     *
     * @since 1.9.1
     * @param int    $user_id       The user ID.
     * @param string $action        The action type.
     * @param int    $window_seconds Window in seconds for the transient. Default 300 (5 minutes).
     */
    private function increment_setup_rate_limit($user_id, $action, $window_seconds = 300) {
        $rate_key = 'guard_dog_2fa_setup_attempts_' . $user_id . '_' . $action;
        $attempts = get_transient($rate_key);

        if ($attempts === false) {
            $attempts = 0;
        }

        set_transient($rate_key, $attempts + 1, $window_seconds);
    }

    /**
     * Clear rate limit counter after successful setup.
     *
     * @since 1.9.1
     * @param int    $user_id The user ID.
     * @param string $action  The action type.
     */
    private function clear_setup_rate_limit($user_id, $action) {
        $rate_key = 'guard_dog_2fa_setup_attempts_' . $user_id . '_' . $action;
        delete_transient($rate_key);
    }

    /**
     * AJAX handler: Generate 2FA secret during setup interstitial.
     *
     * For unauthenticated users during enforcement setup flow.
     *
     * @since 1.9.1
     */
    public function ajax_setup_generate_secret() {
        $user_id = isset($_POST['user_id']) ? absint($_POST['user_id']) : 0;
        if ($user_id <= 0 || !isset($_POST['security']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['security'])), 'guard_dog_2fa_setup_' . $user_id)) {
            wp_send_json_error(__('Security check failed.', 'guard-dog'), 403);
        }

        $validation = $this->validate_setup_request($user_id);

        if (!$validation) {
            wp_send_json_error(__('Invalid or expired session. Please start over.', 'guard-dog'), 403);
        }

        if (!$this->check_setup_rate_limit($user_id, 'totp_generate')) {
            wp_send_json_error(__('Too many requests. Please wait a few minutes.', 'guard-dog'), 429);
        }
        $this->increment_setup_rate_limit($user_id, 'totp_generate');

        $user = $validation['user'];
        $transient_key = $validation['transient_key'];
        $transient_data = $validation['transient_data'];

        // Generate secret
        $google2fa = new Google2FA();
        $secret = $google2fa->generateSecretKey();

        // Store secret in transient (server-side) instead of sending to client
        $transient_data['totp_secret'] = $secret;
        $transient_data['totp_generated_at'] = time();
        set_transient($transient_key, $transient_data, 1800); // 30 minutes

        // Generate QR code locally to avoid disclosing the TOTP seed to third parties.
        $site_name = get_bloginfo('name');
        $site_name = preg_replace('/[^a-zA-Z0-9]/', '', $site_name); // Clean for QR code
        $qr_url = $google2fa->getQRCodeUrl($site_name, $user->user_email, $secret);

        $qr_code_data_uri = '';
        try {
            $renderer = new ImageRenderer(
                new RendererStyle(200),
                new SvgImageBackEnd()
            );
            $writer = new Writer($renderer);
            $svg_data = $writer->writeString($qr_url);
            $qr_code_data_uri = 'data:image/svg+xml;base64,' . base64_encode($svg_data);
        } catch (\Throwable $qr_exception) {
            if (function_exists('Guard_Dog') && Guard_Dog()->debug) {
                Guard_Dog()->debug->error('2FA setup interstitial QR generation failed', 'two_factor', [
                    'user_id' => $user_id,
                    'error_message' => $qr_exception->getMessage(),
                ]);
            }
        }

        wp_send_json_success([
            'qr_code' => $qr_code_data_uri,
            'secret'  => $secret, // Still send for manual entry display
        ]);
    }

    /**
     * AJAX handler: Send email 2FA code during setup interstitial.
     *
     * @since 1.9.1
     */
    public function ajax_setup_send_email_code() {
        $user_id = isset($_POST['user_id']) ? absint($_POST['user_id']) : 0;
        if ($user_id <= 0 || !isset($_POST['security']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['security'])), 'guard_dog_2fa_setup_' . $user_id)) {
            wp_send_json_error(__('Security check failed.', 'guard-dog'), 403);
        }

        $validation = $this->validate_setup_request($user_id);

        if (!$validation) {
            wp_send_json_error(__('Invalid or expired session. Please start over.', 'guard-dog'), 403);
        }

        // Check rate limiting (failures: 5 per 5 min; successful sends: 3 per 15 min)
        if (!$this->check_setup_rate_limit($user_id, 'email_send')) {
            wp_send_json_error(__('Too many requests. Please wait a few minutes.', 'guard-dog'), 429);
        }
        if (!$this->check_setup_rate_limit($user_id, 'email_send_ok', 3)) {
            wp_send_json_error(__('Too many code requests. Please try again in 15 minutes.', 'guard-dog'), 429);
        }

        $user = $validation['user'];

        // Check if email 2FA is available
        if (!function_exists('Guard_Dog') || !isset(Guard_Dog()->features['email_two_factor_auth'])) {
            wp_send_json_error(__('Email 2FA is not available.', 'guard-dog'), 400);
        }

        $email_2fa = Guard_Dog()->features['email_two_factor_auth'];

        // Generate and send code
        $result = $email_2fa->send_verification_code($user_id);

        if (is_wp_error($result)) {
            $this->increment_setup_rate_limit($user_id, 'email_send');
            wp_send_json_error($result->get_error_message(), 500);
        }

        $this->increment_setup_rate_limit($user_id, 'email_send_ok', 900);
        wp_send_json_success(__('Verification code sent to your email.', 'guard-dog'));
    }

    /**
     * AJAX handler: Get passkey registration options during setup interstitial.
     *
     * @since 1.9.1
     */
    public function ajax_setup_passkey_options() {
        $user_id = isset($_POST['user_id']) ? absint($_POST['user_id']) : 0;
        if ($user_id <= 0 || !isset($_POST['security']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['security'])), 'guard_dog_2fa_setup_' . $user_id)) {
            wp_send_json_error(__('Security check failed.', 'guard-dog'), 403);
        }

        $validation = $this->validate_setup_request($user_id);

        if (!$validation) {
            wp_send_json_error(__('Invalid or expired session. Please start over.', 'guard-dog'), 403);
        }

        // Check if passkeys are available
        if (!function_exists('Guard_Dog') || !isset(Guard_Dog()->features['passkeys'])) {
            wp_send_json_error(__('Passkeys are not available.', 'guard-dog'), 400);
        }

        $passkeys = Guard_Dog()->features['passkeys'];
        $user = $validation['user'];

        // Get registration options (this is a public method in the passkeys class)
        $options = $passkeys->get_registration_options($user);

        if (is_wp_error($options)) {
            wp_send_json_error($options->get_error_message(), 500);
        }

        wp_send_json_success($options);
    }

    /**
     * AJAX handler: Verify passkey registration during setup interstitial.
     *
     * @since 1.9.1
     */
    public function ajax_setup_passkey_verify() {
        $user_id = isset($_POST['user_id']) ? absint($_POST['user_id']) : 0;
        if ($user_id <= 0 || !isset($_POST['security']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['security'])), 'guard_dog_2fa_setup_' . $user_id)) {
            wp_send_json_error(__('Security check failed.', 'guard-dog'), 403);
        }

        $validation = $this->validate_setup_request($user_id);

        if (!$validation) {
            wp_send_json_error(__('Invalid or expired session. Please start over.', 'guard-dog'), 403);
        }

        // Check rate limiting
        if (!$this->check_setup_rate_limit($user_id, 'passkey')) {
            wp_send_json_error(__('Too many failed attempts. Please wait a few minutes.', 'guard-dog'), 429);
        }

        // Check if passkeys are available
        if (!function_exists('Guard_Dog') || !isset(Guard_Dog()->features['passkeys'])) {
            wp_send_json_error(__('Passkeys are not available.', 'guard-dog'), 400);
        }

        $passkeys = Guard_Dog()->features['passkeys'];
        $user = $validation['user'];
        $transient_key = $validation['transient_key'];

        // Get credential data from request (opaque JSON; do not use sanitize_text_field on it)
        $credential = (isset($_POST['credential']) && is_string($_POST['credential'])) ? wp_unslash($_POST['credential']) : '';
        if ('' === $credential || json_decode($credential) === null) {
            $this->increment_setup_rate_limit($user_id, 'passkey');
            wp_send_json_error(__('Invalid credential data.', 'guard-dog'), 400);
        }
        $friendly_name = isset($_POST['friendly_name']) ? sanitize_text_field(wp_unslash($_POST['friendly_name'])) : '';

        // Verify and register the passkey
        $result = $passkeys->verify_and_register_credential($user, $credential, $friendly_name);

        if (is_wp_error($result)) {
            $this->increment_setup_rate_limit($user_id, 'passkey');
            wp_send_json_error($result->get_error_message(), 400);
        }

        // Clear rate limit on success
        $this->clear_setup_rate_limit($user_id, 'passkey');

        // Clean up transient
        delete_transient($transient_key);

        // Log the setup
        $this->activity_log->add_log_entry('user_2fa_setup_completed', [
            'user_id' => $user_id,
            'method'  => 'passkey',
            'context' => 'enforcement_interstitial',
        ]);

        // Set auth cookie and redirect
        wp_set_auth_cookie($user_id, false);

        wp_send_json_success(__('Passkey registered successfully!', 'guard-dog'));
    }

    /**
     * Validates a 2FA code (TOTP, recovery, or email) for a given user.
     *
     * @since 1.8.0
     * @param WP_User $user The user object.
     * @param array $post_data The submitted POST data (e.g., $_POST).
     * @return WP_User|WP_Error The user object on success, WP_Error on failure.
     */
    private function validate_2fa_code($user, $post_data) {
        // Check if this is an email 2FA verification
        $method = isset($post_data['guard_dog_2fa_method']) ? sanitize_text_field($post_data['guard_dog_2fa_method']) : 'app';
        
        if ($method === 'email') {
            return $this->validate_email_2fa_code($user, $post_data);
        }
        
        // Continue with app-based 2FA validation
        // Debug logging
        if (isset($GLOBALS['guard_dog']) && $GLOBALS['guard_dog']->debug) {
            $GLOBALS['guard_dog']->debug->info('2FA validation started', 'two_factor', [
                'user_id' => $user->ID,
                'username' => $user->user_login,
                'post_data_keys' => array_keys($post_data),
                'has_code' => isset($post_data['guard_dog_2fa_code']),
                'has_nonce' => isset($post_data['guard_dog_2fa_nonce'])
            ]);
        }
        
        if (!isset($post_data['guard_dog_2fa_code']) || !isset($post_data['guard_dog_2fa_nonce'])) {
            if (isset($GLOBALS['guard_dog']) && $GLOBALS['guard_dog']->debug) {
                $GLOBALS['guard_dog']->debug->error('2FA validation failed: missing code or nonce', 'two_factor', [
                    'has_code' => isset($post_data['guard_dog_2fa_code']),
                    'has_nonce' => isset($post_data['guard_dog_2fa_nonce'])
                ]);
            }
            return new WP_Error(self::ERROR_CODE_REQUIRED, 'Verification code not provided.');
        }

        if (!wp_verify_nonce($post_data['guard_dog_2fa_nonce'], 'guard_dog_verify_2fa_login_' . $user->ID)) {
            if (isset($GLOBALS['guard_dog']) && $GLOBALS['guard_dog']->debug) {
                $GLOBALS['guard_dog']->debug->error('2FA validation failed: nonce verification failed', 'two_factor');
            }
            return new WP_Error('guard_dog_nonce_error', 'Security check failed.');
        }

        $submitted_code = sanitize_text_field($post_data['guard_dog_2fa_code']);
        $secret = get_user_meta($user->ID, self::USER_META_SECRET_KEY, true);
        
        if (isset($GLOBALS['guard_dog']) && $GLOBALS['guard_dog']->debug) {
            $GLOBALS['guard_dog']->debug->info('2FA validation details', 'two_factor', [
                'code_length' => strlen($submitted_code),
                'code_is_numeric' => ctype_digit($submitted_code),
                'has_secret' => !empty($secret)
            ]);
        }

        // --- Access Control Check (username and IP blacklist) ---
        if (class_exists('Guard_Dog_Access_Control')) {
            $access_control = function_exists('Guard_Dog') && isset(Guard_Dog()->features['access_control']) ? Guard_Dog()->features['access_control'] : null;
            if ($access_control) {
                $current_ip = Guard_Dog_Utils::get_client_ip();
                $attempted_username = strtolower($user->user_login);
                // Username Blacklist
                $user_blacklist_str = get_option(Guard_Dog_Access_Control::OPTION_USER_BLACKLIST, '');
                $user_blacklist = $access_control->parse_comma_list($user_blacklist_str);
                if (!empty($user_blacklist) && in_array($attempted_username, $user_blacklist)) {
                    return new WP_Error('access_denied', __('Access denied. This username has been blocked from logging in.', 'guard-dog'));
                }
                // IP Blacklist
                $ip_blacklist = $access_control->get_active_blacklist();
                if (!empty($ip_blacklist) && $access_control->is_ip_in_list($current_ip, $ip_blacklist)) {
                    return new WP_Error('access_denied', __('Access denied. Your IP address has been blocked from logging in.', 'guard-dog'));
                }
            }
        }

        // 1. Check standard TOTP code
        if (strlen($submitted_code) === 6 && ctype_digit($submitted_code)) {
            if (isset($GLOBALS['guard_dog']) && $GLOBALS['guard_dog']->debug) {
                $GLOBALS['guard_dog']->debug->info('Attempting TOTP verification', 'two_factor', [
                    'code_length' => strlen($submitted_code),
                    'has_secret' => !empty($secret)
                ]);
            }
            
            if ($secret && $this->google2fa->verifyKey($secret, $submitted_code)) {
                if (isset($GLOBALS['guard_dog']) && $GLOBALS['guard_dog']->debug) {
                    $GLOBALS['guard_dog']->debug->info('TOTP verification successful', 'two_factor');
                }
                
                if ($this->activity_log) {
                    $this->activity_log->add_log_entry('2fa_successful', array('user_id' => $user->ID, 'username' => $user->user_login, 'details' => array('method' => 'Authenticator App')));
                }
                return $user; // Success
            } else {
                if (isset($GLOBALS['guard_dog']) && $GLOBALS['guard_dog']->debug) {
                    $GLOBALS['guard_dog']->debug->error('TOTP verification failed', 'two_factor');
                }
            }
        } else {
            if (isset($GLOBALS['guard_dog']) && $GLOBALS['guard_dog']->debug) {
                $GLOBALS['guard_dog']->debug->info('Code does not match TOTP format, checking recovery codes', 'two_factor', [
                    'code_length' => strlen($submitted_code),
                    'is_numeric' => ctype_digit($submitted_code)
                ]);
            }
        }

        // 2. Check recovery codes
        $encrypted_codes = get_user_meta($user->ID, self::USER_META_RECOVERY_CODES, true);
        $recovery_codes = $this->decode_recovery_codes($encrypted_codes);
        if (!empty($recovery_codes)) {
            // Normalize the submitted code (remove dashes and convert to lowercase)
            $normalized_code = strtolower(str_replace('-', '', $submitted_code));
            $match_found = false;
            $updated_codes = [];

            foreach ($recovery_codes as $stored_code) {
                if (!$match_found && hash_equals(strtolower(str_replace('-', '', $stored_code)), $normalized_code)) {
                    $match_found = true; // Mark as found, but don't add to updated list
                } else {
                    $updated_codes[] = $stored_code;
                }
            }

            if ($match_found) {
                // Re-encrypt the remaining codes
                $encrypted_updated_codes = $this->encode_recovery_codes($updated_codes);
                update_user_meta($user->ID, self::USER_META_RECOVERY_CODES, $encrypted_updated_codes);
                
                // Log the successful recovery code usage
                if ($this->activity_log) {
                    $this->activity_log->add_log_entry('2fa_successful', [
                        'user_id' => $user->ID, 
                        'username' => $user->user_login, 
                        'details' => [
                            'method' => 'Recovery Code',
                            'remaining_codes' => count($updated_codes)
                        ]
                    ]);
                }
                
                return $user; // Success
            }
        }

        // 3. If all checks fail
        return new WP_Error(self::ERROR_CODE_INVALID, 'The verification code is incorrect.');
    }

    /**
     * Validates an email 2FA code for a given user.
     *
     * @since 1.8.0
     * @param WP_User $user The user object.
     * @param array $post_data The submitted POST data.
     * @return WP_User|WP_Error The user object on success, WP_Error on failure.
     */
    private function validate_email_2fa_code($user, $post_data) {
        // Debug logging
        if (isset($GLOBALS['guard_dog']) && $GLOBALS['guard_dog']->debug) {
            $GLOBALS['guard_dog']->debug->info('Email 2FA validation started', 'two_factor', [
                'user_id' => $user->ID,
                'username' => $user->user_login,
                'post_data_keys' => array_keys($post_data),
                'has_code' => isset($post_data['guard_dog_2fa_code']),
                'has_nonce' => isset($post_data['guard_dog_2fa_nonce'])
            ]);
        }
        
        if (!isset($post_data['guard_dog_2fa_code']) || !isset($post_data['guard_dog_2fa_nonce'])) {
            if (isset($GLOBALS['guard_dog']) && $GLOBALS['guard_dog']->debug) {
                $GLOBALS['guard_dog']->debug->error('Email 2FA validation failed: missing code or nonce', 'two_factor', [
                    'has_code' => isset($post_data['guard_dog_2fa_code']),
                    'has_nonce' => isset($post_data['guard_dog_2fa_nonce'])
                ]);
            }
            return new WP_Error(self::ERROR_CODE_REQUIRED, 'Email verification code not provided.');
        }

        if (!wp_verify_nonce($post_data['guard_dog_2fa_nonce'], 'guard_dog_verify_2fa_login_' . $user->ID)) {
            if (isset($GLOBALS['guard_dog']) && $GLOBALS['guard_dog']->debug) {
                $GLOBALS['guard_dog']->debug->error('Email 2FA validation failed: nonce verification failed', 'two_factor');
            }
            return new WP_Error('guard_dog_nonce_error', 'Security check failed.');
        }

        $submitted_code = sanitize_text_field($post_data['guard_dog_2fa_code']);
        
        if (isset($GLOBALS['guard_dog']) && $GLOBALS['guard_dog']->debug) {
            $GLOBALS['guard_dog']->debug->info('Email 2FA validation details', 'two_factor', [
                'code_length' => strlen($submitted_code),
                'code_is_numeric' => ctype_digit($submitted_code)
            ]);
        }

        // Validate code format
        if (empty($submitted_code) || !ctype_digit($submitted_code) || strlen($submitted_code) !== 6) {
            return new WP_Error(self::ERROR_CODE_INVALID, 'Please enter a valid 6-digit email verification code.');
        }

        // Check if email 2FA is enabled for this user
        $email_2fa_enabled = get_option('guard_dog_email_2fa_enabled', false);
        if (!$email_2fa_enabled) {
            return new WP_Error('email_2fa_disabled', 'Email two-factor authentication is not enabled.');
        }

        if (!function_exists('Guard_Dog') || !isset(Guard_Dog()->features['email_two_factor_auth'])) {
            return new WP_Error('email_2fa_unavailable', 'Email two-factor authentication is not available.');
        }

        $email_2fa_instance = Guard_Dog()->features['email_two_factor_auth'];
        
        if (!$email_2fa_instance->is_email_2fa_enabled_for_user($user)) {
            return new WP_Error('email_2fa_not_enabled_for_user', 'Email two-factor authentication is not enabled for this user.');
        }

        // Verify the email code
        $result = $email_2fa_instance->verify_code($user->ID, $submitted_code);
        
        if (is_array($result) && isset($result['success'])) {
            if ($result['success']) {
                if (isset($GLOBALS['guard_dog']) && $GLOBALS['guard_dog']->debug) {
                    $GLOBALS['guard_dog']->debug->info('Email 2FA verification successful', 'two_factor');
                }
                
                if ($this->activity_log) {
                    $this->activity_log->add_log_entry('2fa_successful', [
                        'user_id' => $user->ID, 
                        'username' => $user->user_login, 
                        'details' => array('method' => 'Email Code')
                    ]);
                }
                
                return $user; // Success
            } else {
                if (isset($GLOBALS['guard_dog']) && $GLOBALS['guard_dog']->debug) {
                    $GLOBALS['guard_dog']->debug->error('Email 2FA verification failed', 'two_factor', [
                        'message' => $result['message']
                    ]);
                }
                return new WP_Error(self::ERROR_CODE_INVALID, $result['message']);
            }
        } else {
            // Fallback error
            return new WP_Error(self::ERROR_CODE_INVALID, 'Email verification code is incorrect.');
        }
    }

    //--------------------------------------------------------------------------
    // Helper methods
    //--------------------------------------------------------------------------

    /**
     * Get the transient key for a pending 2FA login.
     *
     * @since 1.9.41
     * @param string $username Username for the pending login.
     * @return string Transient key.
     */
    private function get_2fa_transient_key( $username ) {
        return 'guard_dog_2fa_user_' . hash( 'sha256', (string) $username );
    }

    /**
     * Create a state-token URL for the pending 2FA login form.
     *
     * @since 1.9.41
     * @param string $username Username for the pending login.
     * @param string $flow_token Random token bound to the pending login transient.
     * @param string $error Optional error code to display.
     * @return string Login URL containing an opaque state token.
     */
    private function create_2fa_state_url( $username, $flow_token, $error = '' ) {
        $state = array(
            'type'       => '2fa',
            'username'   => sanitize_user( $username ),
            'flow_token' => (string) $flow_token,
        );

        if ( ! empty( $error ) ) {
            $state['error'] = sanitize_text_field( $error );
        }

        return Guard_Dog_Utils::create_login_state_url(
            Guard_Dog_Utils::get_login_base_url(),
            $state
        );
    }

    /**
     * Get the rate-limit key for 2FA verification attempts.
     *
     * @since 1.9.41
     * @param int $user_id User ID.
     * @return string Transient key.
     */
    private function get_2fa_attempt_key( $user_id ) {
        $ip = class_exists('Guard_Dog_Utils') ? Guard_Dog_Utils::get_client_ip() : ( $_SERVER['REMOTE_ADDR'] ?? '' );
        return 'guard_dog_2fa_attempts_' . absint( $user_id ) . '_' . md5( (string) $ip );
    }

    /**
     * Check whether the current user/IP pair is over the 2FA attempt limit.
     *
     * @since 1.9.41
     * @param int $user_id User ID.
     * @return bool True when rate limited.
     */
    private function is_2fa_rate_limited( $user_id ) {
        $attempts = (int) get_transient( $this->get_2fa_attempt_key( $user_id ) );
        return $attempts >= 5;
    }

    /**
     * Record a failed 2FA attempt for the current user/IP pair.
     *
     * @since 1.9.41
     * @param int $user_id User ID.
     * @return void
     */
    private function record_failed_2fa_attempt( $user_id ) {
        $key = $this->get_2fa_attempt_key( $user_id );
        $attempts = (int) get_transient( $key );
        set_transient( $key, $attempts + 1, MINUTE_IN_SECONDS * 15 );
    }

    /**
     * Clear failed 2FA attempts for the current user/IP pair.
     *
     * @since 1.9.41
     * @param int $user_id User ID.
     * @return void
     */
    private function clear_failed_2fa_attempts( $user_id ) {
        delete_transient( $this->get_2fa_attempt_key( $user_id ) );
    }

    /**
     * Check if 2FA is enabled for a given user.
     *
     * @since 1.2.0
     * @param int|WP_User|string $user User ID, user object, username, or email.
     * @return bool True if enabled, false otherwise.
     */
    public function is_2fa_enabled_for_user($user) {
        $user_id = 0;

        // Handle different input types
        if (is_numeric($user)) {
            $user_id = absint($user);
        } elseif ($user instanceof WP_User) {
            $user_id = $user->ID;
        } elseif (is_string($user)) {
            // Handle username or email
            if (is_email($user)) {
                $user_obj = get_user_by('email', sanitize_email($user));
            } else {
                $user_obj = get_user_by('login', sanitize_user($user));
            }
            
            if ($user_obj && !is_wp_error($user_obj)) {
                $user_id = $user_obj->ID;
            }
        }

        if ($user_id <= 0) {
            return false;
        }

        return (bool) get_user_meta($user_id, self::USER_META_ENABLED_KEY, true);
    }

    /**
     * Check if a user has any 2FA method enabled (app-based or email-based).
     *
     * @since 1.8.0
     * @param int|WP_User|string $user User ID, user object, username, or email.
     * @return bool True if user has at least one 2FA method enabled, false otherwise.
     */
    public function user_has_any_2fa_enabled($user) {
        $user_id = 0;

        // Handle different input types
        if (is_numeric($user)) {
            $user_id = absint($user);
        } elseif ($user instanceof WP_User) {
            $user_id = $user->ID;
        } elseif (is_string($user)) {
            // Handle username or email
            if (is_email($user)) {
                $user_obj = get_user_by('email', sanitize_email($user));
            } else {
                $user_obj = get_user_by('login', sanitize_user($user));
            }
            
            if ($user_obj && !is_wp_error($user_obj)) {
                $user_id = $user_obj->ID;
            }
        }

        if ($user_id <= 0) {
            return false;
        }

        // Check if user has app-based 2FA enabled
        $has_app_2fa = $this->is_2fa_enabled_for_user($user_id);

        // Check if user has email 2FA enabled
        $has_email_2fa = false;
        $email_2fa_enabled = get_option('guard_dog_email_2fa_enabled', false);

        if ($email_2fa_enabled && function_exists('Guard_Dog') && isset(Guard_Dog()->features['email_two_factor_auth'])) {
            $has_email_2fa = Guard_Dog()->features['email_two_factor_auth']->is_email_2fa_enabled_for_user($user_id);
        }

        // Check if user has passkeys AND passkeys are configured to bypass/satisfy 2FA
        // When bypass_2fa is enabled, passkeys count as having 2FA for enforcement purposes
        $has_passkey_2fa = false;
        $passkeys_enabled = get_option('guard_dog_passkeys_enabled', false);
        $passkeys_bypass_2fa = get_option('guard_dog_passkeys_bypass_2fa', true);

        if ($passkeys_enabled && $passkeys_bypass_2fa) {
            if (function_exists('Guard_Dog') && isset(Guard_Dog()->features['passkeys'])) {
                $has_passkey_2fa = Guard_Dog()->features['passkeys']->user_has_passkeys($user_id);
            }
        }

        // User has 2FA if they have at least one method enabled
        return $has_app_2fa || $has_email_2fa || $has_passkey_2fa;
    }

    /**
     * Check if a user has recovery codes available.
     *
     * @since 1.8.1
     * @param int|WP_User|string $user User ID, user object, username, or email.
     * @return bool True if recovery codes exist, false otherwise.
     */
    public function user_has_recovery_codes($user) {
        $user_id = 0;

        // Handle different input types (same logic as above)
        if (is_numeric($user)) {
            $user_id = absint($user);
        } elseif ($user instanceof WP_User) {
            $user_id = $user->ID;
        } elseif (is_string($user)) {
            if (is_email($user)) {
                $user_obj = get_user_by('email', sanitize_email($user));
            } else {
                $user_obj = get_user_by('login', sanitize_user($user));
            }
            
            if ($user_obj && !is_wp_error($user_obj)) {
                $user_id = $user_obj->ID;
            }
        }

        if ($user_id <= 0) {
            return false;
        }

        $encrypted_codes = get_user_meta($user_id, self::USER_META_RECOVERY_CODES, true);
        $recovery_codes = $this->decode_recovery_codes($encrypted_codes);
        return !empty($recovery_codes);
    }

    /**
     * Get 2FA status information for a user.
     *
     * @since 1.8.1
     * @param int|WP_User|string $user User ID, user object, username, or email.
     * @return array Status information including enabled state and recovery codes count.
     */
    public function get_user_2fa_status($user) {
        $user_id = 0;
        $user_obj = null;

        // Handle different input types
        if (is_numeric($user)) {
            $user_id = absint($user);
            $user_obj = get_user_by('id', $user_id);
        } elseif ($user instanceof WP_User) {
            $user_id = $user->ID;
            $user_obj = $user;
        } elseif (is_string($user)) {
            if (is_email($user)) {
                $user_obj = get_user_by('email', sanitize_email($user));
            } else {
                $user_obj = get_user_by('login', sanitize_user($user));
            }
            
            if ($user_obj && !is_wp_error($user_obj)) {
                $user_id = $user_obj->ID;
            }
        }

        if ($user_id <= 0 || !$user_obj) {
            return [
                'enabled' => false,
                'user_exists' => false,
                'recovery_codes_count' => 0,
                'user_id' => 0
            ];
        }

        $is_enabled = (bool) get_user_meta($user_id, self::USER_META_ENABLED_KEY, true);
        $encrypted_codes = get_user_meta($user_id, self::USER_META_RECOVERY_CODES, true);
        $recovery_codes = $this->decode_recovery_codes($encrypted_codes);
        $recovery_count = count($recovery_codes);

        return [
            'enabled' => $is_enabled,
            'user_exists' => true,
            'recovery_codes_count' => $recovery_count,
            'user_id' => $user_id,
            'username' => $user_obj->user_login,
            'email' => $user_obj->user_email
        ];
    }

    /**
     * Public wrapper for validating 2FA codes in custom login forms.
     *
     * Nonce is required for HTTP-callable use (e.g. from browser forms) for CSRF protection.
     *
     * @since 1.8.1
     * @param string|int $user_identifier User ID, username, or email.
     * @param string $code The 2FA code to validate.
     * @param string $nonce Nonce from wp_create_nonce('guard_dog_verify_2fa_login_' . $user_id). Required for HTTP-callable use.
     * @return array Result array with 'success' boolean and 'message' string.
     */
    public function verify_user_2fa_code($user_identifier, $code, $nonce = '') {
        // Input validation
        if (empty($user_identifier) || empty($code)) {
            return [
                'success' => false,
                'message' => __('User identifier and verification code are required.', 'guard-dog')
            ];
        }

        if (empty($nonce)) {
            return [
                'success' => false,
                'message' => __('Security verification required.', 'guard-dog')
            ];
        }

        // Get user object
        if (is_numeric($user_identifier)) {
            $user = get_user_by('id', absint($user_identifier));
        } elseif (is_email($user_identifier)) {
            $user = get_user_by('email', sanitize_email($user_identifier));
        } else {
            $user = get_user_by('login', sanitize_user($user_identifier));
        }

        if (!$user || is_wp_error($user)) {
            return [
                'success' => false,
                'message' => __('Invalid user.', 'guard-dog')
            ];
        }

        // Check if 2FA is enabled for this user
        if (!$this->is_2fa_enabled_for_user($user->ID)) {
            return [
                'success' => false,
                'message' => __('Two-factor authentication is not enabled for this user.', 'guard-dog')
            ];
        }

        // Rate limiting check (prevent brute force)
        if ( $this->is_2fa_rate_limited( $user->ID ) ) {
            return [
                'success' => false,
                'message' => __('Too many failed attempts. Please try again later.', 'guard-dog')
            ];
        }

        // Prepare post data for internal validation
        $post_data = [
            'guard_dog_2fa_code' => sanitize_text_field($code),
            'guard_dog_2fa_nonce' => $nonce,
        ];

        // Use the private validation method
        $result = $this->validate_2fa_code($user, $post_data);

        if (is_wp_error($result)) {
            // Increment failed attempts
            $this->record_failed_2fa_attempt( $user->ID );

            return [
                'success' => false,
                'message' => $result->get_error_message()
            ];
        }

        // Success - clear any failed attempts
        $this->clear_failed_2fa_attempts( $user->ID );

        return [
            'success' => true,
            'message' => __('Verification successful.', 'guard-dog'),
            'user_id' => $user->ID
        ];
    }

    /**
     * Complete the 2FA login process securely without storing passwords.
     *
     * @since 1.8.42
     * @param WP_User $user The authenticated user.
     * @return void
     */
    private function complete_2fa_login($user): void {
        // Get the transient data from the stored key
        $transient_key = $this->get_2fa_transient_key( $user->user_login );
        $stored_data = get_transient($transient_key);

        if (false === $stored_data) {
            // Transient expired, redirect to login
            wp_safe_redirect(wp_login_url());
            exit;
        }
        
        // Clear the transient since we're completing the login
        delete_transient($transient_key);
        
        // Check if this login follows a password reset (stored as transient by password policy)
        $password_reset_success = (bool) get_transient('guard_dog_password_reset_pending_' . $user->ID);
        if ($password_reset_success) {
            delete_transient('guard_dog_password_reset_pending_' . $user->ID);
            // Set transient to show success message after redirect
            set_transient('guard_dog_password_reset_success_' . $user->ID, true, 30);
        }
        
        // Set up WordPress authentication
        wp_set_current_user($user->ID, $user->user_login);
        
        // Set authentication cookie
        $remember = $stored_data['remember'] ?? false;
        wp_set_auth_cookie($user->ID, $remember);
        
        // Fire WordPress login action
        // phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
        // wp_login is WordPress core's standard login hook that we're firing for compatibility.
        // This is not a custom hook that needs prefixing - it's WordPress core's hook that
        // other plugins and themes may be listening to. Firing this hook ensures proper
        // integration with WordPress's login system and third-party plugins.
        do_action('wp_login', $user->user_login, $user);
        // phpcs:enable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
        
        // Log the successful 2FA login
        if ($this->activity_log) {
            $this->activity_log->add_log_entry('2fa_successful', array(
                'user_id' => $user->ID,
                'username' => $user->user_login,
                'details' => array('method' => 'Two-Factor Authentication')
            ));
        }
        
        // Redirect to the intended destination (validate to allow only safe redirects)
        $redirect_to = wp_validate_redirect($stored_data['redirect_to'] ?? admin_url(), admin_url());
        if ( function_exists( 'Guard_Dog' ) && isset( Guard_Dog()->features['passkeys'] ) ) {
            $passkeys = Guard_Dog()->features['passkeys'];
            if ( is_object( $passkeys ) && method_exists( $passkeys, 'get_password_login_setup_prompt_redirect' ) ) {
                $redirect_to = $passkeys->get_password_login_setup_prompt_redirect(
                    $user,
                    $redirect_to,
                    ! empty( $stored_data['passkey_setup_intent'] )
                );
            }
        }
        wp_safe_redirect($redirect_to);
        exit;
    }

    /**
     * Check whether the password login came after a passkey setup attempt.
     *
     * @since 1.9.46
     * @return bool
     */
    private function has_passkey_setup_intent_from_post() {
        $field_name = class_exists( 'Guard_Dog_Passkeys' ) ? Guard_Dog_Passkeys::SETUP_INTENT_FIELD : 'guard_dog_passkey_setup_intent';

        // phpcs:disable WordPress.Security.NonceVerification.Missing
        $intent = isset( $_POST[ $field_name ] ) ? sanitize_text_field( wp_unslash( $_POST[ $field_name ] ) ) : '';
        // phpcs:enable WordPress.Security.NonceVerification.Missing

        return '1' === $intent;
    }

    //--------------------------------------------------------------------------
    // Script Enqueueing
    //--------------------------------------------------------------------------

    /**
     * Enqueue scripts and styles for the user profile page.
     *
     * @since 1.2.0
     * @param string $hook_suffix The current admin page hook.
     */
    public function enqueue_profile_scripts($hook_suffix) {
        // Only load on user profile page
        if ('profile.php' !== $hook_suffix && 'user-edit.php' !== $hook_suffix) {
            return;
        }

        // Check if the current user can setup 2FA for the displayed user
        // (simplification: assumes user can only setup for self)
        $user_id_param = filter_input(INPUT_GET, 'user_id', FILTER_VALIDATE_INT);
        $user_id = $user_id_param ? $user_id_param : get_current_user_id();
        if (get_current_user_id() !== $user_id) {
             return; // Don't load script if viewing someone else's profile
        }

        // Enqueue the CSS
        wp_enqueue_style(
            'guard-dog-profile-2fa',
            GUARD_DOG_URL . 'assets/css/guard-dog-profile-2fa.css',
            [],
            GUARD_DOG_VERSION
        );

        // Enqueue the script
        wp_enqueue_script(
            'guard-dog-profile-2fa',
            GUARD_DOG_URL . 'assets/js/guard-dog-profile-2fa.js',
            ['jquery'], // Dependency
            GUARD_DOG_VERSION, // Version
            true // Load in footer
        );

        // Pass data to the script (e.g., AJAX URL)
        // We already added nonce to the button, but could pass it here too if needed.
        wp_localize_script('guard-dog-profile-2fa', 'guard_dog_profile_2fa', [
            'ajax_url' => admin_url('admin-ajax.php'),
            'assets_url' => GUARD_DOG_URL . 'assets/',
            // Add any translatable strings needed by JS here
            'strings' => [
                'generating' => esc_html__('Generating...', 'guard-dog'),
                'enable_button' => esc_html__('Enable 2FA', 'guard-dog'),
                'ajax_error' => esc_html__('AJAX Error', 'guard-dog'),
                // Add more strings as needed
            ]
        ]);
    }

    /**
     * Enqueues scripts needed for the 2FA login form.
     * 
     * @since 1.8.0
     */
    public function enqueue_login_scripts() {
        // Check for state-token-based 2FA flow
        $login_state = Guard_Dog_Utils::consume_login_state();
        $has_2fa_state = is_array($login_state) && isset($login_state['type']) && $login_state['type'] === '2fa';

        if (!$has_2fa_state) {
            return;
        }

        // Enqueue jQuery and 2FA scripts
        wp_enqueue_script('jquery');
        wp_enqueue_style('guard-dog-login-2fa', GUARD_DOG_URL . 'assets/css/guard-dog-login-2fa.css', [], GUARD_DOG_VERSION);
        wp_enqueue_script('guard-dog-login-2fa', GUARD_DOG_URL . 'assets/js/guard-dog-login-2fa.js', ['jquery'], GUARD_DOG_VERSION, true);
    }

    /**
     * Adds session-based errors to the login form
     *
     * @param WP_Error $errors The WP_Error object
     * @param string $redirect_to The redirect URL
     * @return WP_Error The modified WP_Error object
     */
    public function add_session_errors($errors, $redirect_to) {
        // Check for state-token-based errors
        $login_state = Guard_Dog_Utils::consume_login_state();

        if (is_array($login_state) && isset($login_state['type']) && $login_state['type'] === 'error') {
            $state_error = isset($login_state['error']) ? sanitize_text_field($login_state['error']) : '';
            if ($state_error === 'session_expired') {
                $errors->add('session_expired', __('Your session has expired. Please log in again.', 'guard-dog'));
            }
        }

        return $errors;
    }

    /**
     * Shows grace period warnings to users in admin area
     */
    public function show_grace_period_warnings() {
        if (!is_user_logged_in()) {
            return;
        }
        
        $user_id = get_current_user_id();
        $user = get_user_by('ID', $user_id);
        
        // Check if 2FA is enforced
        $tfa_enforced = get_option('guard_dog_two_factor_enforced', false);
        if (!$tfa_enforced) {
            return;
        }
        
        // Check if user has any 2FA method enabled (app or email)
        if ($this->user_has_any_2fa_enabled($user)) {
            return;
        }
        
        // Check grace period
        $enforcement_date = get_option('guard_dog_two_factor_enforcement_date', '');
        $grace_period = get_option('guard_dog_two_factor_grace_period', 7);
        
        if ($enforcement_date) {
            $enforcement_timestamp = intval($enforcement_date) + ($grace_period * DAY_IN_SECONDS);
            $current_time = time();
            $days_remaining = ceil(($enforcement_timestamp - $current_time) / DAY_IN_SECONDS);
            
            if ($days_remaining > 0) {
                // Show warning notice
                $profile_url = admin_url('profile.php#guard-dog-2fa-section');
                $message = sprintf(
                    // Translators: %1$d is the number of days remaining for the user to set up 2FA. %2$s is the URL to the user profile page.
                    __('<strong>Two-Factor Authentication Required:</strong> You have %1$d days to set up 2FA before being locked out. <a href="%2$s">Set up 2FA now</a>.', 'guard-dog'),
                    $days_remaining,
                    $profile_url
                );
                
                // Show warning notice
                ?>
                <div class="notice notice-warning is-dismissible">
                    <p><?php echo wp_kses_post($message); ?></p>
                </div>
                <?php
            } else {
                // Grace period expired - show critical notice
                $message = __('<strong>Two-Factor Authentication Required:</strong> Your grace period has expired. You must set up 2FA to continue accessing the admin area.', 'guard-dog');
                
                // Show critical notice
                ?>
                <div class="notice notice-error">
                    <p><?php echo wp_kses_post($message); ?></p>
                    <p><a href="<?php echo esc_url(admin_url('profile.php#guard-dog-2fa-section')); ?>" class="button button-primary"><?php esc_html_e('Set up 2FA Now', 'guard-dog'); ?></a></p>
                </div>
                <?php
            }
        }
    }

    /**
     * Render the user profile 2FA content
     *
     * @param WP_User $user The user object being edited
     */
    private function render_user_profile_2fa_content($user) {
        // Check if 2FA is globally enabled
        $tfa_globally_enabled = get_option('guard_dog_two_factor_enabled', false);
        
        // Only allow users to setup 2FA for themselves AND if 2FA is globally enabled
        $can_setup = (get_current_user_id() === $user->ID) && $tfa_globally_enabled;

        // Check if recovery codes were just generated and need to be shown
        $show_recovery_codes = false;
        $recovery_codes_transient_key = '_guard_dog_show_recovery_codes_' . $user->ID;
        if ($can_setup) { // Only show to the user themselves
            $recovery_codes_to_show = get_transient($recovery_codes_transient_key);
            if (!empty($recovery_codes_to_show) && is_array($recovery_codes_to_show)) {
                $show_recovery_codes = true;
                // Delete transient once retrieved
                delete_transient($recovery_codes_transient_key);
            }
        }

        // Prepare template variables
        $template_args = [
            'user' => $user,
            'can_setup' => $can_setup,
            'tfa_globally_enabled' => $tfa_globally_enabled,
            'show_recovery_codes' => $show_recovery_codes,
            'recovery_codes_to_show' => $recovery_codes_to_show ?? [],
        ];

        // Debug logging to verify template loading
        if (function_exists('Guard_Dog') && Guard_Dog()->debug) {
            Guard_Dog()->debug->info('2FA user profile template loading', 'two_factor', [
                'user_id' => $user->ID,
                'can_setup' => $can_setup,
                'tfa_globally_enabled' => $tfa_globally_enabled,
                'show_recovery_codes' => $show_recovery_codes
            ]);
        }

        // Load the template using the view loader
        Guard_Dog_View_Loader::render_partial('user-profile-2fa', $template_args);
    }

    /**
     * AJAX handler to refresh 2FA nonces
     */
    public function ajax_refresh_nonces() {
        // Check if user is logged in first
        if (!is_user_logged_in()) {
            wp_send_json_error(array('message' => __('You must be logged in to use this feature.', 'guard-dog')), 403);
        }

        $user_id = get_current_user_id();
        
        // Use WordPress standard AJAX nonce verification (dedicated action for refresh)
        $expected_action = 'guard_dog_refresh_2fa_nonces_' . $user_id;
        $nonce = isset($_POST['_wpnonce']) ? sanitize_text_field(wp_unslash($_POST['_wpnonce'])) : '';
        
        if (!wp_verify_nonce($nonce, $expected_action)) {
            wp_send_json_error(array('message' => __('Security verification failed.', 'guard-dog')), 403);
        }

        // Generate fresh nonces
        $nonces = [
            'generate' => wp_create_nonce('guard_dog_generate_2fa_secret_' . $user_id),
            'verify' => wp_create_nonce('guard_dog_verify_2fa_setup_' . $user_id),
            'regenerateRecovery' => wp_create_nonce('guard_dog_regenerate_recovery_codes_' . $user_id),
            'disable' => wp_create_nonce('guard_dog_disable_2fa_' . $user_id),
            'refresh' => wp_create_nonce('guard_dog_refresh_2fa_nonces_' . $user_id),
        ];

        wp_send_json_success([
            'message' => __('Nonces refreshed successfully.', 'guard-dog'),
            'nonces' => $nonces
        ]);
    }

} // End class Guard_Dog_Two_Factor_Auth
