<?php
/**
 * Two-Factor Authentication for Security Pack.
 *
 * @package ArkHost_Security_Pack
 */

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

/**
 * Two-Factor Authentication class using TOTP.
 */
class ARKSP_Two_Factor {

	/**
	 * User meta key for 2FA secret.
	 *
	 * @var string
	 */
	const SECRET_META_KEY = '_arksp_2fa_secret';

	/**
	 * User meta key for 2FA enabled status.
	 *
	 * @var string
	 */
	const ENABLED_META_KEY = '_arksp_2fa_enabled';

	/**
	 * User meta key for backup codes.
	 *
	 * @var string
	 */
	const BACKUP_CODES_META_KEY = '_arksp_2fa_backup_codes';

	/**
	 * TOTP code length.
	 *
	 * @var int
	 */
	const CODE_LENGTH = 6;

	/**
	 * TOTP time step (30 seconds).
	 *
	 * @var int
	 */
	const TIME_STEP = 30;

	/**
	 * Constructor.
	 */
	public function __construct() {
		if ( ! ARKSP_Plugin::get_setting( 'two_factor_enabled', false ) ) {
			return;
		}

		// Intercept authentication to check for 2FA - runs before cookies are set.
		add_filter( 'authenticate', array( $this, 'check_2fa_on_authenticate' ), 100, 3 );

		// Handle the 2FA verification form.
		// Use login_form_arksp_2fa for standard wp-login.php.
		add_action( 'login_form_arksp_2fa', array( $this, 'render_2fa_form' ) );
		// Also check on login_init for custom login URLs.
		add_action( 'login_init', array( $this, 'maybe_render_2fa_form' ), 5 );

		// User profile settings.
		add_action( 'show_user_profile', array( $this, 'show_user_2fa_settings' ) );
		add_action( 'edit_user_profile', array( $this, 'show_user_2fa_settings' ) );
		add_action( 'personal_options_update', array( $this, 'save_user_2fa_settings' ) );
		add_action( 'edit_user_profile_update', array( $this, 'save_user_2fa_settings' ) );

		// Enqueue scripts on profile pages.
		add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_profile_scripts' ) );

		// AJAX handlers.
		add_action( 'wp_ajax_arksp_generate_2fa_secret', array( $this, 'ajax_generate_secret' ) );
		add_action( 'wp_ajax_arksp_verify_2fa_setup', array( $this, 'ajax_verify_setup' ) );
		add_action( 'wp_ajax_arksp_disable_2fa', array( $this, 'ajax_disable_2fa' ) );
		add_action( 'wp_ajax_arksp_regenerate_backup_codes', array( $this, 'ajax_regenerate_backup_codes' ) );

		// Enforce 2FA for admins.
		if ( ARKSP_Plugin::get_setting( 'two_factor_enforce_admin', false ) ) {
			add_action( 'admin_init', array( $this, 'enforce_admin_2fa' ) );
		}
	}

	/**
	 * Enqueue scripts for profile pages.
	 *
	 * @param string $hook Current admin page hook.
	 */
	public function enqueue_profile_scripts( $hook ) {
		if ( 'profile.php' !== $hook && 'user-edit.php' !== $hook ) {
			return;
		}

		// Determine which user is being edited.
		if ( 'user-edit.php' === $hook && isset( $_GET['user_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			$user_id = (int) $_GET['user_id']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		} else {
			$user_id = get_current_user_id();
		}

		wp_enqueue_script(
			'arksp-qrcode',
			ARKSP_PLUGIN_URL . 'assets/js/qrcode.min.js',
			array(),
			ARKSP_VERSION,
			true
		);

		wp_enqueue_script(
			'arksp-2fa-profile',
			ARKSP_PLUGIN_URL . 'assets/js/2fa-profile.js',
			array( 'jquery', 'arksp-qrcode' ),
			ARKSP_VERSION,
			true
		);

		wp_localize_script( 'arksp-2fa-profile', 'arksp2fa', array(
			'userId'  => $user_id,
			'nonce'   => wp_create_nonce( 'arksp_2fa_setup' ),
			'strings' => array(
				'twoFaEnabledTitle'  => __( '2FA Enabled! Save Your Backup Codes', 'arkhost-security-pack' ),
				'codesNotShownAgain' => __( 'IMPORTANT: These codes will NOT be shown again!', 'arkhost-security-pack' ),
				'storeCodesInfo'     => __( 'Store these codes in a safe place. Each code can only be used once.', 'arkhost-security-pack' ),
				'savedCodes'         => __( 'I have saved my codes', 'arkhost-security-pack' ),
				'confirmDisable'     => __( 'Are you sure you want to disable 2FA?', 'arkhost-security-pack' ),
				'confirmRegenerate'  => __( 'This will invalidate all existing backup codes. Are you sure?', 'arkhost-security-pack' ),
			),
		) );
	}

	/**
	 * Check if 2FA is required during authentication.
	 * This runs BEFORE cookies are set, making it reliable for 2FA.
	 *
	 * @param WP_User|WP_Error|null $user     User object or error.
	 * @param string                $username Username.
	 * @param string                $password Password.
	 * @return WP_User|WP_Error
	 */
	public function check_2fa_on_authenticate( $user, $username, $password ) {
		// If not a valid user, let WordPress handle it.
		if ( ! $user instanceof WP_User ) {
			return $user;
		}

		// Check if 2FA is enabled for this user.
		if ( ! $this->is_2fa_enabled_for_user( $user->ID ) ) {
			return $user;
		}

		// Generate a token for the 2FA session.
		$token = wp_generate_password( 32, false );

		// Nonce not possible during 2FA authentication flow; transient token validates the session.
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$redirect = isset( $_REQUEST['redirect_to'] ) ? esc_url_raw( wp_unslash( $_REQUEST['redirect_to'] ) ) : admin_url();
		set_transient( 'arksp_2fa_' . $token, array(
			'user_id'  => $user->ID,
			'redirect' => $redirect,
		), 5 * MINUTE_IN_SECONDS );

		// Redirect to 2FA form immediately.
		wp_safe_redirect( add_query_arg( array(
			'action' => 'arksp_2fa',
			'token'  => $token,
		), wp_login_url() ) );
		exit;
	}

	/**
	 * Enforce 2FA setup for administrators.
	 */
	public function enforce_admin_2fa() {
		// Skip AJAX requests.
		if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
			return;
		}

		$user = wp_get_current_user();

		// Only apply to administrators.
		if ( ! $user || ! user_can( $user, 'manage_options' ) ) {
			return;
		}

		// Skip if already has 2FA enabled.
		if ( $this->is_2fa_enabled_for_user( $user->ID ) ) {
			return;
		}

		// Allow access to profile page for setup.
		global $pagenow;
		if ( 'profile.php' === $pagenow || 'admin-ajax.php' === $pagenow ) {
			return;
		}

		// Redirect to profile page with notice.
		wp_safe_redirect( add_query_arg( 'arksp_2fa_required', '1', admin_url( 'profile.php' ) ) );
		exit;
	}

	/**
	 * Check if we should render the 2FA form (for custom login URLs).
	 */
	public function maybe_render_2fa_form() {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		if ( isset( $_GET['action'] ) && 'arksp_2fa' === $_GET['action'] ) {
			$this->render_2fa_form();
		}
	}

	/**
	 * Render 2FA verification form.
	 */
	public function render_2fa_form() {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$token = isset( $_GET['token'] ) ? sanitize_text_field( wp_unslash( $_GET['token'] ) ) : '';

		if ( empty( $token ) ) {
			wp_safe_redirect( wp_login_url() );
			exit;
		}

		$data = get_transient( 'arksp_2fa_' . $token );

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

		$error = '';

		// Handle form submission - 2FA verification uses transient token instead of nonce; user already authenticated via password.
		if ( isset( $_POST['arksp_2fa_code'] ) || isset( $_POST['arksp_backup_code'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
			// Check TOTP code first, then backup code.
			$code = '';
			if ( ! empty( $_POST['arksp_2fa_code'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
				$code = sanitize_text_field( wp_unslash( $_POST['arksp_2fa_code'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
			} elseif ( ! empty( $_POST['arksp_backup_code'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
				$code = sanitize_text_field( wp_unslash( $_POST['arksp_backup_code'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
			}

			$user_id = $data['user_id'];

			if ( ! empty( $code ) && $this->verify_code( $user_id, $code ) ) {
				// Delete the token.
				delete_transient( 'arksp_2fa_' . $token );

				// Log the user in.
				wp_set_auth_cookie( $user_id, false );
				wp_set_current_user( $user_id );

				// Redirect.
				wp_safe_redirect( $data['redirect'] );
				exit;
			}

			$error = __( 'Invalid verification code.', 'arkhost-security-pack' );
		}

		// Render the form.
		login_header( __( 'Two-Factor Authentication', 'arkhost-security-pack' ) );
		?>
		<form name="arksp_2fa_form" id="arksp_2fa_form" action="" method="post">
			<?php if ( $error ) : ?>
				<div id="login_error"><?php echo esc_html( $error ); ?></div>
			<?php endif; ?>

			<p><?php esc_html_e( 'Enter the verification code from your authenticator app.', 'arkhost-security-pack' ); ?></p>

			<p>
				<label for="arksp_2fa_code"><?php esc_html_e( 'Verification Code', 'arkhost-security-pack' ); ?></label>
				<input type="text" name="arksp_2fa_code" id="arksp_2fa_code" class="input" size="20" autocomplete="one-time-code" inputmode="numeric" pattern="[0-9]*" autofocus />
			</p>

			<p class="submit">
				<input type="submit" name="wp-submit" id="wp-submit" class="button button-primary button-large" value="<?php esc_attr_e( 'Verify', 'arkhost-security-pack' ); ?>" />
			</p>

			<p class="arksp-backup-code-link">
				<a href="#" onclick="document.getElementById('arksp-backup-field').style.display='block';this.style.display='none';return false;">
					<?php esc_html_e( 'Use a backup code', 'arkhost-security-pack' ); ?>
				</a>
			</p>

			<div id="arksp-backup-field" style="display:none;">
				<p><?php esc_html_e( 'Or enter a backup code:', 'arkhost-security-pack' ); ?></p>
				<p>
					<input type="text" name="arksp_backup_code" id="arksp_backup_code" class="input" size="20" />
				</p>
			</div>
		</form>
		<?php
		login_footer();
		exit;
	}

	/**
	 * Show 2FA settings on user profile.
	 *
	 * @param WP_User $user User object.
	 */
	public function show_user_2fa_settings( $user ) {
		if ( ! ARKSP_Plugin::get_setting( 'two_factor_enabled', false ) ) {
			return;
		}

		$is_enabled = $this->is_2fa_enabled_for_user( $user->ID );

		// Show notice if 2FA is required.
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		if ( isset( $_GET['arksp_2fa_required'] ) && ! $is_enabled ) :
			?>
			<div class="notice notice-warning" style="margin-bottom: 20px;">
				<p><strong><?php esc_html_e( 'Two-Factor Authentication Required', 'arkhost-security-pack' ); ?></strong></p>
				<p><?php esc_html_e( 'You must set up 2FA to access the dashboard.', 'arkhost-security-pack' ); ?></p>
			</div>
			<?php
		endif;
		?>
		<h2><?php esc_html_e( 'Two-Factor Authentication', 'arkhost-security-pack' ); ?></h2>
		<table class="form-table" role="presentation">
			<tr>
				<th scope="row"><?php esc_html_e( 'Status', 'arkhost-security-pack' ); ?></th>
				<td>
					<?php if ( $is_enabled ) : ?>
						<?php $remaining_codes = absint( $this->get_backup_codes_count( $user->ID ) ); ?>
						<span class="arksp-2fa-status arksp-2fa-enabled"><?php esc_html_e( 'Enabled', 'arkhost-security-pack' ); ?></span>
						<p>
							<button type="button" class="button" id="arksp-disable-2fa"><?php esc_html_e( 'Disable 2FA', 'arkhost-security-pack' ); ?></button>
							<button type="button" class="button" id="arksp-regenerate-backup-codes"><?php esc_html_e( 'Regenerate Backup Codes', 'arkhost-security-pack' ); ?></button>
						</p>
						<p class="description">
							<?php
							printf(
								/* translators: %d: number of remaining backup codes */
								esc_html__( 'You have %d backup codes remaining.', 'arkhost-security-pack' ),
								intval( $remaining_codes )
							);
							?>
							<?php if ( $remaining_codes < 3 ) : ?>
								<strong style="color: #dc3232;"><?php esc_html_e( 'Consider regenerating your backup codes.', 'arkhost-security-pack' ); ?></strong>
							<?php endif; ?>
						</p>
						<div id="arksp-backup-codes-display" style="display:none; background: #f6f7f7; padding: 15px; margin-top: 10px; border-radius: 4px;">
							<p><strong><?php esc_html_e( 'New Backup Codes:', 'arkhost-security-pack' ); ?></strong></p>
							<pre id="arksp-new-backup-codes" style="background: #fff; padding: 10px;"></pre>
							<p class="description" style="color: #dc3232;">
								<strong><?php esc_html_e( 'IMPORTANT: Save these codes now!', 'arkhost-security-pack' ); ?></strong><br>
								<?php esc_html_e( 'These codes will NOT be shown again. Store them in a safe place.', 'arkhost-security-pack' ); ?>
							</p>
						</div>
					<?php else : ?>
						<span class="arksp-2fa-status arksp-2fa-disabled"><?php esc_html_e( 'Disabled', 'arkhost-security-pack' ); ?></span>
						<p>
							<button type="button" class="button button-primary" id="arksp-setup-2fa"><?php esc_html_e( 'Set Up 2FA', 'arkhost-security-pack' ); ?></button>
						</p>
						<div id="arksp-2fa-setup" style="display:none;">
							<p><?php esc_html_e( 'Scan this QR code with your authenticator app:', 'arkhost-security-pack' ); ?></p>
							<div id="arksp-2fa-qr"></div>
							<p><strong><?php esc_html_e( 'Manual entry key:', 'arkhost-security-pack' ); ?></strong> <code id="arksp-2fa-secret"></code></p>
							<p>
								<label for="arksp-2fa-verify-code"><?php esc_html_e( 'Enter verification code to confirm:', 'arkhost-security-pack' ); ?></label>
								<input type="text" id="arksp-2fa-verify-code" class="regular-text" autocomplete="off" />
								<button type="button" class="button button-primary" id="arksp-verify-2fa-setup"><?php esc_html_e( 'Verify & Enable', 'arkhost-security-pack' ); ?></button>
							</p>
							<div id="arksp-2fa-setup-result"></div>
						</div>
					<?php endif; ?>
				</td>
			</tr>
		</table>
		<?php
	}

	/**
	 * Save user 2FA settings.
	 *
	 * @param int $user_id User ID.
	 */
	public function save_user_2fa_settings( $user_id ) {
		// Settings are saved via AJAX.
	}

	/**
	 * Generate new 2FA secret via AJAX.
	 */
	public function ajax_generate_secret() {
		check_ajax_referer( 'arksp_2fa_setup' );

		$user_id = isset( $_POST['user_id'] ) ? (int) $_POST['user_id'] : 0;

		if ( ! current_user_can( 'edit_user', $user_id ) ) {
			wp_send_json_error( array( 'message' => __( 'Permission denied.', 'arkhost-security-pack' ) ) );
		}

		$secret = $this->generate_secret();

		// Store temporarily (not enabled yet).
		update_user_meta( $user_id, self::SECRET_META_KEY . '_pending', $secret );

		$user = get_user_by( 'id', $user_id );
		$site = wp_parse_url( home_url(), PHP_URL_HOST );

		// Generate otpauth URL for QR code.
		$otpauth = sprintf(
			'otpauth://totp/%s:%s?secret=%s&issuer=%s',
			rawurlencode( $site ),
			rawurlencode( $user->user_email ),
			$secret,
			rawurlencode( $site )
		);

		wp_send_json_success( array(
			'secret'  => $secret,
			'otpauth' => $otpauth,
		) );
	}

	/**
	 * Verify 2FA setup via AJAX.
	 */
	public function ajax_verify_setup() {
		check_ajax_referer( 'arksp_2fa_setup' );

		$user_id = isset( $_POST['user_id'] ) ? (int) $_POST['user_id'] : 0;
		$code    = isset( $_POST['code'] ) ? sanitize_text_field( wp_unslash( $_POST['code'] ) ) : '';

		if ( ! current_user_can( 'edit_user', $user_id ) ) {
			wp_send_json_error( array( 'message' => __( 'Permission denied.', 'arkhost-security-pack' ) ) );
		}

		$pending_secret = get_user_meta( $user_id, self::SECRET_META_KEY . '_pending', true );

		if ( empty( $pending_secret ) ) {
			wp_send_json_error( array( 'message' => __( 'No pending setup found.', 'arkhost-security-pack' ) ) );
		}

		// Verify the code.
		if ( ! $this->verify_totp( $pending_secret, $code ) ) {
			wp_send_json_error( array( 'message' => __( 'Invalid code. Please try again.', 'arkhost-security-pack' ) ) );
		}

		// Enable 2FA.
		update_user_meta( $user_id, self::SECRET_META_KEY, $pending_secret );
		update_user_meta( $user_id, self::ENABLED_META_KEY, '1' );
		delete_user_meta( $user_id, self::SECRET_META_KEY . '_pending' );

		// Generate backup codes - show plain to user, store hashed.
		$plain_codes  = $this->generate_backup_codes();
		$hashed_codes = array_map( array( $this, 'hash_backup_code' ), $plain_codes );
		update_user_meta( $user_id, self::BACKUP_CODES_META_KEY, $hashed_codes );

		wp_send_json_success( array(
			'message'      => __( '2FA enabled successfully. Save your backup codes now - they will not be shown again!', 'arkhost-security-pack' ),
			'backup_codes' => $plain_codes,
			'show_codes'   => true,
		) );
	}

	/**
	 * Disable 2FA via AJAX.
	 */
	public function ajax_disable_2fa() {
		check_ajax_referer( 'arksp_2fa_setup' );

		$user_id = isset( $_POST['user_id'] ) ? (int) $_POST['user_id'] : 0;

		if ( ! current_user_can( 'edit_user', $user_id ) ) {
			wp_send_json_error( array( 'message' => __( 'Permission denied.', 'arkhost-security-pack' ) ) );
		}

		delete_user_meta( $user_id, self::SECRET_META_KEY );
		delete_user_meta( $user_id, self::ENABLED_META_KEY );
		delete_user_meta( $user_id, self::BACKUP_CODES_META_KEY );

		wp_send_json_success( array( 'message' => __( '2FA disabled.', 'arkhost-security-pack' ) ) );
	}

	/**
	 * Regenerate backup codes via AJAX.
	 */
	public function ajax_regenerate_backup_codes() {
		check_ajax_referer( 'arksp_2fa_setup' );

		$user_id = isset( $_POST['user_id'] ) ? (int) $_POST['user_id'] : 0;

		if ( ! current_user_can( 'edit_user', $user_id ) ) {
			wp_send_json_error( array( 'message' => __( 'Permission denied.', 'arkhost-security-pack' ) ) );
		}

		// Generate new codes.
		$plain_codes = $this->generate_backup_codes();

		// Hash them before storing.
		$hashed_codes = array_map( array( $this, 'hash_backup_code' ), $plain_codes );
		update_user_meta( $user_id, self::BACKUP_CODES_META_KEY, $hashed_codes );

		// Return plain codes to show user ONCE.
		wp_send_json_success( array(
			'codes'   => $plain_codes,
			'message' => __( 'Backup codes regenerated. Save these codes now - they will not be shown again.', 'arkhost-security-pack' ),
		) );
	}

	/**
	 * Check if 2FA is enabled for a user.
	 *
	 * @param int $user_id User ID.
	 * @return bool
	 */
	public function is_2fa_enabled_for_user( $user_id ) {
		return '1' === get_user_meta( $user_id, self::ENABLED_META_KEY, true );
	}

	/**
	 * Verify a code (TOTP or backup).
	 *
	 * @param int    $user_id User ID.
	 * @param string $code    Code to verify.
	 * @return bool
	 */
	public function verify_code( $user_id, $code ) {
		$code = preg_replace( '/\s+/', '', $code );

		// Try TOTP first.
		$secret = get_user_meta( $user_id, self::SECRET_META_KEY, true );

		if ( $this->verify_totp( $secret, $code ) ) {
			return true;
		}

		// Try backup code.
		return $this->verify_backup_code( $user_id, $code );
	}

	/**
	 * Verify TOTP code.
	 *
	 * @param string $secret Secret key.
	 * @param string $code   Code to verify.
	 * @param int    $window Time window tolerance.
	 * @return bool
	 */
	private function verify_totp( $secret, $code, $window = 1 ) {
		if ( empty( $secret ) || empty( $code ) ) {
			return false;
		}

		$time = floor( time() / self::TIME_STEP );

		for ( $i = -$window; $i <= $window; $i++ ) {
			$calculated = $this->calculate_totp( $secret, $time + $i );
			if ( hash_equals( $calculated, $code ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Calculate TOTP code.
	 *
	 * @param string $secret Secret key.
	 * @param int    $time   Time counter.
	 * @return string
	 */
	private function calculate_totp( $secret, $time ) {
		// Decode base32 secret.
		$secret_decoded = $this->base32_decode( $secret );

		// Pack time.
		$time_packed = pack( 'N*', 0, $time );

		// Calculate HMAC.
		$hash = hash_hmac( 'sha1', $time_packed, $secret_decoded, true );

		// Dynamic truncation.
		$offset = ord( substr( $hash, -1 ) ) & 0x0F;
		$code   = ( ord( $hash[ $offset ] ) & 0x7F ) << 24;
		$code  |= ( ord( $hash[ $offset + 1 ] ) & 0xFF ) << 16;
		$code  |= ( ord( $hash[ $offset + 2 ] ) & 0xFF ) << 8;
		$code  |= ( ord( $hash[ $offset + 3 ] ) & 0xFF );
		$code   = $code % pow( 10, self::CODE_LENGTH );

		return str_pad( $code, self::CODE_LENGTH, '0', STR_PAD_LEFT );
	}

	/**
	 * Generate secret key.
	 *
	 * @param int $length Length in bytes (16 = 26 base32 chars).
	 * @return string
	 */
	private function generate_secret( $length = 16 ) {
		$random = wp_generate_password( $length, false, false );
		return $this->base32_encode( $random );
	}

	/**
	 * Generate backup codes.
	 *
	 * @param int $count Number of codes.
	 * @return array
	 */
	private function generate_backup_codes( $count = 10 ) {
		$codes = array();
		for ( $i = 0; $i < $count; $i++ ) {
			$codes[] = strtoupper( wp_generate_password( 8, false, false ) );
		}
		return $codes;
	}

	/**
	 * Get backup codes count for user (hashed codes stored).
	 *
	 * @param int $user_id User ID.
	 * @return int
	 */
	public function get_backup_codes_count( $user_id ) {
		$codes = get_user_meta( $user_id, self::BACKUP_CODES_META_KEY, true );
		return is_array( $codes ) ? count( $codes ) : 0;
	}

	/**
	 * Get backup codes for user (internal use only).
	 *
	 * @param int $user_id User ID.
	 * @return array
	 */
	private function get_backup_codes( $user_id ) {
		$codes = get_user_meta( $user_id, self::BACKUP_CODES_META_KEY, true );
		return is_array( $codes ) ? $codes : array();
	}

	/**
	 * Hash a backup code for secure storage.
	 *
	 * @param string $code Plain backup code.
	 * @return string Hashed code.
	 */
	private function hash_backup_code( $code ) {
		return wp_hash( strtoupper( $code ) );
	}

	/**
	 * Verify and consume a backup code.
	 *
	 * @param int    $user_id User ID.
	 * @param string $code    Backup code.
	 * @return bool
	 */
	private function verify_backup_code( $user_id, $code ) {
		$stored_codes = $this->get_backup_codes( $user_id );
		$code         = strtoupper( preg_replace( '/[^A-Z0-9]/', '', $code ) );
		$hashed_input = $this->hash_backup_code( $code );

		// Check against hashed codes.
		foreach ( $stored_codes as $index => $stored_code ) {
			// Support both hashed (new) and plain (legacy) codes.
			if ( hash_equals( $stored_code, $hashed_input ) || hash_equals( $stored_code, $code ) ) {
				// Remove used code.
				unset( $stored_codes[ $index ] );
				update_user_meta( $user_id, self::BACKUP_CODES_META_KEY, array_values( $stored_codes ) );
				return true;
			}
		}

		return false;
	}

	/**
	 * Base32 encode.
	 *
	 * @param string $data Data to encode.
	 * @return string
	 */
	private function base32_encode( $data ) {
		$alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
		$binary   = '';
		$encoded  = '';

		foreach ( str_split( $data ) as $char ) {
			$binary .= str_pad( decbin( ord( $char ) ), 8, '0', STR_PAD_LEFT );
		}

		$chunks = str_split( $binary, 5 );

		foreach ( $chunks as $chunk ) {
			$chunk = str_pad( $chunk, 5, '0', STR_PAD_RIGHT );
			$encoded .= $alphabet[ bindec( $chunk ) ];
		}

		return $encoded;
	}

	/**
	 * Base32 decode.
	 *
	 * @param string $data Data to decode.
	 * @return string
	 */
	private function base32_decode( $data ) {
		$alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
		$data     = strtoupper( $data );
		$binary   = '';
		$decoded  = '';

		foreach ( str_split( $data ) as $char ) {
			$pos = strpos( $alphabet, $char );
			if ( false !== $pos ) {
				$binary .= str_pad( decbin( $pos ), 5, '0', STR_PAD_LEFT );
			}
		}

		$chunks = str_split( $binary, 8 );

		foreach ( $chunks as $chunk ) {
			if ( strlen( $chunk ) === 8 ) {
				$decoded .= chr( bindec( $chunk ) );
			}
		}

		return $decoded;
	}
}
