<?php

declare(strict_types=1);

namespace SecurWP\Modules\TwoFactor;

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

use SecurWP\Core\Installer;
use SecurWP\Includes\SecretManager;
use WP_REST_Server;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;

/**
 * REST API endpoints for Two-Factor Authentication.
 *
 * Provides endpoints for 2FA setup, confirmation, disabling,
 * recovery code management, and admin settings.
 *
 */
class TwoFactorApi {

	/**
	 * REST namespace.
	 */
	private const NAMESPACE = 'securwp/v1';

	/**
	 * Register all REST API routes.
	 */
	public function register_routes(): void {
		// Per-user endpoints (logged-in users).
		register_rest_route( self::NAMESPACE, '/2fa/status', [
			[
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => [ $this, 'get_status' ],
				'permission_callback' => [ $this, 'check_logged_in' ],
			],
		] );

		register_rest_route( self::NAMESPACE, '/2fa/setup', [
			[
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => [ $this, 'setup' ],
				'permission_callback' => [ $this, 'check_logged_in' ],
				'args'                => [
					'method'   => [
						'required'          => false,
						'type'              => 'string',
						'sanitize_callback' => 'sanitize_key',
					],
					'password' => [
						'required'          => true,
						'type'              => 'string',
						'sanitize_callback' => function ( $value ) {
							return (string) $value;
						},
					],
				],
			],
		] );

		register_rest_route( self::NAMESPACE, '/2fa/confirm', [
			[
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => [ $this, 'confirm' ],
				'permission_callback' => [ $this, 'check_logged_in' ],
				'args'                => [
					'code' => [
						'required'          => true,
						'type'              => 'string',
						'sanitize_callback' => 'sanitize_text_field',
					],
				],
			],
		] );

		register_rest_route( self::NAMESPACE, '/2fa/disable', [
			[
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => [ $this, 'disable' ],
				'permission_callback' => [ $this, 'check_logged_in' ],
				'args'                => [
					'password' => [
						'required'          => true,
						'type'              => 'string',
						'sanitize_callback' => function ( $value ) {
							return (string) $value;
						},
					],
				],
			],
		] );

		register_rest_route( self::NAMESPACE, '/2fa/regenerate-codes', [
			[
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => [ $this, 'regenerate_codes' ],
				'permission_callback' => [ $this, 'check_logged_in' ],
				'args'                => [
					'password' => [
						'required'          => true,
						'type'              => 'string',
						'sanitize_callback' => function ( $value ) {
							return (string) $value;
						},
					],
				],
			],
		] );

		register_rest_route( self::NAMESPACE, '/2fa/send-email-otp', [
			[
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => [ $this, 'send_email_otp' ],
				'permission_callback' => [ $this, 'check_logged_in' ],
				'args'                => [
					'context' => [
						'required'          => false,
						'type'              => 'string',
						'sanitize_callback' => 'sanitize_key',
					],
				],
			],
		] );

		// Admin-only settings endpoints.
		register_rest_route( self::NAMESPACE, '/2fa/settings', [
			[
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => [ $this, 'get_settings' ],
				'permission_callback' => [ $this, 'check_admin' ],
			],
			[
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => [ $this, 'update_settings' ],
				'permission_callback' => [ $this, 'check_admin' ],
			],
		] );
	}

	/**
	 * Permission callback for per-user endpoints.
	 *
	 * @return bool True if the current user is logged in.
	 */
	public function check_logged_in(): bool {
		return is_user_logged_in();
	}

	/**
	 * Permission callback for admin-only endpoints.
	 *
	 * @return bool True if the current user can manage options.
	 */
	public function check_admin(): bool {
		return \SecurWP\Includes\Capabilities::can_manage_plugin();
	}

	/**
	 * GET /2fa/status - Get current user's 2FA status.
	 *
	 * @param WP_REST_Request $request The REST request.
	 * @return WP_REST_Response
	 */
	public function get_status( WP_REST_Request $request ): WP_REST_Response {
		$user_id  = get_current_user_id();
		$row      = $this->get_user_row( $user_id );
		$settings = \SecurWP\Includes\SettingsStore::get( \SecurWP\Includes\SettingsStore::COL_TWOFA, TwoFactor::get_default_settings() );
		$module_enabled  = ! empty( $settings['enabled'] );
		$allowed_methods = self::sanitize_allowed_methods( $settings['allowed_methods'] ?? [ 'totp', 'email' ] );

		if ( ! $row ) {
			return new WP_REST_Response( [
				'enabled'                  => false,
				'confirmed'                => false,
				'has_recovery_codes'       => false,
				'recovery_codes_remaining' => 0,
				'method'                   => 'totp',
				'module_enabled'           => $module_enabled,
				'allowed_methods'          => $allowed_methods,
			], 200 );
		}

		$remaining = RecoveryCodes::get_remaining_count( $user_id );

		return new WP_REST_Response( [
			'enabled'                  => (bool) $row->is_enabled,
			'confirmed'                => ! empty( $row->confirmed_at ),
			'has_recovery_codes'       => $remaining > 0,
			'recovery_codes_remaining' => $remaining,
			'method'                   => $row->method ?? 'totp',
			'module_enabled'           => $module_enabled,
			'allowed_methods'          => $allowed_methods,
		], 200 );
	}

	/**
	 * POST /2fa/setup - Generate a new 2FA setup state and recovery codes.
	 *
	 * Stores pending setup state after password re-authentication. Authenticator
	 * app secrets remain encrypted; email setup verification state is persisted
	 * in the plugin-owned 2FA row until the user confirms the code.
	 *
	 * @param WP_REST_Request $request The REST request.
	 * @return WP_REST_Response|WP_Error
	 */
	public function setup( WP_REST_Request $request ) {
		$user_id = get_current_user_id();
		$user    = wp_get_current_user();
		$method  = sanitize_text_field( $request->get_param( 'method' ) ?? 'totp' );
		$method  = in_array( $method, [ 'totp', 'email' ], true ) ? $method : 'totp';

		$module_error = $this->ensure_module_enabled();
		if ( is_wp_error( $module_error ) ) {
			return $module_error;
		}

		$storage_error = $this->ensure_setup_storage_ready();
		if ( is_wp_error( $storage_error ) ) {
			return $storage_error;
		}

		// Always require password confirmation before issuing fresh setup state.
		$pw_error = $this->verify_password( $request, $user_id );
		if ( is_wp_error( $pw_error ) ) {
			return $pw_error;
		}

		$allowed_methods = $this->get_allowed_methods();
		if ( ! in_array( $method, $allowed_methods, true ) ) {
			return new WP_Error(
				'securwp_2fa_method_not_allowed',
				__( 'This two-factor method is not allowed by your administrator.', 'securwp' ),
				[ 'status' => 400 ]
			);
		}

		$secret           = null;
		$qr_data_uri      = null;
		$encrypted_secret = null;

		try {
			$recovery_codes = RecoveryCodes::generate();

			if ( $method === 'totp' ) {
				$secret           = TOTPProvider::generate_secret();
				$qr_data_uri      = self::generate_qr_data_uri( TOTPProvider::generate_otp_url( $secret, $user ) );
				$encrypted_secret = SecretManager::encrypt( $secret );
			}
		} catch ( \SodiumException|\RuntimeException $e ) {
			return new WP_Error(
				'securwp_2fa_setup_error',
				__( 'Failed to generate 2FA credentials.', 'securwp' ),
				[ 'status' => 500 ]
			);
		}

		global $wpdb;
		$table = $wpdb->prefix . 'securwp_two_factor';
		$now   = time();

		// Check if a row already exists for this user.
		$existing = $this->get_user_row( $user_id );
		if ( $existing && ! empty( $existing->is_enabled ) && ! empty( $existing->confirmed_at ) ) {
			return new WP_Error(
				'securwp_2fa_already_enabled',
				__( 'Two-factor authentication is already enabled for this account. Disable it first before starting a new setup.', 'securwp' ),
				[ 'status' => 400 ]
			);
		}

		if ( $existing ) {
			// Update existing row (reset to unconfirmed state).
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery
			$wpdb->update(
				$table,
				[
					'totp_secret'            => $encrypted_secret,
					'method'                 => $method,
					'is_enabled'             => 0,
					'confirmed_at'           => null,
					'setup_email_code_hash'  => null,
					'setup_email_attempts'   => 0,
					'setup_email_sent_at'    => null,
					'setup_email_delivered'  => 0,
					'updated_at'             => $now,
				],
				[ 'user_id' => $user_id ],
				[ '%s', '%s', '%d', null, null, '%d', null, '%d', '%d' ],
				[ '%d' ]
			);
		} else {
			// Insert new row.
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery
			$wpdb->insert(
				$table,
				[
					'user_id'               => $user_id,
					'totp_secret'           => $encrypted_secret,
					'method'                => $method,
					'is_enabled'            => 0,
					'setup_email_code_hash' => null,
					'setup_email_attempts'  => 0,
					'setup_email_sent_at'   => null,
					'setup_email_delivered' => 0,
					'created_at'            => $now,
					'updated_at'            => $now,
				],
				[ '%d', '%s', '%s', '%d', null, '%d', null, '%d', '%d', '%d' ]
			);
		}

		// Save recovery codes.
		if ( ! RecoveryCodes::save_codes( $user_id, $recovery_codes ) ) {
			return new WP_Error(
				'securwp_2fa_recovery_codes_error',
				__( 'Failed to store recovery codes. Please try again.', 'securwp' ),
				[ 'status' => 500 ]
			);
		}

		$response = [
			'method'         => $method,
			'recovery_codes' => $recovery_codes,
		];

		if ( $method === 'totp' ) {
			$response['secret'] = $secret;
			$response['qr_svg'] = $qr_data_uri;
		}

		return new WP_REST_Response( $response, 200 );
	}

	/**
	 * POST /2fa/confirm - Verify the initial setup code and activate 2FA.
	 *
	 * @param WP_REST_Request $request The REST request.
	 * @return WP_REST_Response|WP_Error
	 */
	public function confirm( WP_REST_Request $request ) {
		$user_id = get_current_user_id();
		$code    = $request->get_param( 'code' );
		$user    = wp_get_current_user();

		$module_error = $this->ensure_module_enabled();
		if ( is_wp_error( $module_error ) ) {
			return $module_error;
		}

		$row = $this->get_user_row( $user_id );

		if ( ! $row ) {
			return new WP_Error(
				'securwp_2fa_not_setup',
				__( 'Two-factor authentication has not been set up. Please run setup first.', 'securwp' ),
				[ 'status' => 400 ]
			);
		}

		$method = $row->method ?? 'totp';
		if ( ! in_array( $method, $this->get_allowed_methods(), true ) ) {
			return new WP_Error(
				'securwp_2fa_method_not_allowed',
				__( 'This two-factor method is no longer allowed by your administrator. Start setup again with an available method.', 'securwp' ),
				[ 'status' => 400 ]
			);
		}

		if ( $method === 'totp' && empty( $row->totp_secret ) ) {
			return new WP_Error(
				'securwp_2fa_not_setup',
				__( 'Two-factor authentication has not been set up. Please run setup first.', 'securwp' ),
				[ 'status' => 400 ]
			);
		}

		if ( $row->is_enabled && ! empty( $row->confirmed_at ) ) {
			return new WP_Error(
				'securwp_2fa_already_enabled',
				__( 'Two-factor authentication is already enabled.', 'securwp' ),
				[ 'status' => 400 ]
			);
		}

		// Verify the code based on method.
		$verified = false;

		if ( $method === 'email' ) {
			$verified = EmailOTPProvider::verify_setup_code( $user_id, $code );
		} else {
			$secret = SecretManager::decrypt( $row->totp_secret );
			if ( $secret === false ) {
				return new WP_Error(
					'securwp_2fa_decrypt_error',
					__( 'Failed to decrypt TOTP secret.', 'securwp' ),
					[ 'status' => 500 ]
				);
			}
			$verified = TOTPProvider::verify_code( $secret, $code );
		}

		if ( ! $verified ) {
			return new WP_Error(
				'securwp_2fa_invalid_code',
				__( 'Invalid verification code. Please try again.', 'securwp' ),
				[ 'status' => 400 ]
			);
		}

		// Activate 2FA.
		global $wpdb;
		$table = $wpdb->prefix . 'securwp_two_factor';
		$now   = time();

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery
		$wpdb->update(
			$table,
			[
				'is_enabled'   => 1,
				'confirmed_at' => $now,
				'updated_at'   => $now,
			],
			[ 'user_id' => $user_id ],
			[ '%d', '%d', '%d' ],
			[ '%d' ]
		);

		// Clear enforcement grace period tracking so a fresh grace period
		// applies if the user later disables 2FA and enforcement is still active.
		LoginInterstitial::clear_enforcement_start( $user_id );

		do_action( 'securwp_2fa_enrolled', $user_id, [
			'username'                 => $user instanceof \WP_User ? $user->user_login : '',
			'method'                   => $method,
			'recovery_codes_remaining' => RecoveryCodes::get_remaining_count( $user_id ),
		] );

		return new WP_REST_Response( [
			'success' => true,
			'message' => __( 'Two-factor authentication has been enabled.', 'securwp' ),
		], 200 );
	}

	/**
	 * POST /2fa/disable - Disable 2FA after password verification.
	 *
	 * @param WP_REST_Request $request The REST request.
	 * @return WP_REST_Response|WP_Error
	 */
	public function disable( WP_REST_Request $request ) {
		$user_id = get_current_user_id();
		$user    = wp_get_current_user();
		$row     = $this->get_user_row( $user_id );

		$pw_error = $this->verify_password( $request, $user_id );
		if ( is_wp_error( $pw_error ) ) {
			return $pw_error;
		}

		if ( ! TwoFactor::is_schema_current() ) {
			return new WP_Error(
				'securwp_2fa_storage_unavailable',
				__( 'Two-factor storage is not ready yet. Please try again in a moment.', 'securwp' ),
				[ 'status' => 503 ]
			);
		}

		global $wpdb;
		$table = $wpdb->prefix . 'securwp_two_factor';
		$now   = time();
		$update_data = [
			'is_enabled'       => 0,
			'totp_secret'      => null,
			'method'           => 'totp',
			'confirmed_at'     => null,
			'remember_token'   => null,
			'remember_expires' => null,
			'updated_at'       => $now,
		];
		$update_format = [ '%d', null, '%s', null, null, null, '%d' ];

		if ( EmailOTPProvider::is_setup_schema_current() ) {
			$update_data['setup_email_code_hash'] = null;
			$update_data['setup_email_attempts']  = 0;
			$update_data['setup_email_sent_at']   = null;
			$update_data['setup_email_delivered'] = 0;

			$update_format[] = null;
			$update_format[] = '%d';
			$update_format[] = null;
			$update_format[] = '%d';
		}

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery
		$wpdb->update(
			$table,
			$update_data,
			[ 'user_id' => $user_id ],
			$update_format,
			[ '%d' ]
		);

		RecoveryCodes::clear_codes( $user_id );
		LoginSessionStore::delete_for_user( $user_id );

		do_action( 'securwp_2fa_disabled', $user_id, [
			'username'               => $user->user_login,
			'previous_method'        => $row && isset( $row->method ) ? (string) $row->method : 'totp',
			'recovery_codes_cleared' => true,
		] );

		return new WP_REST_Response( [
			'success' => true,
			'message' => __( 'Two-factor authentication has been disabled.', 'securwp' ),
		], 200 );
	}

	/**
	 * POST /2fa/regenerate-codes - Generate new recovery codes.
	 *
	 * @param WP_REST_Request $request The REST request.
	 * @return WP_REST_Response|WP_Error
	 */
	public function regenerate_codes( WP_REST_Request $request ) {
		$user_id = get_current_user_id();

		$module_error = $this->ensure_module_enabled();
		if ( is_wp_error( $module_error ) ) {
			return $module_error;
		}

		$row = $this->get_user_row( $user_id );
		if ( ! $row || ! $row->is_enabled ) {
			return new WP_Error(
				'securwp_2fa_not_enabled',
				__( 'Two-factor authentication is not enabled.', 'securwp' ),
				[ 'status' => 400 ]
			);
		}

		// Require password confirmation to prevent session hijacking
		// from invalidating the user's existing recovery codes.
		$pw_error = $this->verify_password( $request, $user_id );
		if ( is_wp_error( $pw_error ) ) {
			return $pw_error;
		}

		try {
			$codes = RecoveryCodes::generate();
			if ( ! RecoveryCodes::save_codes( $user_id, $codes ) ) {
				return new WP_Error(
					'securwp_2fa_codes_error',
					__( 'Failed to store recovery codes.', 'securwp' ),
					[ 'status' => 500 ]
				);
			}
		} catch ( \SodiumException|\RuntimeException $e ) {
			return new WP_Error(
				'securwp_2fa_codes_error',
				__( 'Failed to generate recovery codes.', 'securwp' ),
				[ 'status' => 500 ]
			);
		}

		$user = wp_get_current_user();
		do_action( 'securwp_2fa_recovery_codes_regenerated', $user_id, [
			'username'                 => $user instanceof \WP_User ? $user->user_login : '',
			'recovery_codes_remaining' => count( $codes ),
		] );

		return new WP_REST_Response( [
			'recovery_codes' => $codes,
		], 200 );
	}

	/**
	 * POST /2fa/send-email-otp - Send (or resend) a setup email OTP code.
	 *
	 * @param WP_REST_Request $request The REST request.
	 * @return WP_REST_Response|WP_Error
	 */
	public function send_email_otp( WP_REST_Request $request ) {
		$user_id = get_current_user_id();
		$context = sanitize_text_field( $request->get_param( 'context' ) ?? 'setup' );

		if ( 'setup' !== $context ) {
			return new WP_Error(
				'securwp_2fa_invalid_email_context',
				__( 'This endpoint only supports authenticated two-factor setup verification.', 'securwp' ),
				[ 'status' => 400 ]
			);
		}

		$module_error = $this->ensure_module_enabled();
		if ( is_wp_error( $module_error ) ) {
			return $module_error;
		}

		$storage_error = $this->ensure_setup_storage_ready();
		if ( is_wp_error( $storage_error ) ) {
			return $storage_error;
		}

		$setup_error = $this->ensure_pending_email_setup( $user_id );
		if ( is_wp_error( $setup_error ) ) {
			return $setup_error;
		}

		$sent = EmailOTPProvider::send_setup_code( $user_id );
		if ( ! $sent ) {
			return new WP_Error(
				'securwp_2fa_email_failed',
				__( 'Email delivery failed. Please check your site\'s email configuration (SMTP settings) and try again.', 'securwp' ),
				[ 'status' => 500 ]
			);
		}

		return new WP_REST_Response( [
			'success' => true,
			'message' => __( 'Verification code sent to your email.', 'securwp' ),
		], 200 );
	}

	/**
	 * GET /2fa/settings - Retrieve admin 2FA settings.
	 *
	 * @param WP_REST_Request $request The REST request.
	 * @return WP_REST_Response
	 */
	public function get_settings( WP_REST_Request $request ): WP_REST_Response {
		$defaults = TwoFactor::get_default_settings();
		$settings = \SecurWP\Includes\SettingsStore::get( \SecurWP\Includes\SettingsStore::COL_TWOFA, $defaults );
		if ( ! is_array( $settings ) ) {
			$settings = $defaults;
		}
		$settings = wp_parse_args( $settings, $defaults );
		$settings['allowed_methods'] = self::sanitize_allowed_methods( $settings['allowed_methods'] ?? $defaults['allowed_methods'] );
		if ( empty( $settings['allowed_methods'] ) ) {
			$settings['allowed_methods'] = $defaults['allowed_methods'];
		}
		$settings['enforce_for_roles'] = array_values(
			array_unique(
				array_filter(
					array_map(
						static fn( $role ): string => sanitize_key( (string) $role ),
						is_array( $settings['enforce_for_roles'] ?? null ) ? $settings['enforce_for_roles'] : []
					)
				)
			)
		);

		return new WP_REST_Response( $settings, 200 );
	}

	/**
	 * POST /2fa/settings - Save admin 2FA settings.
	 *
	 * @param WP_REST_Request $request The REST request.
	 * @return WP_REST_Response|WP_Error
	 */
	public function update_settings( WP_REST_Request $request ) {
		$params = $request->get_json_params();

		if ( ! is_array( $params ) ) {
			return new WP_Error(
				'securwp_invalid_json',
				__( 'Invalid JSON payload.', 'securwp' ),
				[ 'status' => 400 ]
			);
		}

		$defaults  = TwoFactor::get_default_settings();
		$current   = \SecurWP\Includes\SettingsStore::get( \SecurWP\Includes\SettingsStore::COL_TWOFA, $defaults );
		$sanitized = $current;

		// Sanitize each setting.
		if ( array_key_exists( 'enabled', $params ) ) {
			$sanitized['enabled'] = rest_sanitize_boolean( $params['enabled'] );
		}

		if ( array_key_exists( 'enforce_for_roles', $params ) ) {
			if ( is_array( $params['enforce_for_roles'] ) ) {
				$raw_roles = array_map( 'sanitize_key', $params['enforce_for_roles'] );
				$wp_roles  = function_exists( 'wp_roles' ) ? wp_roles() : null;
				$valid_map = ( $wp_roles && is_array( $wp_roles->roles ) ) ? $wp_roles->roles : [];
				$sanitized['enforce_for_roles'] = array_values(
					array_unique(
						array_filter(
							$raw_roles,
							static function ( $role ) use ( $valid_map ) {
								return is_string( $role ) && $role !== '' && isset( $valid_map[ $role ] );
							}
						)
					)
				);
			}
		}

		if ( array_key_exists( 'remember_device_days', $params ) ) {
			$days = absint( $params['remember_device_days'] );
			$sanitized['remember_device_days'] = max( 1, min( 365, $days ) );
		}

		if ( array_key_exists( 'grace_period_days', $params ) ) {
			$days = absint( $params['grace_period_days'] );
			$sanitized['grace_period_days'] = max( 0, min( 30, $days ) );
		}

		if ( array_key_exists( 'allowed_methods', $params ) && is_array( $params['allowed_methods'] ) ) {
			$sanitized['allowed_methods'] = self::sanitize_allowed_methods( $params['allowed_methods'] );
			if ( empty( $sanitized['allowed_methods'] ) ) {
				$sanitized['allowed_methods'] = $defaults['allowed_methods'];
			}
		}

		if ( ! \SecurWP\Includes\SettingsStore::save( \SecurWP\Includes\SettingsStore::COL_TWOFA, $sanitized ) ) {
			return new WP_Error(
				'securwp_2fa_settings_save_failed',
				__( 'Unable to save two-factor authentication settings right now.', 'securwp' ),
				[ 'status' => 500 ]
			);
		}
		TwoFactor::apply_settings_side_effects( $current, $sanitized );

		return new WP_REST_Response( [
			'success'  => true,
			'message'  => __( 'Two-factor authentication settings saved.', 'securwp' ),
			'settings' => $sanitized,
		], 200 );
	}

	/**
	 * Get the 2FA database row for a user.
	 *
	 * @param int $user_id The WordPress user ID.
	 * @return object|null The database row, or null if not found.
	 */
	private function get_user_row( int $user_id ): ?object {
		global $wpdb;

		$table = self::table_identifier( $wpdb->prefix . 'securwp_two_factor' );

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
		$sql = $wpdb->prepare(
			"SELECT * FROM {$table} WHERE user_id = %d",
			$user_id
		);
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
		if ( ! is_string( $sql ) || '' === $sql ) {
			return null;
		}

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Executes the prepared 2FA row lookup against the plugin-owned table.
		$row = $wpdb->get_row( $sql );

		return $row ?: null;
	}

	/**
	 * Verify the user's password from the REST request.
	 *
	 * Returns null on success, WP_Error if the password is missing or incorrect.
	 *
	 * @param WP_REST_Request $request The REST request.
	 * @param int             $user_id The user ID to verify against.
	 * @return WP_Error|null Null on success, WP_Error on failure.
	 */
	private function verify_password( WP_REST_Request $request, int $user_id ): ?WP_Error {
		$password = $request->get_param( 'password' );
		if ( empty( $password ) ) {
			return new WP_Error(
				'securwp_2fa_password_required',
				__( 'Password confirmation is required for this action.', 'securwp' ),
				[ 'status' => 403 ]
			);
		}

		$user = get_userdata( $user_id );
		if ( ! $user || ! wp_check_password( $password, $user->user_pass, $user_id ) ) {
			return new WP_Error(
				'securwp_2fa_invalid_password',
				__( 'Incorrect password.', 'securwp' ),
				[ 'status' => 403 ]
			);
		}

		return null;
	}

	/**
	 * Generate a QR code as an SVG data URI for safe image rendering.
	 *
	 * @param string $data The data to encode (e.g. otpauth:// URI).
	 * @return string SVG data URI, or empty string on failure.
	 */
	private static function generate_qr_data_uri( string $data ): string {
		try {
			$options = new \chillerlan\QRCode\QROptions( [
				'outputType'       => \chillerlan\QRCode\QRCode::OUTPUT_MARKUP_SVG,
				'cssClass'         => '',
				'addQuietzone'     => true,
				'quietzoneSize'    => 2,
				'svgDefs'          => '',
			] );

			$qr = new \chillerlan\QRCode\QRCode( $options );

			return $qr->render( $data );
		} catch ( \Throwable $e ) {
			return '';
		}
	}

	/**
	 * Normalize allowed method settings to known values only.
	 *
	 * @param mixed $methods Raw methods setting payload.
	 * @return array<int,string>
	 */
	private static function sanitize_allowed_methods( $methods ): array {
		$valid = TwoFactor::sanitize_allowed_methods( $methods );
		if ( ! empty( $valid ) ) {
			return $valid;
		}

		$defaults = TwoFactor::get_default_settings();
		return TwoFactor::sanitize_allowed_methods( $defaults['allowed_methods'] ?? [ 'totp', 'email' ] );
	}

	/**
	 * Quote a table identifier after validating its shape.
	 */
	private static function table_identifier( string $table ): string {
		if ( $table === '' || strlen( $table ) > 64 || ! preg_match( '/^[A-Za-z0-9_]+$/', $table ) ) {
			throw new \InvalidArgumentException( 'Invalid table identifier.' );
		}

		return '`' . $table . '`';
	}

	/**
	 * Return the current administrator-allowed method set.
	 *
	 * @return array<int,string>
	 */
	private function get_allowed_methods(): array {
		$settings = \SecurWP\Includes\SettingsStore::get(
			\SecurWP\Includes\SettingsStore::COL_TWOFA,
			TwoFactor::get_default_settings()
		);

		return self::sanitize_allowed_methods( $settings['allowed_methods'] ?? TwoFactor::get_default_settings()['allowed_methods'] );
	}

	/**
	 * Ensure the 2FA module is currently enabled before mutating setup state.
	 */
	private function ensure_module_enabled(): ?WP_Error {
		$settings = \SecurWP\Includes\SettingsStore::get(
			\SecurWP\Includes\SettingsStore::COL_TWOFA,
			TwoFactor::get_default_settings()
		);

		if ( ! empty( $settings['enabled'] ) ) {
			return null;
		}

		return new WP_Error(
			'securwp_2fa_module_disabled',
			__( 'Two-factor authentication is currently disabled by the site administrator.', 'securwp' ),
			[ 'status' => 400 ]
		);
	}

	/**
	 * Ensure setup-email delivery is only used for a valid pending email setup.
	 */
	private function ensure_pending_email_setup( int $user_id ): ?WP_Error {
		if ( ! in_array( 'email', $this->get_allowed_methods(), true ) ) {
			return new WP_Error(
				'securwp_2fa_method_not_allowed',
				__( 'Email verification is not currently allowed by your administrator.', 'securwp' ),
				[ 'status' => 400 ]
			);
		}

		$row = $this->get_user_row( $user_id );
		if ( ! $row || ( $row->method ?? '' ) !== 'email' || ! empty( $row->is_enabled ) || ! empty( $row->confirmed_at ) ) {
			return new WP_Error(
				'securwp_2fa_no_pending_email_setup',
				__( 'No pending email-based two-factor setup was found for this account.', 'securwp' ),
				[ 'status' => 400 ]
			);
		}

		return null;
	}

	/**
	 * Ensure the database-backed setup email state is available before use.
	 */
	private function ensure_setup_storage_ready(): ?WP_Error {
		if ( EmailOTPProvider::is_setup_hash_schema_current() ) {
			return null;
		}

		return new WP_Error(
			'securwp_2fa_setup_storage_unavailable',
			__( 'Two-factor setup storage is not ready yet. Please try again in a moment.', 'securwp' ),
			[ 'status' => 503 ]
		);
	}
}
