<?php
/**
 * Two-Factor Authentication handling.
 *
 * @package ZA_Creative_Login_Shield
 */

defined( 'ABSPATH' ) || exit;

/**
 * Handles two-factor authentication via email OTP and TOTP (Google Authenticator).
 */
class ZACLS_TwoFA {

	/**
	 * Maximum incorrect OTP attempts before a code is invalidated.
	 */
	const OTP_MAX_ATTEMPTS = 5;

	/**
	 * Login limiter instance.
	 *
	 * @var ZACLS_Login_Limiter
	 */
	private $limiter;

	/**
	 * Auth token for pending 2FA session.
	 *
	 * @var string
	 */
	private $auth_token = '';

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->limiter = new ZACLS_Login_Limiter();
		$this->register_hooks();
	}

	/**
	 * Register WordPress hooks.
	 */
	private function register_hooks() {
		add_action( 'show_user_profile', array( $this, 'user_profile_fields' ) );
		add_action( 'edit_user_profile', array( $this, 'user_profile_fields' ) );
		add_action( 'personal_options_update', array( $this, 'save_user_profile_fields' ) );
		add_action( 'edit_user_profile_update', array( $this, 'save_user_profile_fields' ) );
		add_action( 'wp_ajax_zacls_verify_2fa', array( $this, 'ajax_verify_2fa' ) );
		add_action( 'wp_ajax_nopriv_zacls_verify_2fa', array( $this, 'ajax_verify_2fa' ) );
		add_action( 'wp_ajax_zacls_send_otp', array( $this, 'ajax_send_otp' ) );
		add_action( 'wp_ajax_nopriv_zacls_send_otp', array( $this, 'ajax_send_otp' ) );
		add_action( 'wp_ajax_zacls_setup_2fa', array( $this, 'ajax_setup_2fa' ) );
		add_action( 'login_form', array( $this, 'add_2fa_field_to_login' ) );
		add_action( 'login_enqueue_scripts', array( $this, 'maybe_enqueue_2fa_assets' ) );
		add_filter( 'wp_authenticate_user', array( $this, 'check_2fa_requirement' ), 99, 2 );
	}

	/**
	 * Check if a user needs 2FA verification after authentication.
	 *
	 * @param WP_User|WP_Error $user     User object or WP_Error.
	 * @param string           $password Plaintext password.
	 * @return WP_User|WP_Error
	 */
	public function check_2fa_requirement( $user, $password ) {
		if ( is_wp_error( $user ) || ! $user instanceof WP_User ) {
			return $user;
		}

		if ( ! self::is_2fa_enabled_for_user( $user ) ) {
			return $user;
		}

		$redirect_to = '';
		if ( ! empty( $_POST['redirect_to'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
			$redirect_to = esc_url_raw( wp_unslash( $_POST['redirect_to'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
			$redirect_to = wp_validate_redirect( $redirect_to, admin_url() );
		}

		$token = self::start_2fa_flow( $user, $redirect_to );
		if ( empty( $token ) ) {
			return $user;
		}

		$this->auth_token = $token;

		wp_clear_auth_cookie();
		wp_safe_redirect(
			add_query_arg(
				array(
					'zacls_2fa' => 'required',
					'token'     => rawurlencode( $token ),
				),
				wp_login_url()
			)
		);
		exit;
	}

	/**
	 * Begin a 2FA challenge: create a pending token, send the OTP email, and
	 * return the token. The redirect target is stored server-side.
	 *
	 * @param WP_User|int $user        User object or ID.
	 * @param string      $redirect_to Validated redirect target (optional).
	 * @return string The pending token, or '' on failure.
	 */
	public static function start_2fa_flow( $user, $redirect_to = '' ) {
		if ( is_numeric( $user ) ) {
			$user = get_userdata( (int) $user );
		}
		if ( ! $user instanceof WP_User ) {
			return '';
		}

		if ( ! empty( $redirect_to ) ) {
			$redirect_to = wp_validate_redirect( $redirect_to, admin_url() );
		}

		$token = bin2hex( random_bytes( 32 ) );

		$data = array(
			'user_id'    => $user->ID,
			'redirect_to' => $redirect_to,
			'created_at' => time(),
		);

		set_transient( 'zacls_2fa_' . $token, $data, 10 * MINUTE_IN_SECONDS );

		self::send_otp_email( $user );

		return $token;
	}

	/**
	 * Check if 2FA is enabled or required for a user.
	 *
	 * @param WP_User|int $user User object or ID.
	 * @return bool
	 */
	public static function is_2fa_enabled_for_user( $user ) {
		if ( is_numeric( $user ) ) {
			$user = get_userdata( (int) $user );
		}
		if ( ! $user ) {
			return false;
		}

		$global_enabled = get_option( 'zacls_2fa_enabled', 'no' );
		if ( 'yes' !== $global_enabled ) {
			return false;
		}

		$status = self::get_user_2fa_status( $user->ID );
		if ( $status && ! empty( $status['enabled'] ) ) {
			return true;
		}

		$forced_roles = get_option( 'zacls_2fa_forced_roles', array() );
		if ( ! empty( $forced_roles ) ) {
			$user_roles = (array) $user->roles;
			foreach ( $forced_roles as $role ) {
				if ( in_array( $role, $user_roles, true ) ) {
					return true;
				}
			}
		}

		return false;
	}

	/**
	 * Get 2FA status from database for a user.
	 *
	 * @param int $user_id User ID.
	 * @return array|null
	 */
	public static function get_user_2fa_status( $user_id ) {
		global $wpdb;

		$row = $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
			$wpdb->prepare(
				"SELECT * FROM {$wpdb->prefix}zacls_2fa_status WHERE user_id = %d",
				(int) $user_id
			),
			ARRAY_A
		);
		return $row;
	}

	/**
	 * Hash an OTP code before it is stored.
	 *
	 * @param string $code The plaintext OTP code.
	 * @return string HMAC hash.
	 */
	private static function hash_otp( $code ) {
		return hash_hmac( 'sha256', (string) $code, wp_salt( 'auth' ) );
	}

	/**
	 * Send OTP email to user.
	 *
	 * @param WP_User $user User object.
	 * @return bool Whether the email was queued/sent.
	 */
	public static function send_otp_email( $user ) {
		$otp     = (string) random_int( 100000, 999999 );
		$expires = gmdate( 'Y-m-d H:i:s', time() + 10 * MINUTE_IN_SECONDS );

		global $wpdb;
		$wpdb->insert( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
			$wpdb->prefix . 'zacls_2fa_codes',
			array(
				'user_id'    => $user->ID,
				'code'       => self::hash_otp( $otp ),
				'type'       => 'email',
				'expires_at' => $expires,
				'used'       => 0,
			),
			array( '%d', '%s', '%s', '%s', '%d' )
		);

		$subject = sprintf(
			/* translators: %s: site name */
			esc_html__( '[%s] Your login verification code', 'za-creative-login-shield' ),
			wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES )
		);

		$message = sprintf(
			/* translators: %1$s: display name, %2$s: OTP code, %3$s: site name */
			esc_html__(
				"Hi %1\$s,\n\nYour one-time verification code is: %2\$s\n\nThis code expires in 10 minutes.\n\nIf you didn't request this, please ignore this email.\n\nRegards,\n%3\$s Team",
				'za-creative-login-shield'
			),
			$user->display_name,
			$otp,
			wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES )
		);

		return wp_mail( $user->user_email, $subject, $message );
	}

	/**
	 * Render 2FA fields in user profile.
	 *
	 * @param WP_User $user User object.
	 */
	public function user_profile_fields( $user ) {
		if ( ! current_user_can( 'edit_user', $user->ID ) ) {
			return;
		}

		$status  = self::get_user_2fa_status( $user->ID );
		$enabled = $status && ! empty( $status['enabled'] );
		$method  = $status ? esc_attr( $status['method'] ) : 'email';
		?>
		<h3><?php esc_html_e( 'Two-Factor Authentication', 'za-creative-login-shield' ); ?></h3>
		<table class="form-table">
			<tr>
				<th><label><?php esc_html_e( 'Enable 2FA', 'za-creative-login-shield' ); ?></label></th>
				<td>
					<label>
						<input type="checkbox" name="zacls_2fa_enabled" value="1" <?php checked( $enabled ); ?> />
						<?php esc_html_e( 'Enable two-factor authentication', 'za-creative-login-shield' ); ?>
					</label>
					<p class="description">
						<?php esc_html_e( 'When enabled, you will need to enter a verification code sent to your email after logging in.', 'za-creative-login-shield' ); ?>
					</p>
				</td>
			</tr>
			<tr>
				<th><label><?php esc_html_e( '2FA Method', 'za-creative-login-shield' ); ?></label></th>
				<td>
					<select name="zacls_2fa_method" <?php disabled( ! $enabled ); ?>>
						<option value="email" <?php selected( $method, 'email' ); ?>>
							<?php esc_html_e( 'Email OTP', 'za-creative-login-shield' ); ?>
						</option>
						<option value="totp" <?php selected( $method, 'totp' ); ?>>
							<?php esc_html_e( 'Google Authenticator (TOTP)', 'za-creative-login-shield' ); ?>
						</option>
					</select>
				</td>
			</tr>
			<?php if ( 'totp' === $method && $enabled ) : ?>
			<tr>
				<th><label><?php esc_html_e( 'TOTP Secret', 'za-creative-login-shield' ); ?></label></th>
				<td>
					<p><code><?php echo esc_html( $status['secret_key'] ?? '' ); ?></code></p>
					<p class="description">
						<?php esc_html_e( 'Scan this QR code or enter the secret key in Google Authenticator.', 'za-creative-login-shield' ); ?>
					</p>
				</td>
			</tr>
			<?php endif; ?>
		</table>
		<?php
		wp_nonce_field( 'zacls_2fa_profile_nonce', 'zacls_2fa_profile_nonce' );
	}

	/**
	 * Save 2FA settings from user profile.
	 *
	 * @param int $user_id User ID.
	 */
	public function save_user_profile_fields( $user_id ) {
		if ( ! current_user_can( 'edit_user', $user_id ) ) {
			return;
		}

		if ( ! isset( $_POST['zacls_2fa_profile_nonce'] ) || ! wp_verify_nonce(
			sanitize_text_field( wp_unslash( $_POST['zacls_2fa_profile_nonce'] ) ),
			'zacls_2fa_profile_nonce'
		) ) {
			return;
		}

		$enabled = isset( $_POST['zacls_2fa_enabled'] ) ? 1 : 0;
		$method  = isset( $_POST['zacls_2fa_method'] ) ? sanitize_text_field( wp_unslash( $_POST['zacls_2fa_method'] ) ) : 'email';
		if ( ! in_array( $method, array( 'email', 'totp' ), true ) ) {
			$method = 'email';
		}

		global $wpdb;
		$existing = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
			$wpdb->prepare(
				"SELECT id FROM {$wpdb->prefix}zacls_2fa_status WHERE user_id = %d",
				$user_id
			)
		);

		if ( $existing ) {
			$wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
				$wpdb->prefix . 'zacls_2fa_status',
				array(
					'enabled' => $enabled,
					'method'  => $method,
				),
				array( 'user_id' => $user_id ),
				array( '%d', '%s' ),
				array( '%d' )
			);
		} else {
			$secret = '';
			if ( 'totp' === $method && $enabled ) {
				$secret = $this->generate_totp_secret();
			}
			$wpdb->insert( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
				$wpdb->prefix . 'zacls_2fa_status',
				array(
					'user_id'    => $user_id,
					'method'     => $method,
					'enabled'    => $enabled,
					'secret_key' => $secret,
				),
				array( '%d', '%s', '%d', '%s' )
			);
		}
	}

	/**
	 * Whether the current user/IP has exceeded OTP attempt limits.
	 *
	 * @param int $user_id User ID.
	 * @return bool
	 */
	private static function too_many_otp_attempts( $user_id ) {
		return (int) get_transient( 'zacls_otp_attempts_' . (int) $user_id ) >= self::OTP_MAX_ATTEMPTS;
	}

	/**
	 * Record a failed OTP attempt.
	 *
	 * @param int $user_id User ID.
	 */
	private static function increment_otp_attempts( $user_id ) {
		$key   = 'zacls_otp_attempts_' . (int) $user_id;
		$count = (int) get_transient( $key );
		set_transient( $key, $count + 1, 15 * MINUTE_IN_SECONDS );
	}

	/**
	 * Clear the OTP attempt counter for a user.
	 *
	 * @param int $user_id User ID.
	 */
	private static function reset_otp_attempts( $user_id ) {
		delete_transient( 'zacls_otp_attempts_' . (int) $user_id );
	}

	/**
	 * AJAX: Verify 2FA code.
	 */
	public function ajax_verify_2fa() {
		check_ajax_referer( 'zacls_2fa_nonce', 'nonce' );

		$code  = isset( $_POST['code'] ) ? sanitize_text_field( wp_unslash( $_POST['code'] ) ) : '';
		$token = isset( $_POST['token'] ) ? sanitize_text_field( wp_unslash( $_POST['token'] ) ) : '';

		if ( empty( $code ) || empty( $token ) ) {
			wp_send_json_error( array( 'message' => esc_html__( 'Invalid verification request.', 'za-creative-login-shield' ) ) );
		}

		$pending = self::get_pending_2fa( $token );
		if ( ! $pending ) {
			wp_send_json_error( array( 'message' => esc_html__( 'Session expired. Please login again.', 'za-creative-login-shield' ) ) );
		}

		$user_id = (int) $pending['user_id'];
		$user    = get_userdata( $user_id );
		if ( ! $user ) {
			wp_send_json_error( array( 'message' => esc_html__( 'User not found.', 'za-creative-login-shield' ) ) );
		}

		if ( self::too_many_otp_attempts( $user_id ) ) {
			$this->invalidate_active_codes( $user_id );
			wp_send_json_error( array( 'message' => esc_html__( 'Too many attempts. Please request a new code.', 'za-creative-login-shield' ) ) );
		}

		$status = self::get_user_2fa_status( $user_id );
		if ( $status && 'totp' === $status['method'] && ! empty( $status['secret_key'] ) ) {
			$valid = $this->verify_totp_code( $status['secret_key'], $code );
			if ( ! $valid ) {
				self::increment_otp_attempts( $user_id );
				wp_send_json_error( array( 'message' => esc_html__( 'Invalid verification code.', 'za-creative-login-shield' ) ) );
			}
		} else {
			$verified = $this->verify_otp_code( $user_id, $code );
			if ( is_wp_error( $verified ) ) {
				self::increment_otp_attempts( $user_id );
				wp_send_json_error( array( 'message' => $verified->get_error_message() ) );
			}
		}

		self::reset_otp_attempts( $user_id );

		$this->clear_pending_user( $token );

		wp_set_auth_cookie( $user->ID, false );
		ZACLS_Session::create_for_user( $user );
		do_action( 'zacls_successful_login', $user );

		$redirect = ! empty( $pending['redirect_to'] ) ? $pending['redirect_to'] : admin_url();

		wp_send_json_success(
			array(
				'redirect' => $redirect,
				'message'  => esc_html__( 'Verification successful!', 'za-creative-login-shield' ),
			)
		);
	}

	/**
	 * AJAX: Resend OTP code.
	 */
	public function ajax_send_otp() {
		check_ajax_referer( 'zacls_2fa_nonce', 'nonce' );

		$token = isset( $_POST['token'] ) ? sanitize_text_field( wp_unslash( $_POST['token'] ) ) : '';

		if ( empty( $token ) ) {
			wp_send_json_error( array( 'message' => esc_html__( 'Session expired. Please login again.', 'za-creative-login-shield' ) ) );
		}

		$pending = self::get_pending_2fa( $token );
		if ( ! $pending ) {
			wp_send_json_error( array( 'message' => esc_html__( 'Session expired. Please login again.', 'za-creative-login-shield' ) ) );
		}

		$user = get_userdata( (int) $pending['user_id'] );
		if ( ! $user ) {
			wp_send_json_error( array( 'message' => esc_html__( 'User not found.', 'za-creative-login-shield' ) ) );
		}

		$last_send_key = 'zacls_otp_resend_' . $token;
		if ( get_transient( $last_send_key ) ) {
			wp_send_json_error( array( 'message' => esc_html__( 'Please wait before requesting another code.', 'za-creative-login-shield' ) ) );
		}

		$resend_count = (int) get_transient( 'zacls_otp_resends_' . $token );
		if ( $resend_count >= 3 ) {
			wp_send_json_error( array( 'message' => esc_html__( 'Too many requests. Please try again later.', 'za-creative-login-shield' ) ) );
		}

		$ip_key = 'zacls_otp_ip_' . hash( 'sha256', ZACLS_Main::get_ip() );
		if ( (int) get_transient( $ip_key ) >= 5 ) {
			wp_send_json_error( array( 'message' => esc_html__( 'Too many requests from your network. Please try again later.', 'za-creative-login-shield' ) ) );
		}

		$account_key = 'zacls_otp_account_' . (int) $user->ID;
		if ( (int) get_transient( $account_key ) >= 5 ) {
			wp_send_json_error( array( 'message' => esc_html__( 'Too many requests for this account. Please try again later.', 'za-creative-login-shield' ) ) );
		}

		set_transient( $last_send_key, 1, 60 );
		set_transient( 'zacls_otp_resends_' . $token, $resend_count + 1, 10 * MINUTE_IN_SECONDS );
		set_transient( $ip_key, (int) get_transient( $ip_key ) + 1, HOUR_IN_SECONDS );
		set_transient( $account_key, (int) get_transient( $account_key ) + 1, HOUR_IN_SECONDS );

		self::send_otp_email( $user );

		wp_send_json_success(
			array(
				'message' => esc_html__( 'A new verification code has been sent to your email.', 'za-creative-login-shield' ),
			)
		);
	}

	/**
	 * AJAX: Enable 2FA for the current user.
	 */
	public function ajax_setup_2fa() {
		check_ajax_referer( 'zacls_2fa_nonce', 'nonce' );

		if ( ! is_user_logged_in() ) {
			wp_send_json_error( array( 'message' => esc_html__( 'You must be logged in.', 'za-creative-login-shield' ) ) );
		}

		$user_id = get_current_user_id();
		$method  = isset( $_POST['method'] ) ? sanitize_text_field( wp_unslash( $_POST['method'] ) ) : 'email';
		if ( ! in_array( $method, array( 'email', 'totp' ), true ) ) {
			$method = 'email';
		}

		global $wpdb;

		if ( 'totp' === $method ) {
			$secret = $this->generate_totp_secret();
		} else {
			$secret = '';
		}

		$existing = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
			$wpdb->prepare(
				"SELECT id FROM {$wpdb->prefix}zacls_2fa_status WHERE user_id = %d",
				$user_id
			)
		);

		if ( $existing ) {
			$wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
				$wpdb->prefix . 'zacls_2fa_status',
				array(
					'enabled'    => 1,
					'method'     => $method,
					'secret_key' => $secret,
				),
				array( 'user_id' => $user_id ),
				array( '%d', '%s', '%s' ),
				array( '%d' )
			);
		} else {
			$wpdb->insert( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
				$wpdb->prefix . 'zacls_2fa_status',
				array(
					'user_id'    => $user_id,
					'method'     => $method,
					'enabled'    => 1,
					'secret_key' => $secret,
				),
				array( '%d', '%s', '%d', '%s' )
			);
		}

		$response = array( 'message' => esc_html__( '2FA enabled successfully.', 'za-creative-login-shield' ) );
		if ( 'totp' === $method ) {
			$response['secret'] = $secret;
		}

		wp_send_json_success( $response );
	}

	/**
	 * Get the pending 2FA data for a token.
	 *
	 * @param string $token The auth token.
	 * @return array|null
	 */
	public static function get_pending_2fa( $token ) {
		if ( empty( $token ) ) {
			return null;
		}
		$data = get_transient( 'zacls_2fa_' . $token );
		if ( ! is_array( $data ) || empty( $data['user_id'] ) ) {
			return null;
		}
		return $data;
	}

	/**
	 * Get the user ID from a pending 2FA token.
	 *
	 * @param string $token The auth token.
	 * @return int
	 */
	public function get_pending_user_id( $token ) {
		$pending = self::get_pending_2fa( $token );
		return $pending ? (int) $pending['user_id'] : 0;
	}

	/**
	 * Clear a pending 2FA token.
	 *
	 * @param string $token The auth token.
	 */
	private function clear_pending_user( $token ) {
		delete_transient( 'zacls_2fa_' . $token );
	}

	/**
	 * Invalidate all active OTP codes for a user.
	 *
	 * @param int $user_id User ID.
	 */
	private function invalidate_active_codes( $user_id ) {
		global $wpdb;
		$wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
			$wpdb->prefix . 'zacls_2fa_codes',
			array( 'used' => 2 ),
			array(
				'user_id' => $user_id,
				'used'    => 0,
			),
			array( '%d' ),
			array( '%d', '%d' )
		);
	}

	/**
	 * Verify an OTP code for a user.
	 *
	 * @param int    $user_id User ID.
	 * @param string $code    The OTP code.
	 * @return bool|WP_Error
	 */
	public function verify_otp_code( $user_id, $code ) {
		global $wpdb;

		$stored = $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
			$wpdb->prepare(
				"SELECT * FROM {$wpdb->prefix}zacls_2fa_codes
				WHERE user_id = %d AND used = 0 AND expires_at > %s
				ORDER BY id DESC LIMIT 1",
				$user_id,
				current_time( 'mysql' )
			)
		);

		if ( ! $stored ) {
			return new WP_Error( 'invalid_otp', esc_html__( 'Invalid or expired verification code.', 'za-creative-login-shield' ) );
		}

		if ( (int) $stored->attempt_count >= self::OTP_MAX_ATTEMPTS ) {
			return new WP_Error( 'otp_locked', esc_html__( 'Too many attempts. Please request a new code.', 'za-creative-login-shield' ) );
		}

		$stored_code = (string) $stored->code;
		$valid       = ( 6 === strlen( $stored_code ) && hash_equals( $stored_code, (string) $code ) )
			|| hash_equals( self::hash_otp( $code ), $stored_code );

		if ( $valid ) {
			$wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
				$wpdb->prefix . 'zacls_2fa_codes',
				array( 'used' => 1 ),
				array( 'id' => $stored->id ),
				array( '%d' ),
				array( '%d' )
			);
			return true;
		}

		$new_count = (int) $stored->attempt_count + 1;
		if ( $new_count >= self::OTP_MAX_ATTEMPTS ) {
			$wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
				$wpdb->prefix . 'zacls_2fa_codes',
				array(
					'used'          => 2,
					'attempt_count' => $new_count,
				),
				array( 'id' => $stored->id ),
				array( '%d', '%d' ),
				array( '%d' )
			);
			return new WP_Error( 'otp_max_attempts', esc_html__( 'Too many incorrect attempts. Please request a new code.', 'za-creative-login-shield' ) );
		}

		$wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
			$wpdb->prefix . 'zacls_2fa_codes',
			array( 'attempt_count' => $new_count ),
			array( 'id' => $stored->id ),
			array( '%d' ),
			array( '%d' )
		);

		return new WP_Error( 'invalid_otp', esc_html__( 'Invalid or expired verification code.', 'za-creative-login-shield' ) );
	}

	/**
	 * Verify a TOTP code.
	 *
	 * @param string $secret The TOTP secret.
	 * @param string $code   The TOTP code.
	 * @return bool
	 */
	public function verify_totp_code( $secret, $code ) {
		if ( empty( $secret ) || empty( $code ) ) {
			return false;
		}

		$totp          = $this->generate_totp( $secret );
		$allowed_codes = array( $totp );

		$before          = $this->generate_totp( $secret, -1 );
		$after           = $this->generate_totp( $secret, 1 );
		$allowed_codes[] = $before;
		$allowed_codes[] = $after;

		return in_array( (string) $code, $allowed_codes, true );
	}

	/**
	 * Generate a random TOTP secret (Base32).
	 *
	 * @return string
	 */
	private function generate_totp_secret() {
		$chars  = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
		$secret = '';
		for ( $i = 0; $i < 16; $i++ ) {
			$secret .= $chars[ random_int( 0, 31 ) ];
		}
		return $secret;
	}

	/**
	 * Generate a TOTP code for a given secret and time offset.
	 *
	 * @param string $secret The Base32 secret.
	 * @param int    $offset Time step offset.
	 * @return string
	 */
	private function generate_totp( $secret, $offset = 0 ) {
		if ( ! class_exists( 'ZACLS_Base32' ) ) {
			$this->include_base32();
		}

		$time_slice = (int) floor( time() / 30 ) + $offset;
		$time_bytes = pack( 'N*', 0 ) . pack( 'N*', $time_slice );
		$secret_bin = ZACLS_Base32::decode( $secret );
		$hash       = hash_hmac( 'sha1', $time_bytes, $secret_bin, true );

		$offset_val = ord( $hash[19] ) & 0x0F;
		$otp        = (
			( ( ord( $hash[ $offset_val ] ) & 0x7F ) << 24 ) |
			( ( ord( $hash[ min( $offset_val + 1, 19 ) ] ) & 0xFF ) << 16 ) |
			( ( ord( $hash[ min( $offset_val + 2, 19 ) ] ) & 0xFF ) << 8 ) |
			( ord( $hash[ min( $offset_val + 3, 19 ) ] ) & 0xFF )
		) % 1000000;

		return str_pad( (string) $otp, 6, '0', STR_PAD_LEFT );
	}

	/**
	 * Include the Base32 class if not already loaded.
	 */
	private function include_base32() {
		if ( ! class_exists( 'ZACLS_Base32' ) ) {
			require_once ZACLS_PLUGIN_DIR . 'includes/class-zacls-base32.php';
		}
	}

	/**
	 * Add 2FA form to the login page when required.
	 */
	public function add_2fa_field_to_login() {
		$zacls_2fa = isset( $_GET['zacls_2fa'] ) ? sanitize_text_field( wp_unslash( $_GET['zacls_2fa'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
		if ( 'required' === $zacls_2fa ) {
			$token = isset( $_GET['token'] ) ? sanitize_text_field( wp_unslash( $_GET['token'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
			self::render_2fa_login_form( $token );
		}
	}

	/**
	 * Enqueue 2FA assets for the login page.
	 *
	 * @param string $token The auth token.
	 */
	private static function enqueue_2fa_assets( $token ) {
		wp_enqueue_script(
			'zacls-login-2fa',
			ZACLS_PLUGIN_URL . 'assets/js/login-2fa.js',
			array(),
			ZACLS_VERSION,
			true
		);

		wp_localize_script(
			'zacls-login-2fa',
			'zacls2fa',
			array(
				'token'   => $token,
				'ajaxUrl' => admin_url( 'admin-ajax.php' ),
				'nonce'   => wp_create_nonce( 'zacls_2fa_nonce' ),
				'i18n'    => array(
					'enterCode' => esc_html__( 'Please enter the verification code.', 'za-creative-login-shield' ),
				),
			)
		);
	}

	/**
	 * Enqueue 2FA assets on standard wp-login.php when required.
	 */
	public function maybe_enqueue_2fa_assets() {
		$zacls_2fa = isset( $_GET['zacls_2fa'] ) ? sanitize_text_field( wp_unslash( $_GET['zacls_2fa'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
		if ( 'required' === $zacls_2fa ) {
			$token = isset( $_GET['token'] ) ? sanitize_text_field( wp_unslash( $_GET['token'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
			self::enqueue_2fa_assets( $token );
		}
	}

	/**
	 * Render the 2FA verification form on the login page.
	 *
	 * @param string $token The auth token.
	 */
	public static function render_2fa_login_form( $token = '' ) {
		if ( empty( $token ) ) {
			$token = isset( $_GET['token'] ) ? sanitize_text_field( wp_unslash( $_GET['token'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
		}

		self::enqueue_2fa_assets( $token );
		?>
		<div class="zacls-2fa-form">
			<p><?php esc_html_e( 'Enter the verification code sent to your email.', 'za-creative-login-shield' ); ?></p>
			<p>
				<label for="zacls_2fa_code">
					<?php esc_html_e( 'Verification Code', 'za-creative-login-shield' ); ?>
				</label>
				<input type="text" name="zacls_2fa_code" id="zacls_2fa_code" class="input"
					autocomplete="off" pattern="[0-9]*" inputmode="numeric"
					placeholder="<?php esc_attr_e( 'Enter 6-digit code', 'za-creative-login-shield' ); ?>" />
			</p>
			<p>
				<button type="button" id="zacls_verify_2fa" class="button button-primary">
					<?php esc_html_e( 'Verify', 'za-creative-login-shield' ); ?>
				</button>
				<button type="button" id="zacls_resend_otp" class="button">
					<?php esc_html_e( 'Resend Code', 'za-creative-login-shield' ); ?>
				</button>
			</p>
			<div id="zacls_2fa_message" style="display:none;"></div>
		</div>
		<?php
	}

	/**
	 * Get 2FA adoption statistics.
	 *
	 * @return array
	 */
	public static function get_2fa_stats() {
		global $wpdb;

		$total_users   = count_users();
		$enabled_count = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
			"SELECT COUNT(*) FROM {$wpdb->prefix}zacls_2fa_status WHERE enabled = 1"
		);
		return array(
			'total'   => (int) $total_users['total_users'],
			'enabled' => (int) $enabled_count,
		);
	}
}