<?php

namespace WPSecurityNinja\Plugin;

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

use Da\TwoFA\Manager;
use Da\TwoFA\Service\TOTPSecretKeyUriGeneratorService;
use WPSecurityNinja\Plugin\chillerlan\QRCode\QRCode;
use WPSecurityNinja\Plugin\chillerlan\QRCode\QROptions;
use WP_User;

class Wf_Sn_2fa {

	/**
	 * Singleton instance.
	 *
	 * @var self|null
	 */
	private static $instance = null;
	private static $options  = null;
	private static $manager  = null;

	/**
	 * Stores password-based authentication sessions to be invalidated before 2FA.
	 *
	 * @since 1.0.0
	 * @var array
	 */
	private static $password_auth_tokens = array();

	/**
	 * Rate limiting for temp_token attempts
	 *
	 * @since 1.0.0
	 * @var array
	 */
	private static $rate_limit_cache = array();

	public static function init() {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	private function __construct() {
		// Defer options loading until init hook
		add_action( 'init', array( $this, 'load_options' ) );

		// Defer Manager initialization until needed
		add_action( 'wp_ajax_secnin_generate_qr_code', array( $this, 'ajax_generate_qr_code' ) );
		add_action( 'wp_ajax_nopriv_secnin_generate_qr_code', array( $this, 'ajax_generate_qr_code' ) );
		add_action( 'wp_ajax_secnin_skip_2fa', array( $this, 'ajax_skip_2fa' ) );
		add_action( 'wp_ajax_nopriv_secnin_skip_2fa', array( $this, 'ajax_skip_2fa' ) );
		add_action( 'wp_ajax_secnin_verify_2fa_code', array( $this, 'ajax_verify_2fa_code' ) );
		add_action( 'wp_ajax_nopriv_secnin_verify_2fa_code', array( $this, 'ajax_verify_2fa_code' ) );
		add_action( 'wp_ajax_secnin_send_2fa_email', array( $this, 'ajax_send_2fa_email' ) );
		add_action( 'wp_ajax_nopriv_secnin_send_2fa_email', array( $this, 'ajax_send_2fa_email' ) );

		// Admin 2FA setup handlers
		add_action( 'wp_ajax_secnin_admin_generate_qr', array( $this, 'ajax_admin_generate_qr' ) );
		add_action( 'wp_ajax_secnin_admin_verify_2fa', array( $this, 'ajax_admin_verify_2fa' ) );
		add_action( 'wp_ajax_secnin_admin_reset_2fa', array( $this, 'ajax_admin_reset_2fa' ) );
		add_action( 'wp_ajax_secnin_admin_enroll_email_2fa', array( $this, 'ajax_admin_enroll_email_2fa' ) );
	}

	/**
	 * Load options during init hook to ensure translations are available
	 *
	 * @author  Unknown
	 * @since   v0.0.1
	 * @version v1.0.0  Thursday, April 3rd, 2025.
	 * @access  public
	 * @return  void
	 */
	public function load_options() {
		if ( is_null( self::$options ) ) {
			self::$options = self::get_options();

			if ( ! empty( self::$options['2fa_enabled'] ) ) {
				$this->register_hooks();
			}
		}
	}

	private function init_manager() {
		if ( is_null( self::$manager ) ) {
			try {

				$class_name = 'Da\TwoFA\Manager';
				if ( ! class_exists( $class_name ) ) {
					// Check if vendor directory exists
					if ( ! file_exists( WF_SN_PLUGIN_DIR . 'vendor/2amigos/2fa-library/src/Manager.php' ) ) {
						throw new \Exception( '2FA library files not found. Path: ' . WF_SN_PLUGIN_DIR . 'vendor/2amigos/2fa-library/src/Manager.php' );
					}

					// Try direct include if autoloader fails
					require_once WF_SN_PLUGIN_DIR . 'vendor/2amigos/2fa-library/src/Manager.php';

					if ( ! class_exists( $class_name ) ) {
						// Let's check if it's in the prefixed namespace
						$prefixed_class = 'WPSecurityNinja\Plugin\Da\TwoFA\Manager';
						if ( class_exists( $prefixed_class ) ) {
							$class_name = $prefixed_class;
						} else {
							throw new \Exception( '2FA Manager class not found in either namespace. Check composer installation.' );
						}
					}
				}

				self::$manager = new $class_name();

			} catch ( \Exception $e ) {
				throw $e;
			}
		}
		return self::$manager;
	}

	/**
	 * register_hooks.
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Tuesday, June 4th, 2024.
	 * @access  private
	 * @return  void
	 */
	private function register_hooks() {
		// Authentication hooks
		add_action( 'wp_login', array( __CLASS__, 'wp_login' ), 10, 2 );
		add_action( 'clear_auth_cookie', array( $this, 'clear_2fa_session' ) );

		add_action( 'edit_user_profile', array( $this, 'add_bypass_2fa_checkbox' ) );
		add_action( 'show_user_profile', array( $this, 'add_bypass_2fa_checkbox' ) );
		add_action( 'edit_user_profile_update', array( $this, 'save_bypass_2fa_checkbox' ) );
		add_action( 'personal_options_update', array( $this, 'save_bypass_2fa_checkbox' ) );

		// Enqueue admin 2FA scripts
		add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_2fa_scripts' ) );

		// Add hooks for collecting auth cookies
		add_action( 'set_auth_cookie', array( __CLASS__, 'collect_auth_cookie_tokens' ) );
		add_action( 'set_logged_in_cookie', array( __CLASS__, 'collect_auth_cookie_tokens' ) );
	}

	/**
	 * Handles the login process for users with 2FA enabled.
	 *
	 * @param string $user_login The user's login name.
	 * @param WP_User $user The user object.
	 * @return void
	 */
	public static function wp_login( $user_login, $user ) {
		if ( ! $user instanceof WP_User || ! isset( $user->ID ) ) {
			return;
		}
		$is_user_using_two_factor   = self::is_user_using_two_factor( $user->ID );
		$should_user_use_two_factor = self::should_user_use_two_factor( $user->ID );
		$requires_2fa_challenge     = self::user_requires_2fa_challenge( $user->ID );

		$my_options = self::get_options();

		if ( isset( $my_options['2fa_enabled'] ) && $my_options['2fa_enabled'] ) {
			if ( $requires_2fa_challenge ) {
				if ( isset( $_GET['skip_2fa'] ) && '1' === $_GET['skip_2fa'] &&
					isset( $_GET['nonce'] ) && wp_verify_nonce( sanitize_key( $_GET['nonce'] ), 'skip_2fa_nonce' ) ) {

					if ( self::user_may_skip_2fa_setup_during_grace( $user->ID ) ) {
						self::handle_2fa_session( $user, true );

						wp_set_current_user( $user->ID, $user->user_login );
						wp_set_auth_cookie( $user->ID, true );

						remove_action( 'wp_login', array( __CLASS__, 'wp_login' ), 10 );
						do_action( 'wp_login', $user->user_login, $user );
						add_action( 'wp_login', array( __CLASS__, 'wp_login' ), 10, 2 );

						$skip_redirect_to = '';
						if ( isset( $_REQUEST['redirect_to'] ) ) {
							$skip_redirect_to = esc_url_raw( $_REQUEST['redirect_to'] );
						} elseif ( isset( $_GET['redirect_to'] ) ) {
							$skip_redirect_to = esc_url_raw( $_GET['redirect_to'] );
						} elseif ( isset( $_POST['redirect_to'] ) ) {
							$skip_redirect_to = esc_url_raw( $_POST['redirect_to'] );
						} elseif ( isset( $_REQUEST['redirect'] ) ) {
							$skip_redirect_to = esc_url_raw( $_REQUEST['redirect'] );
						} elseif ( isset( $_POST['redirect'] ) ) {
							$skip_redirect_to = esc_url_raw( $_POST['redirect'] );
						}
						$redirect_to = self::get_redirect_url( $user, $skip_redirect_to );
						wp_safe_redirect( $redirect_to );
						exit;
					}
				}

				self::handle_2fa_session( $user );

				$redirect_to = '';
				if ( isset( $_REQUEST['redirect_to'] ) ) {
					$redirect_to = esc_url_raw( $_REQUEST['redirect_to'] );
				} elseif ( isset( $_GET['redirect_to'] ) ) {
					$redirect_to = esc_url_raw( $_GET['redirect_to'] );
				} elseif ( isset( $_POST['redirect_to'] ) ) {
					$redirect_to = esc_url_raw( $_POST['redirect_to'] );
				} elseif ( isset( $_REQUEST['redirect'] ) ) {
					$redirect_to = esc_url_raw( $_REQUEST['redirect'] );
				} elseif ( isset( $_POST['redirect'] ) ) {
					$redirect_to = esc_url_raw( $_POST['redirect'] );
				}

				if ( ! $is_user_using_two_factor ) {
					$temp_token = self::create_secure_temp_token( $user->ID, $redirect_to );
					self::render_2fa_verify_page( $user, $temp_token );
				} else {
					$temp_token = self::create_secure_temp_token( $user->ID, $redirect_to );
					self::render_2fa_verify_page( $user, $temp_token );
				}
				exit;
			}
		}
	}

	/**
	 * Generate QR code for 2FA setup
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Thursday, July 20th, 2023.
	 * @access  public static
	 * @return  void
	 */
	public function ajax_generate_qr_code() {
		try {
			if ( empty( $_POST['temp_token'] ) ) {
				throw new \Exception( __( 'Missing temp_token', 'security-ninja' ) );
			}

			$temp_token   = sanitize_text_field( $_POST['temp_token'] );
			$session_data = self::validate_temp_token( $temp_token );

			if ( ! $session_data ) {
				throw new \Exception( __( 'Invalid or expired session', 'security-ninja' ) );
			}

			$user_id = $session_data['user_id'];

			// Increment attempt counter for rate limiting
			self::increment_token_attempts( $temp_token, $session_data );

			$user = get_user_by( 'ID', $user_id );
			if ( ! $user ) {
				throw new \Exception( 'User not found' );
			}

			// Initialize manager using the shared method
			$manager = $this->init_manager();

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

			// Store temporary secret
			if ( ! set_transient( 'secnin_2fa_temp_secret_' . $temp_token, $secret, 30 * MINUTE_IN_SECONDS ) ) {
				throw new \Exception( 'Failed to store secret' );
			}

			// Get site info for the label
			$site_url = Wf_Sn_Vu::remove_http( site_url() );

			// Create the otpauth URI manually to ensure correct format
			// Format: site_url:username (e.g., "example.com:john")
			$label   = rawurlencode( $site_url );
			$issuer  = rawurlencode( $site_url );
			$account = rawurlencode( $user->user_login );

			// Format: otpauth://totp/Label:account?secret=SECRET&issuer=Issuer
			$uri = sprintf(
				'otpauth://totp/%s:%s?secret=%s&issuer=%s',
				$label,
				$account,
				$secret,
				$issuer
			);

			// Generate QR code
			$options = new QROptions(
				array(
					'outputType'  => QRCode::OUTPUT_MARKUP_SVG,
					'imageBase64' => true,
				)
			);

			$qrcode           = new QRCode( $options );
			$qr_code_data_uri = $qrcode->render( $uri );

			wp_send_json_success(
				array(
					'qr_code' => $qr_code_data_uri,
					'secret'  => $secret,
					'message' => __( 'QR code generated successfully', 'security-ninja' ),
				)
			);

		} catch ( \Exception $e ) {
			self::rethrow_ajax_die_exception( $e );
			wp_send_json_error( array( 'message' => $e->getMessage() ) );
		}
	}

	/**
	 * Adds 2FA setup section to user profile page.
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.1  Thursday, July 18th, 2024.
	 * @access  public static
	 * @param   WP_User $user The user object.
	 * @return  void
	 */
	public static function add_bypass_2fa_checkbox( $user ) {
		if ( ! $user instanceof WP_User ) {
			return;
		}

		$my_options   = self::get_options();
		$global_2fa   = ! empty( $my_options['2fa_enabled'] );
		if ( ! $global_2fa ) {
			return;
		}

		$bypass2fa       = get_user_meta( $user->ID, 'bypass_2fa', true );
		$is_2fa_setup    = self::is_2fa_setup_complete( $user->ID );
		$should_use_2fa  = self::should_user_use_two_factor( $user->ID );
		$is_opted_in     = self::is_user_opted_in_to_2fa( $user->ID );
		$is_admin        = in_array( 'administrator', (array) $user->roles, true );
		$allowed_methods = self::get_allowed_methods();
		$allows_app      = in_array( 'app', $allowed_methods, true );
		$allows_email    = in_array( 'email', $allowed_methods, true );
		$user_method     = self::get_user_2fa_method( $user->ID );
		$can_edit_user   = current_user_can( 'edit_user', $user->ID );
		$is_self         = ( get_current_user_id() === (int) $user->ID );
		$can_reset       = false;

		if ( $is_2fa_setup ) {
			if ( $is_opted_in && ! $should_use_2fa ) {
				$can_reset = true;
			} elseif ( $can_edit_user && ! $is_self ) {
				$can_reset = true;
			} elseif ( current_user_can( 'manage_options' ) ) {
				$can_reset = true;
			}
		}

		?>
		<h3><?php esc_html_e( 'Two Factor Authentication', 'security-ninja' ); ?></h3>
		<table class="form-table">
			<?php if ( $is_admin && $can_edit_user && ! $is_self ) : ?>
			<tr>
				<th><label for="bypass_2fa"><?php esc_html_e( 'Bypass 2FA', 'security-ninja' ); ?></label></th>
				<td>
					<input type="checkbox" name="bypass_2fa" id="bypass_2fa" value="1" <?php checked( $bypass2fa, '1' ); ?> />
					<span class="description">
						<?php esc_html_e( 'Check this box to allow the user to bypass Two Factor Authentication.', 'security-ninja' ); ?>
					</span>
				</td>
			</tr>
			<?php endif; ?>

			<tr>
				<th><label><?php esc_html_e( '2FA Status', 'security-ninja' ); ?></label></th>
				<td>
					<?php if ( $is_2fa_setup ) : ?>
						<span class="dashicons dashicons-yes-alt" style="color: #46b450;"></span>
						<strong style="color: #46b450;"><?php esc_html_e( '2FA is configured', 'security-ninja' ); ?></strong>
						<p class="description">
							<?php
							if ( 'email' === $user_method ) {
								esc_html_e( 'This user uses email verification for 2FA.', 'security-ninja' );
							} else {
								esc_html_e( 'This user has completed 2FA setup with an authenticator app.', 'security-ninja' );
							}
							?>
							<?php if ( $can_reset ) : ?>
								<button type="button" id="reset-2fa" class="button button-secondary" style="margin-left: 10px;">
									<?php esc_html_e( 'Disable 2FA', 'security-ninja' ); ?>
								</button>
							<?php endif; ?>
						</p>
					<?php else : ?>
						<span class="dashicons dashicons-warning" style="color: #dc3232;"></span>
						<strong style="color: #dc3232;"><?php esc_html_e( '2FA not configured', 'security-ninja' ); ?></strong>
						<p class="description">
							<?php if ( $should_use_2fa ) : ?>
								<?php esc_html_e( 'This user is required to set up 2FA but has not done so yet.', 'security-ninja' ); ?>
							<?php else : ?>
								<?php esc_html_e( 'Two-factor authentication is optional for this user. Enable it below if desired.', 'security-ninja' ); ?>
							<?php endif; ?>
						</p>
					<?php endif; ?>
				</td>
			</tr>

			<?php if ( ! $is_2fa_setup ) : ?>
			<tr>
				<th><label><?php esc_html_e( 'Setup 2FA', 'security-ninja' ); ?></label></th>
				<td>
					<div id="admin-2fa-setup">
						<?php if ( $allows_app ) : ?>
						<p class="description">
							<?php esc_html_e( 'Scan the QR code with your authenticator app and enter the verification code to enable 2FA.', 'security-ninja' ); ?>
						</p>
						<button type="button" id="setup-2fa" class="button button-primary">
							<?php esc_html_e( 'Setup with authenticator app', 'security-ninja' ); ?>
						</button>
						<?php endif; ?>
						<?php if ( $allows_email ) : ?>
							<?php if ( $allows_app ) : ?>
						<p class="description" style="margin-top: 15px;">
								<?php esc_html_e( 'Or enable email verification instead:', 'security-ninja' ); ?>
						</p>
						<?php endif; ?>
						<button type="button" id="enroll-email-2fa" class="button <?php echo $allows_app ? 'button-secondary' : 'button-primary'; ?>">
							<?php esc_html_e( 'Enable 2FA via email', 'security-ninja' ); ?>
						</button>
						<?php endif; ?>
						<div id="admin-2fa-qr-container" style="display: none; margin-top: 20px;">
							<div class="qr-code-admin">
								<img id="admin-qr-code-img"
									src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200'%3E%3Crect width='200' height='200' fill='%23ffffff' stroke='%23eee' stroke-width='2'/%3E%3C/svg%3E"
									alt="<?php esc_attr_e( 'QR Code Loading', 'security-ninja' ); ?>"
									style="display: inline-block; width: 200px; height: 200px;" />

									<div id="admin-secret-container" style="display: none; margin-top: 15px; padding: 15px; background: #f0f0f1; border-radius: 5px; border: 1px solid #ddd;">
								<p style="margin: 0 0 10px 0; font-weight: bold;">
									<?php esc_html_e( 'Manual Entry Secret Key:', 'security-ninja' ); ?>
								</p>
								<div style="display: flex; align-items: center; gap: 10px;">
									<input type="text" id="admin-secret-key" readonly style="text-align: center; flex: 1; font-family: monospace; font-size: 14px; padding: 8px; border: 1px solid #ccc; border-radius: 3px; background: #fff;" />
									<button type="button" id="admin-copy-secret" class="button button-secondary">
										<?php esc_html_e( 'Copy', 'security-ninja' ); ?>
									</button>
								</div>
								<p style="margin: 10px 0 0 0; font-size: 12px; color: #666;">
									<?php esc_html_e( 'Use this secret key to manually add the account to your authenticator app if you cannot scan the QR code.', 'security-ninja' ); ?>
								</p>
							</div>
								<div id="admin-qr-code-err"></div>
								<div class="spinner" id="admin-qr-spinner"></div>
								<button id="admin-generate-qr" class="button button-secondary" style="display: none;">
									<?php esc_html_e( 'Generate New QR Code', 'security-ninja' ); ?>
								</button>
							</div>

							<div id="admin-2fa-verification" style="margin-top: 20px;">
								<p>
									<label for="admin-2fa-code">
										<?php esc_html_e( 'Enter the 6-digit code from your authentication app:', 'security-ninja' ); ?>
									</label>
									<input type="text" name="admin-2fa-code" id="admin-2fa-code" class="regular-text" value="" size="6" maxlength="6" pattern="\d{6}" inputmode="numeric" autocapitalize="off" autocomplete="off" />
								</p>
								<button type="button" id="admin-verify-2fa" class="button button-primary" disabled>
									<?php esc_html_e( 'Verify & Save', 'security-ninja' ); ?>
								</button>
								<div id="admin-2fa-verify-msg"></div>
							</div>
						</div>
					</div>
				</td>
			</tr>
			<?php endif; ?>
		</table>

		<style>
		.qr-code-admin {
			text-align: center;
			margin: 20px 0;
			padding: 20px;
			background: #f9f9f9;
			border-radius: 5px;
			border: 1px solid #ddd;
		}

		.qr-code-admin img {
			display: inline-block;
			max-width: 100%;
			height: auto;
			border: 1px solid #ccc;
			border-radius: 3px;
		}

		.spinner {
			background: url(<?php echo esc_url( admin_url( 'images/spinner.gif' ) ); ?>) no-repeat;
			background-size: 20px 20px;
			display: none;
			float: right;
			opacity: 0.7;
			width: 20px;
			height: 20px;
			margin: 5px 5px 0;
		}

		.spinner.is-active {
			display: inline-block;
		}

		#admin-2fa-verify-msg {
			margin-top: 10px;
		}

		#admin-2fa-verify-msg .notice {
			margin: 5px 0;
			padding: 8px 12px;
		}

		#admin-2fa-code {
			width: 120px;
			text-align: center;
			font-size: 16px;
			letter-spacing: 2px;
		}
		</style>

		<script>
		var admin_2fa_config = {
			ajaxurl: '<?php echo esc_js( admin_url( 'admin-ajax.php' ) ); ?>',
			user_id: <?php echo intval( $user->ID ); ?>,
			nonce: '<?php echo esc_js( wp_create_nonce( 'admin_2fa_setup_' . $user->ID ) ); ?>',
			allowed_methods: <?php echo wp_json_encode( array_values( $allowed_methods ) ); ?>,
			messages: {
				verifying: '<?php echo esc_js( __( 'Verifying code...', 'security-ninja' ) ); ?>',
				network_error: '<?php echo esc_js( __( 'Network error. Please check your connection and try again.', 'security-ninja' ) ); ?>',
				qr_error: '<?php echo esc_js( __( 'Error generating QR code. Please try again.', 'security-ninja' ) ); ?>',
				success: '<?php echo esc_js( __( '2FA setup completed successfully!', 'security-ninja' ) ); ?>',
				invalid_code: '<?php echo esc_js( __( 'Invalid code. Please try again.', 'security-ninja' ) ); ?>',
				unknown_error: '<?php echo esc_js( __( 'An unknown error occurred. Please try again.', 'security-ninja' ) ); ?>',
				reset_confirm: '<?php echo esc_js( __( 'Are you sure you want to disable 2FA for this user? This action cannot be undone.', 'security-ninja' ) ); ?>',
				email_enroll_confirm: '<?php echo esc_js( __( 'Enable two-factor authentication using email codes?', 'security-ninja' ) ); ?>',
				email_enroll_success: '<?php echo esc_js( __( 'Email 2FA enabled successfully!', 'security-ninja' ) ); ?>'
			}
		};
		</script>
		<?php
	}

	/**
	 * Save the bypass 2FA checkbox value for a user.
	 *
	 * @param int $user_id User ID.
	 * @return void
	 */
	public static function save_bypass_2fa_checkbox( $user_id ) {
		if ( ! current_user_can( 'edit_user', $user_id ) ) {
			return false;
		}

		$bypass_2fa = isset( $_POST['bypass_2fa'] ) ? sanitize_text_field( $_POST['bypass_2fa'] ) : '';
		update_user_meta( $user_id, 'bypass_2fa', $bypass_2fa );
	}

	/**
	 * Enqueue admin 2FA scripts on user profile pages
	 *
	 * @param string $hook_suffix Current admin page hook suffix
	 */
	public function enqueue_admin_2fa_scripts( $hook_suffix ) {
		// Only enqueue on user profile pages
		if ( 'profile.php' === $hook_suffix || 'user-edit.php' === $hook_suffix ) {
			Wf_Sn::enqueue_sn_dialog();
			wp_enqueue_script(
				'admin-two-factor-auth',
				plugins_url( '/js/admin-two-factor-auth.js', __FILE__ ),
				array( 'jquery', 'sn-dialog' ),
				'1.1.0',
				true
			);
		}
	}

	/**
	 * Checks if the 2FA code is validated for the current session.
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Tuesday, June 4th, 2024.
	 * @access  public static
	 * @param   WP_User $user The user object to check.
	 * @return  bool    True if the code is validated, false otherwise.
	 */
	public static function is_code_validated_for_session( $user ) {
		if ( ! $user instanceof WP_User ) {
			return false;
		}

		$code_validated = get_user_meta( $user->ID, 'secnin_2fa_code_validated', true );
		return ! empty( $code_validated );
	}

	/**
	 * Clears user session for 2FA when logging out.
	 *
	 * This method is called when the auth cookie is cleared, typically during logout.
	 * It removes the 2FA session validation metadata for the current user.
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.1  Tuesday, June 4th, 2024.
	 * @access  public static
	 * @return  void
	 */
	public static function clear_2fa_session() {
		$user_id = get_current_user_id();
		if ( $user_id > 0 ) {
			delete_user_meta( $user_id, 'secnin_2fa_session_validated' );
		}
	}

	/**
	 * Retrieves the 2FA options.
	 *
	 * This method fetches all options and filters out only those related to 2FA.
	 * If options have already been retrieved, it returns the cached version.
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.1  Tuesday, June 4th, 2024.
	 * @access  public static
	 * @return  array An array of 2FA-related options.
	 */
	public static function get_options() {
		if ( ! is_null( self::$options ) ) {
			return self::$options;
		}

		$options = \WPSecurityNinja\Plugin\wf_sn_cf::get_options();

		$options = array_filter(
			$options,
			function ( $key ) {
				return strpos( $key, '2fa_' ) === 0;
			},
			ARRAY_FILTER_USE_KEY
		);

		self::$options = $options;
		return self::$options;
	}

	/**
	 * Retrieve or generate a passphrase for encryption.
	 *
	 * This method retrieves an existing passphrase from the WordPress options table,
	 * or generates a new one if it doesn't exist.
	 *
	 * @since   v0.0.1
	 * @version v1.0.0  Wednesday, July 17th, 2024.
	 * @access  private
	 * @return  string The retrieved or newly generated passphrase.
	 * @throws  \Exception If a secure random bytes generation fails.
	 */
	private static function get_passphrase() {
		$option_name = 'secnin_2fa_passphrase';
		$passphrase  = get_option( $option_name );

		if ( empty( $passphrase ) ) {
			try {
				// Generate a secure 256-bit passphrase.
				$random_bytes = random_bytes( 32 );
				$passphrase   = bin2hex( $random_bytes );

				$updated = update_option( $option_name, $passphrase );
				if ( ! $updated ) {
					throw new \Exception( __( 'Failed to save the generated passphrase.', 'security-ninja' ) );
				}
			} catch ( \Exception $e ) {
				// Log the error or handle it appropriately
				// Log the error using the event logger
				wf_sn_el_modules::log_event(
					'security_ninja',
					'2fa_passphrase_generation_failed',
					sprintf(
						/* translators: %s: Error message */
						__( 'Failed to generate or save 2FA passphrase: %s', 'security-ninja' ),
						$e->getMessage()
					)
				);
				throw $e; // Re-throw the exception for the caller to handle
			}
		}

		return $passphrase;
	}

	/**
	 * Encrypt a secret with IV concatenation.
	 *
	 * @param string $secret The secret to encrypt.
	 * @param string $passphrase The passphrase to use for encryption.
	 * @return string|false The concatenated IV and encrypted secret, or false on failure.
	 */
	private static function encrypt_secret( $secret, $passphrase ) {
		if ( empty( $secret ) || empty( $passphrase ) ) {
			return false;
		}

		$iv               = openssl_random_pseudo_bytes( 16 );
		$encrypted_secret = openssl_encrypt(
			$secret,
			'aes-256-cbc',
			$passphrase,
			OPENSSL_RAW_DATA,
			$iv
		);

		if ( false === $encrypted_secret ) {
			return false;
		}

		return base64_encode( $iv . $encrypted_secret );
	}

	/**
	 * Decrypt a secret with IV extraction.
	 *
	 * @param string $data The concatenated IV and encrypted secret.
	 * @param string $passphrase The passphrase to use for decryption.
	 * @return string|false The decrypted secret, or false on failure.
	 */
	private static function decrypt_secret( $data, $passphrase ) {
		if ( empty( $data ) || empty( $passphrase ) ) {
			return false;
		}

		$decoded_data = base64_decode( $data );
		if ( false === $decoded_data ) {
			return false;
		}

		if ( strlen( $decoded_data ) < 16 ) {
			return false;
		}

		$iv               = substr( $decoded_data, 0, 16 );
		$encrypted_secret = substr( $decoded_data, 16 );

		$decrypted = openssl_decrypt(
			$encrypted_secret,
			'aes-256-cbc',
			$passphrase,
			OPENSSL_RAW_DATA,
			$iv
		);

		if ( false !== $decrypted ) {
			return $decrypted;
		} else {
			return false;
		}
	}

	/**
	 * Verify the 2FA code submitted by the user.
	 *
	 * This method handles the AJAX request for verifying the 2FA code.
	 * It checks the nonce, validates the code, and logs the user in if successful.
	 *
	 * @since   v0.0.1
	 * @version v1.0.2  Friday, July 19th, 2024.
	 * @access  public static
	 * @return  void
	 */
	public function ajax_verify_2fa_code() {
		try {
			if ( empty( $_POST['code'] ) || empty( $_POST['temp_token'] ) ) {
				throw new \Exception( __( 'Missing required information.', 'security-ninja' ) );
			}

			// Initialize manager using the instance method
			$manager = $this->init_manager();

			$code         = sanitize_text_field( $_POST['code'] );
			$temp_token   = sanitize_text_field( $_POST['temp_token'] );
			$session_data = self::validate_temp_token( $temp_token );

			if ( ! $session_data ) {
				// Increment failed attempts for rate limiting
				$failed_session = get_transient( 'secnin_2fa_temp_' . $temp_token );
				if ( $failed_session && is_array( $failed_session ) ) {
					self::increment_token_attempts( $temp_token, $failed_session );
				}
				throw new \Exception( __( 'Invalid or expired session.', 'security-ninja' ) );
			}

			$user_id = $session_data['user_id'];

			// Increment attempt counter
			self::increment_token_attempts( $temp_token, $session_data );

			// Check 2FA method and verify accordingly
			$method   = self::resolve_verification_method( $user_id );
			$is_valid = false;

			if ( 'email' === $method ) {
				// Verify email code
				$is_valid = self::validate_email_code( $user_id, $code );
			} else {
				// Verify app code - prioritize temp secret for initial setup
				$temp_secret = get_transient( 'secnin_2fa_temp_secret_' . $temp_token );
				$secret      = null;

				if ( $temp_secret ) {
					// Use temporary secret (for initial setup)
					$secret = $temp_secret;
				} else {
					// Fall back to stored user secret (for existing users)
					$encrypted_secret = get_user_meta( $user_id, 'secnin_2fa_secret', true );
					if ( ! empty( $encrypted_secret ) ) {
						$secret = self::decrypt_secret( $encrypted_secret, self::get_passphrase() );
					}
				}

				if ( empty( $secret ) ) {
					throw new \Exception( __( 'No valid 2FA secret found. Please generate a new QR code.', 'security-ninja' ) );
				}

				$is_valid = self::$manager->verify( $code, $secret );
			}

			if ( $is_valid ) {
				// If verification successful and we used a temp secret, store it permanently
				if ( isset( $temp_secret ) && ! empty( $temp_secret ) ) {
					$encrypted_secret = self::encrypt_secret( $temp_secret, self::get_passphrase() );
					update_user_meta( $user_id, 'secnin_2fa_secret', $encrypted_secret );
					delete_transient( 'secnin_2fa_temp_secret_' . $temp_token );
				}

				// Rest of the success handling code...
				update_user_meta( $user_id, 'secnin_2fa_code_validated', 1 );
				update_user_meta( $user_id, 'secnin_2fa_session_validated', time() );
				update_user_meta( $user_id, 'secnin_2fa_setup_complete', 1 );
				update_user_meta( $user_id, 'secnin_2fa_method', $method );
				if ( ! self::should_user_use_two_factor( $user_id ) ) {
					update_user_meta( $user_id, 'secnin_2fa_optin', 1 );
				}

				$user_data = get_userdata( $user_id );
				if ( false === $user_data ) {
					\WPSecurityNinja\Plugin\wf_sn_el_modules::log_event(
						'security_ninja',
						'login_2fa_verification_failed',
						/* translators: %d: User ID */
						sprintf( __( 'Failed to retrieve user data for user ID: %d', 'security-ninja' ), $user_id )
					);
					wp_send_json_error( array( 'message' => __( 'Failed to retrieve user data.', 'security-ninja' ) ) );
					return;
				}

				wp_set_current_user( $user_id, $user_data->user_login );
				wp_set_auth_cookie( $user_id, true );
				remove_action( 'wp_login', array( __CLASS__, 'wp_login' ), 10 );
				do_action( 'wp_login', $user_data->user_login, $user_data );
				add_action( 'wp_login', array( __CLASS__, 'wp_login' ), 10, 2 );

				\WPSecurityNinja\Plugin\wf_sn_el_modules::log_event(
					'security_ninja',
					'login_2fa_verification_successful',
					/* translators: %s: User display name */
					sprintf( __( '2FA verification successful for %s', 'security-ninja' ), $user_data->display_name )
				);

				// Get stored redirect URL from session data
				$stored_redirect_to = isset( $session_data['redirect_to'] ) ? $session_data['redirect_to'] : '';
				$redirect_to        = self::get_redirect_url( $user_data, $stored_redirect_to );

				wp_send_json_success(
					array(
						'message'  => __( 'Verified. Logging you in.', 'security-ninja' ),
						'redir_to' => self::ensure_safe_redirect_url_for_response( $redirect_to ),
					)
				);
			} else {
				wp_send_json_error(
					array(
						'message' => __( 'Incorrect 2FA code. Please try again.', 'security-ninja' ),
					)
				);
			}
		} catch ( \Exception $e ) {
			self::rethrow_ajax_die_exception( $e );
			wp_send_json_error( array( 'message' => $e->getMessage() ) );
		}
	}

	/**
	 * Checks if 2FA setup is complete for a user.
	 *
	 * @param int $user_id The ID of the user to check.
	 * @return bool True if 2FA setup is complete, false otherwise.
	 */
	public static function is_2fa_setup_complete( $user_id ) {
		if ( ! is_numeric( $user_id ) || $user_id <= 0 ) {
			return false;
		}

		$setup_complete = get_user_meta( $user_id, 'secnin_2fa_setup_complete', true );
		return '1' === $setup_complete;
	}

	/**
	 * Renders the 2FA verification page.
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.1  Tuesday, June 4th, 2024.
	 * @access  public static
	 * @param   WP_User|int $user       The user object or user ID to verify.
	 * @param   string      $temp_token Optional temporary token for verification flow.
	 * @return  void
	 */
	public static function render_2fa_verify_page( $user, $temp_token = '' ) {
		if ( empty( $temp_token ) ) {
			$redirect_to = '';
			if ( isset( $_REQUEST['redirect_to'] ) ) {
				$redirect_to = esc_url_raw( $_REQUEST['redirect_to'] );
			} elseif ( isset( $_GET['redirect_to'] ) ) {
				$redirect_to = esc_url_raw( $_GET['redirect_to'] );
			} elseif ( isset( $_POST['redirect_to'] ) ) {
				$redirect_to = esc_url_raw( $_POST['redirect_to'] );
			} elseif ( isset( $_REQUEST['redirect'] ) ) {
				$redirect_to = esc_url_raw( $_REQUEST['redirect'] );
			} elseif ( isset( $_POST['redirect'] ) ) {
				$redirect_to = esc_url_raw( $_POST['redirect'] );
			}
			$temp_token = self::create_secure_temp_token( $user->ID, $redirect_to );
		}

		// No nonce needed - temp_token provides security
		$qr_nonce = '';

		if ( ! $user ) {
			return;
		}

		$user_id = 0;

		// Check if $user_id is an int value or a user object
		if ( $user instanceof WP_User ) {
			$user_id = $user->ID;
		} elseif ( is_numeric( $user ) ) {
			$user_id = absint( $user );
		} else {
			wp_die( 'Invalid user ID or object.' );
		}
		if ( ! get_userdata( $user_id ) ) {
			wp_die( 'Invalid user ID.' );
		}

		$user_2fa_setup_complete = self::is_2fa_setup_complete( $user_id );
		$has_app_secret          = self::is_user_using_two_factor( $user_id );
		$allowed_methods         = self::get_allowed_methods();
		$selected_method         = self::get_user_2fa_method( $user_id );
		$show_method_switcher    = count( $allowed_methods ) > 1;
		$show_qr_setup           = in_array( 'app', $allowed_methods, true ) && ! $has_app_secret;
		$show_email_panel        = in_array( 'email', $allowed_methods, true ) && ( 'email' === $selected_method || ( $user_2fa_setup_complete && 'email' === $selected_method ) );
		$code_label_email        = __( 'Enter the 6-digit code sent to your email.', 'security-ninja' );
		$code_label_app          = __( 'Enter the 6-digit code from your authentication app.', 'security-ninja' );

		$my_options      = self::get_options();
		$custom_intro    = isset( $my_options['2fa_intro'] ) ? trim( (string) $my_options['2fa_intro'] ) : '';
		$custom_enter_code = isset( $my_options['2fa_enter_code'] ) ? trim( (string) $my_options['2fa_enter_code'] ) : '';
		if ( '' !== $custom_enter_code ) {
			$code_label_email = $custom_enter_code;
			$code_label_app   = $custom_enter_code;
		}

		?>
		<script>
			// Enhanced 2FA configuration with error handling
			(function() {
				'use strict';

				window.two_factor_auth = {
					ajaxurl: '<?php echo esc_js( admin_url( 'admin-ajax.php' ) ); ?>',
					temp_token: '<?php echo esc_js( $temp_token ); ?>',
					fallback_admin_url: '<?php echo esc_js( esc_url( admin_url() ) ); ?>',
					qr_nonce: '<?php echo esc_js( $qr_nonce ); ?>',
					user_id: '<?php echo esc_js( $user_id ); ?>',
					user_ip: '<?php echo esc_js( isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : '' ); ?>',
					user_agent: '<?php echo esc_js( isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : '' ); ?>',
					verify_nonce: '<?php echo esc_js( wp_create_nonce( 'secnin_verify_2fa_' . $user_id ) ); ?>',
					allowed_methods: <?php echo wp_json_encode( array_values( $allowed_methods ) ); ?>,
					selected_method: '<?php echo esc_js( $selected_method ); ?>',
					setup_complete: <?php echo $user_2fa_setup_complete ? 'true' : 'false'; ?>,
					has_app_secret: <?php echo $has_app_secret ? 'true' : 'false'; ?>,
					messages: {
						verifying: '<?php echo esc_js( __( 'Checking code...', 'security-ninja' ) ); ?>',
						network_error: '<?php echo esc_js( __( 'Network error. Please check your connection and try again.', 'security-ninja' ) ); ?>',
						qr_error: '<?php echo esc_js( __( 'Error generating QR code. Please try again.', 'security-ninja' ) ); ?>',
						email_error: '<?php echo esc_js( __( 'Error sending email. Please try again.', 'security-ninja' ) ); ?>',
						sending_email: '<?php echo esc_js( __( 'Sending email...', 'security-ninja' ) ); ?>',
						unknown_error: '<?php echo esc_js( __( 'An unknown error occurred. Please try again.', 'security-ninja' ) ); ?>',
						code_label_app: '<?php echo esc_js( $code_label_app ); ?>',
						code_label_email: '<?php echo esc_js( $code_label_email ); ?>'
					}
				};

				if (!window.two_factor_auth.ajaxurl || !window.two_factor_auth.temp_token) {
					console.error('2FA: Critical configuration missing');
				}
			})();
		</script>
		<?php

		$script_path = plugin_dir_path( __FILE__ ) . 'js/two-factor-auth.js';
		$script_url  = plugins_url( '/js/two-factor-auth.js', __FILE__ );
		if ( is_readable( $script_path ) ) {
			$script_url = add_query_arg( 'ver', (string) filemtime( $script_path ), $script_url );
		}

		?>
		<!DOCTYPE html>
		<html <?php language_attributes(); ?>>

		<head>
			<meta http-equiv="Content-Type" content="<?php bloginfo( 'html_type' ); ?>; charset=<?php bloginfo( 'charset' ); ?>" />
			<title><?php esc_html_e( '2FA Verification', 'security-ninja' ); ?></title>
			<?php
			wp_enqueue_style( 'login' );
			wp_enqueue_script( 'jquery' );
			Wf_Sn::enqueue_sn_dialog();
			do_action( 'login_enqueue_scripts' );
			do_action( 'login_head' );
			wp_print_styles( array( 'sn-dialog' ) );
			wp_print_scripts( array( 'jquery', 'sn-dialog' ) );
			?>
			<style>
				.spinner {
					background: url(<?php echo esc_url( admin_url( 'images/spinner.gif' ) ); ?>) no-repeat;
					background-size: 20px 20px;
					display: none;
					float: right;
					opacity: 0.7;
					width: 20px;
					height: 20px;
					margin: 5px 5px 0;
				}

				.spinner.is-active {
					display: inline-block;
				}

				.qr-code {
					text-align: center;
					margin-bottom: 20px;
				}

				#qr-code-img {
					display: inline-block;
					max-width: 100%;
					height: auto;
				}

				.email-2fa {
					text-align: center;
					margin-bottom: 20px;
					padding: 20px;
					background: #f9f9f9;
					border-radius: 5px;
				}

				.errmsg {
					color: #dc3232;
					font-weight: bold;
				}

				.okmsg {
					color: #46b450;
					font-weight: bold;
				}

				#verify-2fa:disabled {
					opacity: 0.5;
					cursor: not-allowed;
				}

				.sn-2fa-method-switcher {
					text-align: center;
					margin-bottom: 20px;
				}

				.sn-2fa-method-switcher .button {
					margin: 0 5px;
				}

				.sn-2fa-method-switcher .button.is-active {
					background: #2271b1;
					border-color: #2271b1;
					color: #fff;
				}

				.sn-2fa-intro {
					text-align: center;
					margin: 0 0 16px;
					line-height: 1.5;
				}
			</style>
			<script src="<?php echo esc_url( $script_url ); ?>"></script>
		</head>

		<body class="login js login-action-2fa_verification wp-core-ui">
			<div id="login">

				<?php if ( '' !== $custom_intro ) : ?>
					<p class="sn-2fa-intro"><?php echo esc_html( $custom_intro ); ?></p>
				<?php endif; ?>

				<?php if ( $show_method_switcher ) : ?>
					<div class="sn-2fa-method-switcher">
						<?php if ( in_array( 'app', $allowed_methods, true ) ) : ?>
							<button type="button" class="button sn-2fa-method-btn <?php echo 'app' === $selected_method ? 'is-active' : ''; ?>" data-method="app">
								<?php esc_html_e( 'Authenticator app', 'security-ninja' ); ?>
							</button>
						<?php endif; ?>
						<?php if ( in_array( 'email', $allowed_methods, true ) ) : ?>
							<button type="button" class="button sn-2fa-method-btn <?php echo 'email' === $selected_method ? 'is-active' : ''; ?>" data-method="email">
								<?php esc_html_e( 'Email code', 'security-ninja' ); ?>
							</button>
						<?php endif; ?>
					</div>
				<?php endif; ?>

				<?php if ( $show_qr_setup ) : ?>
					<div class="qr-code" id="sn-2fa-app-panel" style="<?php echo ( $show_method_switcher && 'email' === $selected_method ) ? 'display:none;' : ''; ?>">
						<img id="qr-code-img"
							src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200'%3E%3Crect width='200' height='200' fill='%23ffffff' stroke='%23eee' stroke-width='2'/%3E%3C/svg%3E"
							alt="<?php esc_attr_e( 'QR Code Loading', 'security-ninja' ); ?>"
							style="display: inline-block; width: 200px; height: 200px; margin-bottom: 10px;" />

						<div id="secret-container" style="display: none; margin-top: 15px; padding: 15px; background: #f0f0f1; border-radius: 5px; border: 1px solid #ddd;">
							<p style="margin: 0 0 10px 0; font-weight: bold;">
								<label for="secret-key"><?php esc_html_e( 'Manual Entry Secret Key:', 'security-ninja' ); ?></label>
							</p>
							<input type="text" id="secret-key" readonly style="text-align: center; width: 100%; box-sizing: border-box; font-family: monospace; font-size: 14px; padding: 8px; border: 1px solid #ccc; border-radius: 3px; background: #fff;" />
						</div>
						<div id="qr-code-img-err"></div>
						<div class="spinner"></div>
						<button id="generate-qr" class="button button-secondary"><?php esc_html_e( 'Generate New QR Code', 'security-ninja' ); ?></button>
					</div>
				<?php endif; ?>

				<?php if ( in_array( 'email', $allowed_methods, true ) ) : ?>
					<div class="email-2fa" id="sn-2fa-email-panel" style="<?php echo ( $show_qr_setup && 'app' === $selected_method ) ? 'display:none;' : ( ! $show_email_panel && $user_2fa_setup_complete && 'app' === $selected_method ? 'display:none;' : '' ); ?>">
						<h3><?php esc_html_e( 'Email Verification', 'security-ninja' ); ?></h3>
						<p><?php esc_html_e( 'A verification code will be sent to your email address.', 'security-ninja' ); ?></p>
						<button id="send-email" class="button button-secondary"><?php esc_html_e( 'Send New Code', 'security-ninja' ); ?></button>
						<div id="email-status"></div>
					</div>
				<?php endif; ?>

				<form name="twofa-form-verify" id="twofa-form-verify" action="" method="post">
					<p>
						<label for="twofa-code" id="twofa-code-label">
							<?php echo esc_html( 'email' === $selected_method ? $code_label_email : $code_label_app ); ?>
						</label>
						<input type="text" name="twofa-code" id="twofa-code" class="input" value="" size="6" maxlength="6" pattern="\d{6}" inputmode="numeric" autocapitalize="off" autocomplete="off" data-1p-ignore required />
					</p>
					<div id="twofa-verify-msg"></div>
					<p class="submit">
						<input type="submit" name="wp-submit" id="verify-2fa" class="button button-primary button-large" value="<?php esc_attr_e( 'Verify Code', 'security-ninja' ); ?>" disabled />
					</p>

					<?php

					$grace_time_left = self::get_grace_period_time_left( $user->ID );

					if ( self::user_may_skip_2fa_setup_during_grace( $user->ID ) && $grace_time_left > 0 ) :
						$days_left  = floor( $grace_time_left / 86400 );
						$hours_left = floor( ( $grace_time_left % 86400 ) / 3600 );
						?>
						<p class="grace-period-notice">
							<?php
							printf(
								/* translators: %1$d: number of days left, %2$d: number of hours left */
								esc_html__( 'You have %1$d days and %2$d hours left to set up 2FA. ', 'security-ninja' ),
								esc_html( $days_left ),
								esc_html( $hours_left )
							);
							?>
							<a href="#" class="skip-2fa-link" data-temp-token="<?php echo esc_attr( $temp_token ); ?>">
								<?php esc_html_e( 'Skip for now', 'security-ninja' ); ?>
							</a>
						</p>
					<?php endif; ?>
				</form>

				<p id="backtoblog">
					<a href="<?php echo esc_url( home_url( '/' ) ); ?>">
						<?php
						/* translators: %s: Site title. */
						printf( esc_html__( '&larr; Back to %s', 'security-ninja' ), esc_html( get_bloginfo( 'title', 'display' ) ) );
						?>
					</a>
				</p>
			</div>

			<?php
			do_action( 'login_footer' );
			?>
			<div class="clear"></div>
			<script>
						document.addEventListener('DOMContentLoaded', function() {
							document.getElementById('twofa-code').focus();
				});



			</script>
		</body>

		</html>
		<?php
	}


	/**
	 * Collect authentication cookies for later removal.
	 *
	 * This method parses the provided cookie string and stores the authentication
	 * token if present. These tokens will be used later to invalidate sessions
	 * during the two-factor authentication process.
	 *
	 * @since 1.0.0
	 *
	 * @param string $cookie The authentication cookie string.
	 * @return void
	 */
	public static function collect_auth_cookie_tokens( $cookie ) {
		if ( ! is_string( $cookie ) || empty( $cookie ) ) {
			return;
		}

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


	/**
	 * Check if the user is using two-factor authentication.
	 *
	 * This method checks if a user has a 2FA secret key set in their user meta.
	 *
	 * @since 1.0.0
	 *
	 * @param int $user_id The ID of the user to check.
	 * @return bool True if the user has a 2FA secret key set, false otherwise.
	 */
	public static function is_user_using_two_factor( $user_id ) {
		if ( ! is_numeric( $user_id ) || $user_id <= 0 ) {
			return false;
		}

		$secret_key = get_user_meta( $user_id, 'secnin_2fa_secret', true );
		return ! empty( $secret_key );
	}

	/**
	 * Whether the user voluntarily enrolled in 2FA (opt-in).
	 *
	 * @param int $user_id User ID.
	 * @return bool
	 */
	public static function is_user_opted_in_to_2fa( $user_id ) {
		if ( ! is_numeric( $user_id ) || $user_id <= 0 ) {
			return false;
		}

		return '1' === get_user_meta( $user_id, 'secnin_2fa_optin', true );
	}

	/**
	 * Whether the user should be challenged for 2FA at login.
	 *
	 * @param int $user_id User ID.
	 * @return bool
	 */
	public static function user_requires_2fa_challenge( $user_id ) {
		if ( ! is_numeric( $user_id ) || $user_id <= 0 ) {
			return false;
		}

		$my_options = self::get_options();
		if ( empty( $my_options['2fa_enabled'] ) ) {
			return false;
		}

		$bypass_2fa = get_user_meta( $user_id, 'bypass_2fa', true );
		if ( '1' === $bypass_2fa ) {
			return false;
		}

		if ( self::should_user_use_two_factor( $user_id ) ) {
			return true;
		}

		if ( self::is_user_opted_in_to_2fa( $user_id ) ) {
			return true;
		}

		// Legacy enrollments (app or email): completed setup without opt-in meta.
		if ( self::is_2fa_setup_complete( $user_id ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Seconds remaining in the 2FA grace period for a role-required user (0 when inactive or expired).
	 *
	 * @param int $user_id User ID.
	 * @return int
	 */
	public static function get_grace_period_time_left( $user_id ) {
		if ( ! self::should_user_use_two_factor( $user_id ) ) {
			return 0;
		}

		$my_options        = self::get_options();
		$grace_period      = isset( $my_options['2fa_grace_period'] ) ? (int) $my_options['2fa_grace_period'] : 0;
		$enabled_timestamp = isset( $my_options['2fa_enabled_timestamp'] ) ? (int) $my_options['2fa_enabled_timestamp'] : 0;

		if ( $grace_period <= 0 || $enabled_timestamp <= 0 ) {
			return 0;
		}

		return max( 0, ( $enabled_timestamp + ( $grace_period * DAY_IN_SECONDS ) ) - time() );
	}

	/**
	 * Whether a role-required user who has not finished 2FA setup may skip during the grace period.
	 *
	 * @param int $user_id User ID.
	 * @return bool
	 */
	public static function user_may_skip_2fa_setup_during_grace( $user_id ) {
		if ( ! is_numeric( $user_id ) || $user_id <= 0 ) {
			return false;
		}

		$my_options = self::get_options();
		if ( empty( $my_options['2fa_enabled'] ) ) {
			return false;
		}

		if ( ! self::should_user_use_two_factor( $user_id ) ) {
			return false;
		}

		if ( self::is_user_opted_in_to_2fa( $user_id ) ) {
			return false;
		}

		if ( self::is_2fa_setup_complete( $user_id ) ) {
			return false;
		}

		$grace_period = isset( $my_options['2fa_grace_period'] ) ? (int) $my_options['2fa_grace_period'] : 0;
		if ( $grace_period <= 0 ) {
			return false;
		}

		return self::get_grace_period_time_left( $user_id ) > 0;
	}

	/**
	 * Check if the user should be using two-factor authentication (role enforcement).
	 *
	 * This method determines whether a user should be required to use 2FA based on their
	 * bypass status and role.
	 *
	 * @since 1.0.0
	 *
	 * @param int $user_id The ID of the user to check.
	 * @return bool True if the user should use 2FA, false otherwise.
	 */
	public static function should_user_use_two_factor( $user_id ) {
		if ( ! is_numeric( $user_id ) || $user_id <= 0 ) {
			return false;
		}

		$bypass_2fa = get_user_meta( $user_id, 'bypass_2fa', true );
		if ( '1' === $bypass_2fa ) {
			return false;
		}

		$user = get_userdata( $user_id );
		if ( ! $user || ! $user instanceof \WP_User ) {
			return false;
		}

		$my_options = self::get_options();
		$two_fa_on  = ! empty( $my_options['2fa_enabled'] );
		$required_roles = \WPSecurityNinja\Plugin\wf_sn_cf::normalize_2fa_required_roles(
			isset( $my_options['2fa_required_roles'] ) ? $my_options['2fa_required_roles'] : array(),
			$two_fa_on
		);

		return ! empty( array_intersect( $required_roles, $user->roles ) );
	}

	/**
	 * Handles session management for 2FA authentication.
	 *
	 * @since 1.0.0
	 * @param WP_User $user User object.
	 * @param bool    $is_skipping Whether this is a skip 2FA operation.
	 * @return void
	 */
	private static function handle_2fa_session( $user, $is_skipping = false ) {
		if ( ! $user instanceof \WP_User ) {
			return;
		}

		if ( $is_skipping ) {
			// For skipping, do nothing - let the normal login flow continue
			return;
		} else {
			// For regular 2FA flow, use full logout
			wp_logout();
			// Restore user context for 2FA
			wp_set_current_user( $user->ID );
		}
	}

	public function ajax_skip_2fa() {
		try {
			if ( empty( $_POST['temp_token'] ) ) {
				throw new \Exception( __( 'Missing temp_token', 'security-ninja' ) );
			}

			$temp_token = sanitize_text_field( $_POST['temp_token'] );

			// No nonce needed - temp_token provides security
			$session_data = self::validate_temp_token( $temp_token );

			if ( ! $session_data ) {
				throw new \Exception( __( 'Invalid or expired session', 'security-ninja' ) );
			}

			$user_id = $session_data['user_id'];

			$user = get_user_by( 'ID', $user_id );
			if ( ! $user ) {
				throw new \Exception( __( 'User not found', 'security-ninja' ) );
			}

			if ( ! self::user_may_skip_2fa_setup_during_grace( $user_id ) ) {
				throw new \Exception( __( 'Skipping 2FA setup is not allowed.', 'security-ninja' ) );
			}

			// Set up proper login session
			wp_set_current_user( $user_id, $user->user_login );
			wp_set_auth_cookie( $user_id, true );

			// Remove our wp_login hook temporarily to avoid recursion
			remove_action( 'wp_login', array( __CLASS__, 'wp_login' ), 10 );

			// Fire wp_login action to ensure all login hooks run
			do_action( 'wp_login', $user->user_login, $user );

			// Re-add our hook
			add_action( 'wp_login', array( __CLASS__, 'wp_login' ), 10, 2 );

			// Remove the temporary tokens
			delete_transient( 'secnin_2fa_temp_' . $temp_token );
			delete_transient( 'secnin_2fa_temp_secret_' . $temp_token );

			// Get stored redirect URL from session data
			$stored_redirect_to = isset( $session_data['redirect_to'] ) ? $session_data['redirect_to'] : '';
			// Get proper redirect URL based on user capabilities
			$redirect_to = self::get_redirect_url( $user, $stored_redirect_to );

			$time_left = self::get_grace_period_time_left( $user_id );

			wf_sn_el_modules::log_event(
				'security_ninja',
				'2fa_skipped',
				sprintf(
					/* translators: %1$d: number of days left, %2$d: number of hours left */
					__( 'User skipped 2FA setup. Time left to activate: %1$d days and %2$d hours.', 'security-ninja' ),
					floor( $time_left / 86400 ),
					floor( ( $time_left % 86400 ) / 3600 )
				)
			);
			wp_send_json_success(
				array(
					'message'      => __( 'Continuing to dashboard...', 'security-ninja' ),
					'redirect_url' => self::ensure_safe_redirect_url_for_response( $redirect_to ),
				)
			);

		} catch ( \Exception $e ) {
			self::rethrow_ajax_die_exception( $e );
			wp_send_json_error( array( 'message' => $e->getMessage() ) );
		}
	}

	/**
	 * Create a secure temp token with session binding
	 *
	 * @param int $user_id User ID
	 * @return string Secure temp token
	 */
	private static function create_secure_temp_token( $user_id, $redirect_to = '' ) {
		$base_token = wp_generate_password( 32, false );

		if ( empty( $redirect_to ) ) {
			$redirect_to = isset( $_REQUEST['redirect_to'] ) ? esc_url_raw( $_REQUEST['redirect_to'] ) : '';
			if ( empty( $redirect_to ) && isset( $_GET['redirect_to'] ) ) {
				$redirect_to = esc_url_raw( $_GET['redirect_to'] );
			}
			if ( empty( $redirect_to ) && isset( $_POST['redirect_to'] ) ) {
				$redirect_to = esc_url_raw( $_POST['redirect_to'] );
			}
			if ( empty( $redirect_to ) && isset( $_REQUEST['redirect'] ) ) {
				$redirect_to = esc_url_raw( $_REQUEST['redirect'] );
			}
			if ( empty( $redirect_to ) && isset( $_POST['redirect'] ) ) {
				$redirect_to = esc_url_raw( $_POST['redirect'] );
			}
		}

		$session_data = array(
			'user_id'      => $user_id,
			'ip'           => self::get_client_ip(),
			'user_agent'   => substr( $_SERVER['HTTP_USER_AGENT'] ?? '', 0, 255 ),
			'created'      => time(),
			'attempts'     => 0,
			'max_attempts' => 5,
			'redirect_to'  => $redirect_to,
		);

		// Store with shorter expiry for security
		set_transient( 'secnin_2fa_temp_' . $base_token, $session_data, 15 * MINUTE_IN_SECONDS );

		return $base_token;
	}

	/**
	 * Validate temp token with security checks
	 *
	 * @param string $temp_token Token to validate
	 * @return array|false Session data if valid, false if invalid
	 */
	private static function validate_temp_token( $temp_token ) {
		if ( empty( $temp_token ) ) {
			return false;
		}

		$session_data = get_transient( 'secnin_2fa_temp_' . $temp_token );
		if ( ! $session_data || ! is_array( $session_data ) ) {
			return false;
		}

		// Check IP address
		$current_ip = self::get_client_ip();
		if ( $session_data['ip'] !== $current_ip ) {
			wf_sn_el_modules::log_event(
				'security_ninja',
				'2fa_token_ip_mismatch',
				sprintf(
					/* translators: %1$s: Original IP address, %2$s: Current IP address */
					__( '2FA token used from different IP. Original: %1$s, Current: %2$s', 'security-ninja' ),
					$session_data['ip'],
					$current_ip
				)
			);
			delete_transient( 'secnin_2fa_temp_' . $temp_token );
			return false;
		}

		// Check user agent
		$current_ua = substr( $_SERVER['HTTP_USER_AGENT'] ?? '', 0, 255 );
		if ( $session_data['user_agent'] !== $current_ua ) {
			wf_sn_el_modules::log_event(
				'security_ninja',
				'2fa_token_ua_mismatch',
				__( '2FA token used from different user agent', 'security-ninja' )
			);
			delete_transient( 'secnin_2fa_temp_' . $temp_token );
			return false;
		}

		// Check rate limiting
		if ( $session_data['attempts'] >= $session_data['max_attempts'] ) {
			wf_sn_el_modules::log_event(
				'security_ninja',
				'2fa_token_rate_limited',
				/* translators: %d: number of attempts */
				sprintf( __( '2FA token rate limited after %d attempts', 'security-ninja' ), $session_data['attempts'] )
			);
			delete_transient( 'secnin_2fa_temp_' . $temp_token );
			return false;
		}

		// Check age (additional security - tokens should be short-lived)
		if ( ( time() - $session_data['created'] ) > ( 15 * MINUTE_IN_SECONDS ) ) {
			delete_transient( 'secnin_2fa_temp_' . $temp_token );
			return false;
		}

		return $session_data;
	}

	/**
	 * Increment attempt counter for temp token
	 *
	 * @param string $temp_token Token to increment attempts for
	 * @param array $session_data Current session data
	 */
	private static function increment_token_attempts( $temp_token, $session_data ) {
		++$session_data['attempts'];
		set_transient( 'secnin_2fa_temp_' . $temp_token, $session_data, 15 * MINUTE_IN_SECONDS );
	}

	/**
	 * Get appropriate redirect URL for user after 2FA verification
	 *
	 * Matches WordPress core behavior (wp-login.php): validates relative and absolute
	 * destinations via wp_validate_redirect(), applies the login_redirect filter, and
	 * then applies core's capability-based fallback so users whose role cannot access
	 * the dashboard are not sent to wp-admin (where WordPress would bounce them back
	 * out - which, with Rename Login active, can leave them on the front page appearing
	 * logged out).
	 *
	 * @param WP_User $user User object
	 * @param string  $stored_redirect_to Stored redirect URL from session
	 * @return string Redirect URL
	 */
	private static function get_redirect_url( $user, $stored_redirect_to = '' ) {
		$default = admin_url();

		$stored_redirect_to = is_string( $stored_redirect_to ) ? trim( $stored_redirect_to ) : '';

		$redirect_to = '' === $stored_redirect_to
			? $default
			: wp_validate_redirect( $stored_redirect_to, $default );

		$redirect_to = apply_filters( 'login_redirect', $redirect_to, $stored_redirect_to, $user );

		// Mirror WordPress core's post-filter capability handling (wp-login.php). Only
		// adjusts when the destination is the default dashboard URL, so an explicit
		// requested redirect or a custom login_redirect target is preserved.
		if ( $user instanceof \WP_User && ( empty( $redirect_to ) || admin_url() === $redirect_to || 'wp-admin/' === $redirect_to ) ) {
			if ( is_multisite() && ! get_active_blog_for_user( $user->ID ) && ! is_super_admin( $user->ID ) ) {
				$redirect_to = user_admin_url();
			} elseif ( is_multisite() && ! $user->has_cap( 'read' ) ) {
				$redirect_to = get_dashboard_url( $user->ID );
			} elseif ( ! $user->has_cap( 'edit_posts' ) ) {
				$redirect_to = $user->has_cap( 'read' ) ? admin_url( 'profile.php' ) : home_url();
			}
		}

		return $redirect_to;
	}

	/**
	 * Ensure a URL is safe and non-empty for JSON client redirects (admin_url fallback).
	 *
	 * @param string $url Redirect URL from get_redirect_url() / filters.
	 * @return string Escaped absolute URL.
	 */
	private static function ensure_safe_redirect_url_for_response( $url ) {
		$fallback = esc_url( admin_url() );
		if ( empty( $url ) || ! is_string( $url ) ) {
			return $fallback;
		}
		$out = esc_url( $url );
		return '' !== $out ? $out : $fallback;
	}

	/**
	 * Get client IP address with proxy support
	 *
	 * @return string Client IP address
	 */
	private static function get_client_ip() {
		$ip_headers = array(
			'HTTP_X_FORWARDED_FOR',
			'HTTP_X_REAL_IP',
			'HTTP_CLIENT_IP',
			'REMOTE_ADDR',
		);

		foreach ( $ip_headers as $header ) {
			if ( ! empty( $_SERVER[ $header ] ) ) {
				$ips = explode( ',', $_SERVER[ $header ] );
				$ip  = trim( $ips[0] );
				if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) {
					return $ip;
				}
			}
		}

		return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
	}

	/**
	 * Create a short-lived nonce for sensitive 2FA operations
	 *
	 * @param string $action Nonce action
	 * @return string Nonce value
	 */
	private static function create_short_nonce( $action ) {
		// Create a more secure nonce that's only valid for 5 minutes
		$time = ceil( time() / 300 ); // 5-minute windows
		return wp_hash( $action . '|' . $time . '|' . get_current_user_id(), 'nonce' );
	}

	/**
	 * Verify short-lived nonce
	 *
	 * @param string $nonce Nonce to verify
	 * @param string $action Nonce action
	 * @return bool True if valid, false otherwise
	 */
	private static function verify_short_nonce( $nonce, $action ) {
		$time = ceil( time() / 300 ); // Current 5-minute window

		// Check current and previous window (10 minutes total)
		for ( $i = 0; $i <= 1; $i++ ) {
			$expected = wp_hash( $action . '|' . ( $time - $i ) . '|' . get_current_user_id(), 'nonce' );
			if ( hash_equals( $expected, $nonce ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Generate a secure 6-digit code for email 2FA
	 *
	 * @param int $user_id User ID
	 * @return string 6-digit code
	 */
	private static function generate_email_code( $user_id ) {
		// Generate a secure 6-digit code
		$code = sprintf( '%06d', wp_rand( 0, 999999 ) );

		// Store the code with user ID and expiry
		$code_data = array(
			'user_id'    => $user_id,
			'code'       => $code,
			'created'    => time(),
			'expires'    => time() + ( 15 * MINUTE_IN_SECONDS ), // 15 minutes
			'ip'         => self::get_client_ip(),
			'user_agent' => substr( $_SERVER['HTTP_USER_AGENT'] ?? '', 0, 255 ),
		);

		// Store with 15-minute expiry
		set_transient( 'secnin_2fa_email_' . $user_id, $code_data, 15 * MINUTE_IN_SECONDS );

		return $code;
	}

	/**
	 * Send 2FA email with code
	 *
	 * @param int $user_id User ID
	 * @param string $code 6-digit code
	 * @return bool True if email sent successfully, false otherwise
	 */
	private static function send_2fa_email( $user_id, $code ) {
		$user = get_userdata( $user_id );
		if ( ! $user ) {
			return false;
		}

		$site_name         = get_bloginfo( 'name' );
		$site_url          = home_url();
		$user_display_name = $user->display_name;
		$user_email        = $user->user_email;

		$site_domain = class_exists( __NAMESPACE__ . '\wf_sn_cf' )
			? wf_sn_cf::url_to_domain( site_url() )
			: (string) wp_parse_url( site_url(), PHP_URL_HOST );

		$subject = sprintf(
			/* translators: %s: Site name */
			__( 'Your 2FA code for %s', 'security-ninja' ),
			$site_name
		);

		$emailintrotext = sprintf(
			/* translators: %s: Site name */
			__( 'Two-Factor Authentication Code for %s', 'security-ninja' ),
			$site_name
		);

		$body  = '<p>' . sprintf(
			/* translators: %s: User display name */
			esc_html__( 'Hello %s,', 'security-ninja' ),
			esc_html( $user_display_name )
		) . '</p>';
		$body .= '<div style="background: #f5f5f5; padding: 20px; border-radius: 5px; text-align: center; margin: 20px 0;">';
		$body .= '<h1 style="font-size: 32px; letter-spacing: 5px; color: #333; margin: 0;">' . esc_html( $code ) . '</h1>';
		$body .= '</div>';
		$body .= '<p><strong>' . esc_html__( 'This code will expire in 15 minutes.', 'security-ninja' ) . '</strong></p>';
		$body .= '<ul>';
		$body .= '<li>' . sprintf(
			/* translators: %s: Site URL */
			esc_html__( 'Site: %s', 'security-ninja' ),
			esc_html( $site_url )
		) . '</li>';
		$body .= '<li>' . sprintf(
			/* translators: %s: User email */
			esc_html__( 'Email: %s', 'security-ninja' ),
			esc_html( $user_email )
		) . '</li>';
		$body .= '<li>' . sprintf(
			/* translators: %s: Timestamp */
			esc_html__( 'Time: %s', 'security-ninja' ),
			esc_html( date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), current_time( 'timestamp' ) ) )
		) . '</li>';
		$body .= '</ul>';
		$body .= '<p><em>' . esc_html__( 'If you did not request this code, please ignore this email and contact your administrator immediately.', 'security-ninja' ) . '</em></p>';

		$dashboardlink       = admin_url( '?page=wf-sn' );
		$dashboardlinkanchor = __( 'Security Settings', 'security-ninja' );

		$my_replacements = array(
			'%%emailintrotext%%'      => $emailintrotext,
			'%%websitedomain%%'       => site_url(),
			'%%dashboardlink%%'       => $dashboardlink,
			'%%dashboardlinkanchor%%' => $dashboardlinkanchor,
			'%%secninlogourl%%'       => WF_SN_PLUGIN_URL . 'images/security-ninja-logo.png',
			'%%emailtitle%%'          => $subject,
			'%%sentfromtext%%'        => sprintf(
				/* translators: %s: Site domain */
				__( 'This email was sent from %s', 'security-ninja' ),
				$site_domain
			),
			'%%emailcontent%%'        => nl2br( $body ),
		);

		if ( secnin_fs()->can_use_premium_code__premium_only() ) {
			if ( class_exists( __NAMESPACE__ . '\Wf_Sn_Wl' ) ) {
				if ( Wf_Sn_Wl::is_active() ) {
					$pluginname                             = Wf_Sn_Wl::get_new_name();
					$my_replacements['%%secninlogourl%%']   = esc_url( Wf_Sn_Wl::get_email_logo_url() );
					$my_replacements['%%sentfromtext%%']    = sprintf(
						/* translators: 1: Plugin name, 2: Site domain */
						__( 'This email was sent by %1$s from %2$s', 'security-ninja' ),
						esc_html( $pluginname ),
						esc_html( $site_domain )
					);
				}
			}
		}

		$template_path = WF_SN_PLUGIN_DIR . 'includes/email-default.php';

		$html = Wf_Sn_Security_Utils::secure_file_get_contents(
			$template_path,
			array(
				'check_readable'     => true,
				'check_size'         => true,
				'max_size'           => 1024 * 1024, // 1MB for templates
				'check_type'         => true,
				'allowed_extensions' => array( 'php' ),
				'check_mime'         => false,
				'check_path'         => true,
				'allowed_dirs'       => array( WF_SN_PLUGIN_DIR ),
			)
		);

		if ( false === $html ) {
			$html = '<p>Email template not found.</p>';
		}

		foreach ( $my_replacements as $needle => $replacement ) {
			$html = str_replace( $needle, $replacement, $html );
		}

		$headers = array( 'Content-Type: text/html; charset=UTF-8' );

		add_filter( 'wp_mail_content_type', array( __CLASS__, 'set_html_content_type' ) );
		$sent = wp_mail( $user_email, $subject, $html, $headers );
		remove_filter( 'wp_mail_content_type', array( __CLASS__, 'set_html_content_type' ) );

		if ( $sent ) {
			wf_sn_el_modules::log_event(
				'security_ninja',
				'2fa_email_sent',
				sprintf(
					/* translators: %s: User email */
					__( '2FA email code sent to %s', 'security-ninja' ),
					$user_email
				)
			);
		} else {
			wf_sn_el_modules::log_event(
				'security_ninja',
				'2fa_email_failed',
				sprintf(
					/* translators: %s: User email */
					__( 'Failed to send 2FA email code to %s', 'security-ninja' ),
					$user_email
				)
			);
		}

		return $sent;
	}

	/**
	 * Set HTML content type for emails
	 *
	 * @return string
	 */
	public static function set_html_content_type() {
		return 'text/html';
	}

	/**
	 * Validate email 2FA code
	 *
	 * @param int $user_id User ID
	 * @param string $code Submitted code
	 * @return bool True if valid, false otherwise
	 */
	private static function validate_email_code( $user_id, $code ) {
		$code_data = get_transient( 'secnin_2fa_email_' . $user_id );

		if ( ! $code_data || ! is_array( $code_data ) ) {
			return false;
		}

		// Check if code matches
		if ( $code_data['code'] !== $code ) {
			return false;
		}

		// Check if expired
		if ( time() > $code_data['expires'] ) {
			delete_transient( 'secnin_2fa_email_' . $user_id );
			return false;
		}

		// Check IP address
		$current_ip = self::get_client_ip();
		if ( $code_data['ip'] !== $current_ip ) {
			wf_sn_el_modules::log_event(
				'security_ninja',
				'2fa_email_ip_mismatch',
				sprintf(
					/* translators: %1$s: Original IP address, %2$s: Current IP address */
					__( '2FA email code used from different IP. Original: %1$s, Current: %2$s', 'security-ninja' ),
					$code_data['ip'],
					$current_ip
				)
			);
			delete_transient( 'secnin_2fa_email_' . $user_id );
			return false;
		}

		// Check user agent
		$current_ua = substr( $_SERVER['HTTP_USER_AGENT'] ?? '', 0, 255 );
		if ( $code_data['user_agent'] !== $current_ua ) {
			wf_sn_el_modules::log_event(
				'security_ninja',
				'2fa_email_ua_mismatch',
				__( '2FA email code used from different user agent', 'security-ninja' )
			);
			delete_transient( 'secnin_2fa_email_' . $user_id );
			return false;
		}

		// Code is valid - clean up
		delete_transient( 'secnin_2fa_email_' . $user_id );
		return true;
	}

	/**
	 * Get allowed 2FA methods for the site.
	 *
	 * @return string[] 'app' and/or 'email'.
	 */
	public static function get_allowed_methods() {
		$options = self::get_options();
		$methods = isset( $options['2fa_methods'] ) ? $options['2fa_methods'] : array( 'app' );

		if ( class_exists( __NAMESPACE__ . '\\wf_sn_cf' ) ) {
			return \WPSecurityNinja\Plugin\wf_sn_cf::normalize_2fa_methods( $methods );
		}

		if ( is_string( $methods ) && '' !== $methods ) {
			return array( $methods );
		}
		if ( ! is_array( $methods ) || empty( $methods ) ) {
			return array( 'app' );
		}

		return array_values( array_unique( $methods ) );
	}

	/**
	 * Get the user's preferred 2FA method, falling back to the first allowed method.
	 *
	 * @param int $user_id User ID.
	 * @return string 'app' or 'email'.
	 */
	public static function get_user_2fa_method( $user_id ) {
		$allowed = self::get_allowed_methods();
		$pref    = get_user_meta( $user_id, 'secnin_2fa_method', true );

		if ( in_array( $pref, $allowed, true ) ) {
			return $pref;
		}

		return $allowed[0];
	}

	/**
	 * Resolve the verification method from POST or user preference.
	 *
	 * @param int $user_id User ID.
	 * @return string 'app' or 'email'.
	 */
	public static function resolve_verification_method( $user_id ) {
		$allowed = self::get_allowed_methods();

		if ( ! empty( $_POST['method'] ) ) {
			$posted = sanitize_text_field( wp_unslash( $_POST['method'] ) );
			if ( in_array( $posted, $allowed, true ) ) {
				return $posted;
			}
		}

		return self::get_user_2fa_method( $user_id );
	}

	/**
	 * Get 2FA method for user/site (legacy shim — first allowed method).
	 *
	 * @return string 'app' or 'email'
	 */
	private static function get_2fa_method() {
		$allowed = self::get_allowed_methods();
		return $allowed[0];
	}

	/**
	 * AJAX handler for sending 2FA email
	 */
	public function ajax_send_2fa_email() {
		try {
			if ( empty( $_POST['temp_token'] ) ) {
				throw new \Exception( __( 'Missing temp_token', 'security-ninja' ) );
			}

			$temp_token   = sanitize_text_field( $_POST['temp_token'] );
			$session_data = self::validate_temp_token( $temp_token );

			if ( ! $session_data ) {
				throw new \Exception( __( 'Invalid or expired session', 'security-ninja' ) );
			}

			$user_id = $session_data['user_id'];

			// Check if email method is enabled
			if ( ! in_array( 'email', self::get_allowed_methods(), true ) ) {
				throw new \Exception( __( 'Email 2FA is not enabled', 'security-ninja' ) );
			}

			// Check for rate limiting on email sending (prevent multiple emails within 30 seconds)
			$email_rate_limit_key = 'secnin_2fa_email_rate_' . $user_id;
			$last_email_time      = get_transient( $email_rate_limit_key );

			if ( $last_email_time && ( time() - $last_email_time ) < 30 ) {
				$time_left = 30 - ( time() - $last_email_time );
				throw new \Exception(
					sprintf(
						/* translators: %d: seconds remaining */
						__( 'Please wait %d seconds before requesting another email code.', 'security-ninja' ),
						$time_left
					)
				);
			}

			// Check if there's already a valid email code that hasn't expired
			$existing_code_data = get_transient( 'secnin_2fa_email_' . $user_id );
			if ( $existing_code_data && is_array( $existing_code_data ) && time() < $existing_code_data['expires'] ) {
				// Resend the existing code instead of generating a new one
				$code = $existing_code_data['code'];
				$sent = self::send_2fa_email( $user_id, $code );
			} else {
				// Generate and send new email code
				$code = self::generate_email_code( $user_id );
				$sent = self::send_2fa_email( $user_id, $code );
			}

			if ( ! $sent ) {
				throw new \Exception( __( 'Failed to send email. Please try again.', 'security-ninja' ) );
			}

			// Set rate limiting for email sending
			set_transient( $email_rate_limit_key, time(), 30 );

			// Increment attempt counter for rate limiting
			self::increment_token_attempts( $temp_token, $session_data );

			wp_send_json_success(
				array(
					'message' => __( 'Email sent successfully. Please check your inbox.', 'security-ninja' ),
				)
			);

		} catch ( \Exception $e ) {
			self::rethrow_ajax_die_exception( $e );
			wp_send_json_error( array( 'message' => $e->getMessage() ) );
		}
	}

	/**
	 * AJAX handler for admin QR code generation
	 */
	public function ajax_admin_generate_qr() {
		try {
			// Verify nonce and permissions
			if ( ! isset( $_POST['nonce'] ) || ! isset( $_POST['user_id'] ) ) {
				throw new \Exception( __( 'Missing required parameters', 'security-ninja' ) );
			}

			$user_id = intval( $_POST['user_id'] );
			$nonce   = sanitize_text_field( $_POST['nonce'] );

			if ( ! wp_verify_nonce( $nonce, 'admin_2fa_setup_' . $user_id ) ) {
				throw new \Exception( __( 'Invalid nonce', 'security-ninja' ) );
			}

			if ( ! current_user_can( 'edit_user', $user_id ) ) {
				throw new \Exception( __( 'Insufficient permissions', 'security-ninja' ) );
			}

			$user = get_user_by( 'ID', $user_id );
			if ( ! $user ) {
				throw new \Exception( __( 'User not found', 'security-ninja' ) );
			}

			// Initialize manager
			$manager = $this->init_manager();

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

			// Store temporary secret for this admin session
			$temp_key = 'admin_2fa_temp_secret_' . get_current_user_id() . '_' . $user_id;
			if ( ! set_transient( $temp_key, $secret, 30 * MINUTE_IN_SECONDS ) ) {
				throw new \Exception( __( 'Failed to store secret', 'security-ninja' ) );
			}

			// Get site info for the label
			$site_url = Wf_Sn_Vu::remove_http( site_url() );

			// Create the otpauth URI
			// Format: site_url:username (e.g., "example.com:john")
			$label   = rawurlencode( $site_url );
			$issuer  = rawurlencode( $site_url );
			$account = rawurlencode( $user->user_login );

			$uri = sprintf(
				'otpauth://totp/%s:%s?secret=%s&issuer=%s',
				$label,
				$account,
				$secret,
				$issuer
			);

			// Generate QR code
			$options = new QROptions(
				array(
					'outputType'  => QRCode::OUTPUT_MARKUP_SVG,
					'imageBase64' => true,
				)
			);

			$qrcode           = new QRCode( $options );
			$qr_code_data_uri = $qrcode->render( $uri );

			wp_send_json_success(
				array(
					'qr_code' => $qr_code_data_uri,
					'secret'  => $secret,
					'message' => __( 'QR code generated successfully', 'security-ninja' ),
				)
			);

		} catch ( \Exception $e ) {
			self::rethrow_ajax_die_exception( $e );
			wp_send_json_error( array( 'message' => $e->getMessage() ) );
		}
	}

	/**
	 * AJAX handler for admin 2FA verification
	 */
	public function ajax_admin_verify_2fa() {
		try {
			// Verify nonce and permissions
			if ( ! isset( $_POST['nonce'] ) || ! isset( $_POST['user_id'] ) || ! isset( $_POST['code'] ) ) {
				throw new \Exception( __( 'Missing required parameters', 'security-ninja' ) );
			}

			$user_id = intval( $_POST['user_id'] );
			$nonce   = sanitize_text_field( $_POST['nonce'] );
			$code    = sanitize_text_field( $_POST['code'] );

			if ( ! wp_verify_nonce( $nonce, 'admin_2fa_setup_' . $user_id ) ) {
				throw new \Exception( __( 'Invalid nonce', 'security-ninja' ) );
			}

			if ( ! current_user_can( 'edit_user', $user_id ) ) {
				throw new \Exception( __( 'Insufficient permissions', 'security-ninja' ) );
			}

			$user = get_user_by( 'ID', $user_id );
			if ( ! $user ) {
				throw new \Exception( __( 'User not found', 'security-ninja' ) );
			}

			// Get temporary secret
			$temp_key = 'admin_2fa_temp_secret_' . get_current_user_id() . '_' . $user_id;
			$secret   = get_transient( $temp_key );

			if ( ! $secret ) {
				throw new \Exception( __( 'No QR code generated. Please generate a new QR code first.', 'security-ninja' ) );
			}

			// Initialize manager and verify code
			$manager  = $this->init_manager();
			$is_valid = $manager->verify( $code, $secret );

			if ( $is_valid ) {
				// Store the secret permanently
				$encrypted_secret = self::encrypt_secret( $secret, self::get_passphrase() );
				update_user_meta( $user_id, 'secnin_2fa_secret', $encrypted_secret );
				update_user_meta( $user_id, 'secnin_2fa_setup_complete', 1 );
				if ( ! self::should_user_use_two_factor( $user_id ) ) {
					update_user_meta( $user_id, 'secnin_2fa_optin', 1 );
				} else {
					delete_user_meta( $user_id, 'secnin_2fa_optin' );
				}
				update_user_meta( $user_id, 'secnin_2fa_method', 'app' );

				// Clean up temporary secret
				delete_transient( $temp_key );

				// Log the event
				wf_sn_el_modules::log_event(
					'security_ninja',
					'admin_2fa_setup_completed',
					sprintf(
						/* translators: %1$s: User display name, %2$d: User ID */
						__( 'Admin completed 2FA setup for user %1$s (ID: %2$d)', 'security-ninja' ),
						$user->display_name,
						$user_id
					)
				);

				wp_send_json_success(
					array(
						'message' => __( '2FA setup completed successfully!', 'security-ninja' ),
					)
				);
			} else {
				wp_send_json_error(
					array(
						'message' => __( 'Invalid code. Please try again.', 'security-ninja' ),
					)
				);
			}
		} catch ( \Exception $e ) {
			self::rethrow_ajax_die_exception( $e );
			wp_send_json_error( array( 'message' => $e->getMessage() ) );
		}
	}

	/**
	 * Re-throw PHPUnit AJAX die exceptions so success responses are not swallowed in tests.
	 *
	 * @param \Exception $e Caught exception.
	 * @return void
	 */
	private static function rethrow_ajax_die_exception( \Exception $e ) {
		if ( class_exists( 'WPAjaxDieContinueException', false ) && $e instanceof \WPAjaxDieContinueException ) {
			throw $e;
		}
		if ( class_exists( 'WPAjaxDieStopException', false ) && $e instanceof \WPAjaxDieStopException ) {
			throw $e;
		}
	}

	/**
	 * AJAX handler for admin 2FA reset
	 */
	public function ajax_admin_reset_2fa() {
		try {
			// Verify nonce and permissions
			if ( ! isset( $_POST['nonce'] ) || ! isset( $_POST['user_id'] ) ) {
				throw new \Exception( __( 'Missing required parameters', 'security-ninja' ) );
			}

			$user_id = intval( $_POST['user_id'] );
			$nonce   = sanitize_text_field( $_POST['nonce'] );

			if ( ! wp_verify_nonce( $nonce, 'admin_2fa_setup_' . $user_id ) ) {
				throw new \Exception( __( 'Invalid nonce', 'security-ninja' ) );
			}

			$is_self      = ( get_current_user_id() === $user_id );
			$should_force = self::should_user_use_two_factor( $user_id );
			$is_opted_in  = self::is_user_opted_in_to_2fa( $user_id );
			$can_reset    = current_user_can( 'edit_user', $user_id )
				|| ( $is_self && $is_opted_in && ! $should_force );

			if ( ! $can_reset ) {
				throw new \Exception( __( 'Insufficient permissions', 'security-ninja' ) );
			}

			if ( $should_force && $is_self && ! current_user_can( 'manage_options' ) ) {
				throw new \Exception( __( 'You cannot disable 2FA while your role requires it.', 'security-ninja' ) );
			}

			$user = get_user_by( 'ID', $user_id );
			if ( ! $user ) {
				throw new \Exception( __( 'User not found', 'security-ninja' ) );
			}

			// Remove 2FA data
			delete_user_meta( $user_id, 'secnin_2fa_secret' );
			delete_user_meta( $user_id, 'secnin_2fa_setup_complete' );
			delete_user_meta( $user_id, 'secnin_2fa_code_validated' );
			delete_user_meta( $user_id, 'secnin_2fa_session_validated' );
			delete_user_meta( $user_id, 'secnin_2fa_optin' );
			delete_user_meta( $user_id, 'secnin_2fa_method' );

			// Clean up any temporary secrets
			$temp_key = 'admin_2fa_temp_secret_' . get_current_user_id() . '_' . $user_id;
			delete_transient( $temp_key );

			// Log the event
			wf_sn_el_modules::log_event(
				'security_ninja',
				'admin_2fa_reset',
				sprintf(
					/* translators: %1$s: User display name, %2$d: User ID */
					__( 'Admin reset 2FA for user %1$s (ID: %2$d)', 'security-ninja' ),
					$user->display_name,
					$user_id
				)
			);

			wp_send_json_success(
				array(
					'message' => __( '2FA has been reset successfully.', 'security-ninja' ),
				)
			);

		} catch ( \Exception $e ) {
			self::rethrow_ajax_die_exception( $e );
			wp_send_json_error( array( 'message' => $e->getMessage() ) );
		}
	}

	/**
	 * AJAX handler for enrolling a user in email-based 2FA from the profile page.
	 */
	public function ajax_admin_enroll_email_2fa() {
		try {
			if ( ! isset( $_POST['nonce'] ) || ! isset( $_POST['user_id'] ) ) {
				throw new \Exception( __( 'Missing required parameters', 'security-ninja' ) );
			}

			$user_id = intval( $_POST['user_id'] );
			$nonce   = sanitize_text_field( $_POST['nonce'] );

			if ( ! wp_verify_nonce( $nonce, 'admin_2fa_setup_' . $user_id ) ) {
				throw new \Exception( __( 'Invalid nonce', 'security-ninja' ) );
			}

			$is_self   = ( get_current_user_id() === $user_id );
			$can_setup = current_user_can( 'edit_user', $user_id ) || $is_self;
			if ( ! $can_setup ) {
				throw new \Exception( __( 'Insufficient permissions', 'security-ninja' ) );
			}

			if ( ! in_array( 'email', self::get_allowed_methods(), true ) ) {
				throw new \Exception( __( 'Email 2FA is not enabled on this site.', 'security-ninja' ) );
			}

			$user = get_user_by( 'ID', $user_id );
			if ( ! $user ) {
				throw new \Exception( __( 'User not found', 'security-ninja' ) );
			}

			if ( ! self::should_user_use_two_factor( $user_id ) ) {
				update_user_meta( $user_id, 'secnin_2fa_optin', 1 );
			} else {
				delete_user_meta( $user_id, 'secnin_2fa_optin' );
			}
			update_user_meta( $user_id, 'secnin_2fa_method', 'email' );
			update_user_meta( $user_id, 'secnin_2fa_setup_complete', 1 );
			delete_user_meta( $user_id, 'secnin_2fa_secret' );

			wf_sn_el_modules::log_event(
				'security_ninja',
				'admin_2fa_email_enrolled',
				sprintf(
					/* translators: %1$s: User display name, %2$d: User ID */
					__( 'Email 2FA enabled for user %1$s (ID: %2$d)', 'security-ninja' ),
					$user->display_name,
					$user_id
				)
			);

			wp_send_json_success(
				array(
					'message' => __( 'Email 2FA enabled successfully!', 'security-ninja' ),
				)
			);
		} catch ( \Exception $e ) {
			self::rethrow_ajax_die_exception( $e );
			wp_send_json_error( array( 'message' => $e->getMessage() ) );
		}
	}
}

// Initialize the plugin
add_action(
	'plugins_loaded',
	function () {
		Wf_Sn_2fa::init();
	}
);