<?php
/**
 * Two-Factor Authentication Class
 * 
 * Handles email-based 2FA for WordPress login.
 * 
 * SECURITY PHILOSOPHY:
 * "Security must fail open, not fail locked."
 * "This plugin provides security but never becomes the enemy of the site owner."
 *
 * @package Polanger_Dashboard_Tools
 */

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

class PDT_Two_Factor_Auth {

  /**
   * Settings page instance
   */
  private $settings;

  /**
   * Number of recovery codes to generate
   */
  const RECOVERY_CODE_COUNT = 10;

  /**
   * Resend cooldown in seconds
   */
  const RESEND_COOLDOWN = 60;

  /**
   * Trusted device duration in days
   */
  const TRUSTED_DEVICE_DAYS = 30;

  /**
   * Constructor
   */
  public function __construct() {
    add_action( 'init', array( $this, 'init' ) );
  }

  /**
   * Initialize 2FA hooks
   */
  public function init() {
    global $pdt_settings_page;
    $this->settings = $pdt_settings_page;

    if ( ! $this->settings ) {
      return;
    }

    // ═══════════════════════════════════════════════════════════════
    // LAYER 1: wp-config.php BYPASS CHECK (Foundation Layer)
    // This is checked FIRST - if set, 2FA is completely disabled
    // User doesn't need to add anything - this is for emergency only
    // ═══════════════════════════════════════════════════════════════
    if ( defined( 'PDT_DISABLE_2FA' ) && PDT_DISABLE_2FA === true ) {
      // Log this critical event
      $this->log_security_event( '2fa_bypassed_config', 'critical', array(
        'reason' => 'PDT_DISABLE_2FA constant is defined in wp-config.php',
      ) );
      
      // Show admin notice (only once per session)
      add_action( 'admin_notices', array( $this, 'show_bypass_notice' ) );
      
      // Don't initialize any 2FA hooks
      return;
    }

    $providers = apply_filters( 'pdt_2fa_factor_providers', array( 'email' ) );
    if ( ! is_array( $providers ) ) {
      $providers = array( 'email' );
    }

    // Keep this core class as email provider only. Addons can replace/extend providers.
    if ( ! in_array( 'email', $providers, true ) ) {
      return;
    }

    // Hook into authentication
    add_filter( 'authenticate', array( $this, 'check_2fa_requirement' ), 100, 3 );
    
    // Handle 2FA verification form
    add_action( 'login_form_2fa_verify', array( $this, 'handle_2fa_verification' ) );
    
    // Add 2FA verification page
    add_action( 'login_init', array( $this, 'maybe_show_2fa_form' ) );
    
    // Handle resend action
    add_action( 'login_init', array( $this, 'handle_resend_code' ) );
  }

  /**
   * Show admin notice when 2FA is bypassed via wp-config
   */
  public function show_bypass_notice() {
    if ( ! current_user_can( 'manage_options' ) ) {
      return;
    }
    
    // Only show once per day
    $notice_key = 'pdt_2fa_bypass_notice_' . gmdate( 'Y-m-d' );
    if ( get_transient( $notice_key ) ) {
      return;
    }
    set_transient( $notice_key, true, DAY_IN_SECONDS );
    
    ?>
    <div class="notice notice-error">
      <p>
        <strong>⚠️ <?php esc_html_e( 'Security Warning:', 'polanger-admin-menu-manager' ); ?></strong>
        <?php esc_html_e( 'Two-Factor Authentication is currently disabled via wp-config.php (PDT_DISABLE_2FA). Remove this constant to re-enable 2FA protection.', 'polanger-admin-menu-manager' ); ?>
      </p>
    </div>
    <?php
  }

  /**
   * Check if 2FA is required after successful password authentication
   * 
   * SECURITY LAYERS (in order):
   * 1. wp-config bypass (checked in init)
   * 2. Super Admin escape
   * 3. Trusted device check
   * 4. 2FA requirement check
   * 5. Graceful degradation (email fail = bypass)
   */
  public function check_2fa_requirement( $user, $username, $password ) {
    $pre_auth_result = apply_filters( 'pdt_2fa_pre_authenticate', null, $user, $username, $password, $this );
    if ( $pre_auth_result instanceof WP_User || is_wp_error( $pre_auth_result ) ) {
      return $pre_auth_result;
    }

    // If authentication already failed, don't interfere
    if ( is_wp_error( $user ) ) {
      return $user;
    }

    // If no user object, skip
    if ( ! ( $user instanceof WP_User ) ) {
      return $user;
    }

    // ═══════════════════════════════════════════════════════════════
    // METHOD CHECK: Skip email flow if authenticator method is selected
    // The authenticator addon handles its own challenge via pre_authenticate hook
    // ═══════════════════════════════════════════════════════════════
    $configured_method = $this->settings->get_2fa_method();
    if ( 'authenticator' === $configured_method ) {
      // Authenticator method selected - let the addon handle it
      // If addon didn't redirect (user not enrolled), return user without email 2FA
      return $user;
    }

    // ═══════════════════════════════════════════════════════════════
    // LAYER 2: SUPER ADMIN ESCAPE
    // Super Admin (user ID 1) is ALWAYS exempt from 2FA
    // This prevents network lockout in worst case scenarios
    // ═══════════════════════════════════════════════════════════════
    if ( $this->is_super_admin_exempt( $user ) ) {
      $this->log_security_event( '2fa_super_admin_exempt', 'info', array(
        'user_id' => $user->ID,
        'reason'  => 'Super Admin is exempt from 2FA requirement',
      ) );
      return $user;
    }

    // Check if 2FA is required for this user's role
    if ( ! $this->settings->is_2fa_required_for_user( $user ) ) {
      return $user;
    }

    $lockout_remaining = $this->get_lockout_remaining_seconds( $user->ID );
    if ( $lockout_remaining > 0 ) {
      $remaining_minutes = max( 1, (int) ceil( $lockout_remaining / MINUTE_IN_SECONDS ) );
      return new WP_Error(
        'pdt_2fa_locked',
        sprintf(
          /* translators: %d: Remaining lockout time in minutes. */
          __( 'Too many failed verification attempts. Please try again in %d minutes.', 'polanger-admin-menu-manager' ),
          $remaining_minutes
        )
      );
    }

    // ═══════════════════════════════════════════════════════════════
    // LAYER 3: TRUSTED DEVICE CHECK
    // If user has a valid trusted device cookie, skip 2FA
    // ═══════════════════════════════════════════════════════════════
    $trusted_from_filter = apply_filters( 'pdt_2fa_is_trusted_device', null, $user->ID, $this );
    $is_trusted = is_bool( $trusted_from_filter ) ? $trusted_from_filter : $this->is_trusted_device( $user->ID );
    if ( $is_trusted ) {
      $this->log_security_event( '2fa_trusted_device_used', 'info', array(
        'user_id' => $user->ID,
      ) );
      return $user;
    }

    // Generate 2FA code
    $code = $this->generate_code();
    $this->store_pending_auth( $user->ID, $code );
    
    // ═══════════════════════════════════════════════════════════════
    // LAYER 4: GRACEFUL DEGRADATION (Fail-Safe)
    // If email cannot be sent, DO NOT lock the user out
    // Log critical event and allow login
    // "Security must fail open, not fail locked"
    // ═══════════════════════════════════════════════════════════════
    $email_sent = $this->send_code_email( $user, $code );
    
    if ( ! $email_sent ) {
      // Email failed - block login by default, but keep a filter for controlled fail-open behavior.
      $this->log_security_event( '2fa_email_delivery_failed', 'critical', array(
        'user_id' => $user->ID,
        'email'   => $user->user_email,
        'reason'  => 'wp_mail() returned false during 2FA verification flow',
      ) );
      
      // Clean up pending auth
      delete_transient( 'pdt_2fa_code_' . $user->ID );
      
      // Set flag to show admin notice
      set_transient( 'pdt_2fa_email_failed_notice_' . $user->ID, true, HOUR_IN_SECONDS );

      $allow_fail_open = (bool) apply_filters( 'pdt_2fa_fail_open_on_mail_error', false, $user );
      if ( $allow_fail_open ) {
        return $user;
      }

      return new WP_Error(
        'pdt_2fa_email_failed',
        __( 'Unable to send the verification code email. Please try again later or contact the site administrator.', 'polanger-admin-menu-manager' )
      );
    }

    // Store user ID in session for verification
    $token = $this->get_session_token();
    $pending_context = array(
      'user_id'    => $user->ID,
      'remember'   => isset( $_POST['rememberme'] ) && 'forever' === sanitize_text_field( wp_unslash( $_POST['rememberme'] ) ),
      'expires_at' => time() + ( $this->settings->get_2fa_code_expiry() * MINUTE_IN_SECONDS ),
      'created_at' => time(),
    );
    $pending_context = apply_filters( 'pdt_2fa_pending_context', $pending_context, $user, $this );

    set_transient( 'pdt_2fa_pending_' . $token, $pending_context, $this->settings->get_2fa_code_expiry() * MINUTE_IN_SECONDS );

    // Redirect to 2FA verification form
    wp_safe_redirect(
      pdt_get_2fa_challenge_url(
        $pending_context,
        array(
          'action' => '2fa_verify',
          'token'  => $token,
        )
      )
    );
    exit;
  }

  /**
   * Check if user is Super Admin and exempt from 2FA
   * Super Admin (user ID 1 or network super admin) is always exempt
   */
  private function is_super_admin_exempt( $user ) {
    $settings = $this->settings->get_settings();
    if ( ! empty( $settings['two_factor_include_super_admin'] ) ) {
      return false;
    }

    // User ID 1 is always exempt (original admin)
    if ( $user->ID === 1 ) {
      return true;
    }
    
    // In multisite, check if user is a super admin
    if ( is_multisite() && is_super_admin( $user->ID ) ) {
      // Check if there are other super admins with 2FA
      // If this is the ONLY super admin, exempt them
      $super_admins = get_super_admins();
      if ( count( $super_admins ) === 1 ) {
        return true;
      }
    }
    
    return false;
  }

  /**
   * Check if current device is trusted for this user
   */
  private function is_trusted_device( $user_id ) {
    $cookie_name = 'pdt_trusted_device_' . $user_id;
    
    if ( ! isset( $_COOKIE[ $cookie_name ] ) ) {
      return false;
    }
    
    $cookie_value = sanitize_text_field( wp_unslash( $_COOKIE[ $cookie_name ] ) );
    $stored_hash = get_user_meta( $user_id, 'pdt_trusted_device_hash', true );
    
    if ( empty( $stored_hash ) ) {
      return false;
    }
    
    // Verify the cookie matches stored user value and current device fingerprint.
    if ( ! hash_equals( (string) $stored_hash, (string) $cookie_value ) ) {
      return false;
    }

    $device_hash = $this->generate_device_hash( $user_id );
    $expected_value = hash_hmac( 'sha256', $device_hash, wp_salt( 'auth' ) );
    
    return hash_equals( $expected_value, $cookie_value );
  }

  /**
   * Set trusted device cookie for user
   */
  private function set_trusted_device( $user_id ) {
    $device_hash = $this->generate_device_hash( $user_id );
    $cookie_value = hash_hmac( 'sha256', $device_hash, wp_salt( 'auth' ) );
    
    // Store hash in user meta
    update_user_meta( $user_id, 'pdt_trusted_device_hash', $cookie_value );
    update_user_meta( $user_id, 'pdt_trusted_device_created', time() );
    
    // Set cookie
    $cookie_name = 'pdt_trusted_device_' . $user_id;
    $expiry = time() + ( self::TRUSTED_DEVICE_DAYS * DAY_IN_SECONDS );
    
    setcookie( $cookie_name, $cookie_value, $expiry, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true );
    
    $this->log_security_event( '2fa_trusted_device_added', 'info', array(
      'user_id' => $user_id,
      'expires' => gmdate( 'Y-m-d H:i:s', $expiry ),
    ) );
  }

  /**
   * Generate device hash (NOT based on IP - IPs change)
   */
  private function generate_device_hash( $user_id ) {
    $user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '';
    return hash( 'sha256', $user_id . $user_agent . wp_salt( 'auth' ) );
  }

  /**
   * Maybe show 2FA verification form
   */
  public function maybe_show_2fa_form() {
    // phpcs:ignore WordPress.Security.NonceVerification.Recommended
    $action = isset( $_GET['action'] ) ? sanitize_text_field( wp_unslash( $_GET['action'] ) ) : '';
    if ( '2fa_verify' === $action ) {
      $this->render_2fa_form();
      exit;
    }
  }

  /**
   * Handle 2FA verification form submission
   */
  public function handle_2fa_verification() {
    $this->render_2fa_form();
    exit;
  }

  /**
   * Render 2FA verification form
   */
  private function render_2fa_form() {
    // phpcs:ignore WordPress.Security.NonceVerification.Recommended
    $token = isset( $_GET['token'] ) ? sanitize_text_field( wp_unslash( $_GET['token'] ) ) : '';
    // phpcs:ignore WordPress.Security.NonceVerification.Recommended
    $mode = isset( $_GET['mode'] ) ? sanitize_text_field( wp_unslash( $_GET['mode'] ) ) : 'code';
    $mode = in_array( $mode, array( 'code', 'recovery' ), true ) ? $mode : 'code';
    $error_message = '';
    $success_message = '';

    // Check for resend status
    // phpcs:ignore WordPress.Security.NonceVerification.Recommended
    $resend_status = isset( $_GET['resend'] ) ? sanitize_text_field( wp_unslash( $_GET['resend'] ) ) : '';
    if ( $resend_status ) {
      switch ( $resend_status ) {
        case 'success':
          $success_message = __( 'A new verification code has been sent to your email.', 'polanger-admin-menu-manager' );
          break;
        case 'failed':
          $error_message = __( 'Failed to send verification code. Please try again.', 'polanger-admin-menu-manager' );
          break;
        case 'cooldown':
          // phpcs:ignore WordPress.Security.NonceVerification.Recommended
          $wait = isset( $_GET['wait'] ) ? absint( wp_unslash( $_GET['wait'] ) ) : self::RESEND_COOLDOWN;
          $error_message = sprintf(
            /* translators: %d: Number of seconds to wait before requesting another verification code. */
            __( 'Please wait %d seconds before requesting a new code.', 'polanger-admin-menu-manager' ),
            $wait
          );
          break;
      }
    }

    // Handle form submission
    if ( isset( $_POST['pdt_2fa_verify'] ) ) {
      $nonce = isset( $_POST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ) : '';
      if ( ! wp_verify_nonce( $nonce, 'pdt_2fa_verify' ) ) {
        $error_message = __( 'Security check failed. Please try again.', 'polanger-admin-menu-manager' );
      } else {
        $submitted_code = isset( $_POST['verification_code'] ) ? sanitize_text_field( wp_unslash( $_POST['verification_code'] ) ) : '';
        $token = isset( $_POST['token'] ) ? sanitize_text_field( wp_unslash( $_POST['token'] ) ) : '';
        $trust_device = isset( $_POST['trust_device'] ) && '1' === sanitize_text_field( wp_unslash( $_POST['trust_device'] ) );
      
        $result = $this->verify_code( $token, $submitted_code );
      
        if ( is_wp_error( $result ) ) {
          $error_message = $result->get_error_message();
        } else {
          // Code verified, log user in
          $pending = get_transient( 'pdt_2fa_pending_' . $token );
        
          if ( $pending && isset( $pending['user_id'] ) ) {
            // Clean up
            delete_transient( 'pdt_2fa_pending_' . $token );
            delete_transient( 'pdt_2fa_code_' . $pending['user_id'] );
          
            // Set trusted device if requested
            if ( $trust_device ) {
              $this->set_trusted_device( $pending['user_id'] );
            }
          
            // Log successful 2FA
            $this->log_security_event( '2fa_login_success', 'info', array(
              'user_id'        => $pending['user_id'],
              'trusted_device' => $trust_device,
              'remember'       => ! empty( $pending['remember'] ),
            ) );
          
            // Log user in
            wp_set_auth_cookie( $pending['user_id'], ! empty( $pending['remember'] ) );
            wp_set_current_user( $pending['user_id'] );
          
            // Redirect to the resolved post-login destination.
            $verified_user = get_userdata( (int) $pending['user_id'] );
            wp_safe_redirect( pdt_get_2fa_success_redirect_url( $pending, $verified_user ) );
            exit;
          }
        }
      }
    }

    // Check if token is valid
    $pending = get_transient( 'pdt_2fa_pending_' . $token );
    
    if ( ! $pending ) {
      wp_safe_redirect( pdt_get_2fa_back_to_login_url() );
      exit;
    }

    $user = get_userdata( $pending['user_id'] );
    if ( ! $user ) {
      delete_transient( 'pdt_2fa_pending_' . $token );
      wp_safe_redirect( pdt_get_2fa_back_to_login_url( $pending ) );
      exit;
    }
    $masked_email = $this->mask_email( $user->user_email );
    $has_recovery_keys = $this->user_has_recovery_keys( $user->ID );
    $recovery_keys_count = $this->get_recovery_keys_count( $user->ID );
    $is_recovery_mode = ( 'recovery' === $mode );

    wp_enqueue_style(
      'pdt-two-factor-verify',
      plugin_dir_url( dirname( __FILE__ ) ) . 'assets/css/two-factor-verify.css',
      array(),
      PDT_VERSION
    );
    
    ?>
    <!DOCTYPE html>
    <html <?php language_attributes(); ?>>
    <head>
      <meta charset="<?php bloginfo( 'charset' ); ?>">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title><?php esc_html_e( 'Verification Required', 'polanger-admin-menu-manager' ); ?> - <?php bloginfo( 'name' ); ?></title>
      <?php wp_head(); ?>
    </head>
    <body>
      <div class="pdt-2fa-container">
        <div class="pdt-2fa-icon">
          <span class="dashicons dashicons-shield-alt"></span>
        </div>
        
        <h1 class="pdt-2fa-title"><?php esc_html_e( 'Verification Required', 'polanger-admin-menu-manager' ); ?></h1>
        <?php if ( $is_recovery_mode ) : ?>
          <p class="pdt-2fa-description">
            <?php esc_html_e( 'Enter one of your 10-character recovery keys.', 'polanger-admin-menu-manager' ); ?>
          </p>
        <?php else : ?>
          <p class="pdt-2fa-description">
            <?php esc_html_e( 'We sent a 6-digit verification code to', 'polanger-admin-menu-manager' ); ?>
            <br><span class="pdt-2fa-email"><?php echo esc_html( $masked_email ); ?></span>
          </p>
        <?php endif; ?>

        <?php if ( $success_message ) : ?>
          <div class="pdt-2fa-success"><?php echo esc_html( $success_message ); ?></div>
        <?php endif; ?>

        <?php if ( $error_message ) : ?>
          <div class="pdt-2fa-error"><?php echo esc_html( $error_message ); ?></div>
        <?php endif; ?>

        <form method="post">
          <?php wp_nonce_field( 'pdt_2fa_verify' ); ?>
          <input type="hidden" name="token" value="<?php echo esc_attr( $token ); ?>" />
          
          <input type="text" 
                 name="verification_code" 
                 class="pdt-2fa-input" 
                 placeholder="<?php echo $is_recovery_mode ? esc_attr__( 'ABCDE12345', 'polanger-admin-menu-manager' ) : '000000'; ?>"
                 maxlength="10"
                 autocomplete="<?php echo $is_recovery_mode ? 'off' : 'one-time-code'; ?>"
                 autofocus
                 required />
          
          <div class="pdt-2fa-trust">
            <input type="checkbox" name="trust_device" id="trust_device" value="1" />
            <label for="trust_device"><?php esc_html_e( 'Trust this device for 30 days', 'polanger-admin-menu-manager' ); ?></label>
          </div>
          
          <button type="submit" name="pdt_2fa_verify" class="pdt-2fa-submit">
            <?php echo $is_recovery_mode ? esc_html__( 'Verify Recovery Key & Login', 'polanger-admin-menu-manager' ) : esc_html__( 'Verify & Login', 'polanger-admin-menu-manager' ); ?>
          </button>
        </form>

        <div class="pdt-2fa-resend">
          <?php esc_html_e( "Didn't receive the code?", 'polanger-admin-menu-manager' ); ?>
          <a href="<?php echo esc_url( pdt_get_2fa_challenge_url( $pending, array( 'action' => '2fa_resend', 'token' => $token, '_wpnonce' => wp_create_nonce( 'pdt_2fa_resend_' . $token ) ) ) ); ?>">
            <?php esc_html_e( 'Resend', 'polanger-admin-menu-manager' ); ?>
          </a>
          <?php if ( $has_recovery_keys ) : ?>
            <div class="pdt-2fa-recovery">
              <?php if ( $is_recovery_mode ) : ?>
                <?php esc_html_e( 'Got your email code?', 'polanger-admin-menu-manager' ); ?>
                <a href="<?php echo esc_url( pdt_get_2fa_challenge_url( $pending, array( 'action' => '2fa_verify', 'token' => $token, 'mode' => 'code' ) ) ); ?>">
                  <?php esc_html_e( 'Use email verification instead', 'polanger-admin-menu-manager' ); ?>
                </a>
              <?php else : ?>
                <?php esc_html_e( 'Lost access to email?', 'polanger-admin-menu-manager' ); ?>
                <a href="<?php echo esc_url( pdt_get_2fa_challenge_url( $pending, array( 'action' => '2fa_verify', 'token' => $token, 'mode' => 'recovery' ) ) ); ?>">
                  <?php esc_html_e( 'Login with a recovery key', 'polanger-admin-menu-manager' ); ?>
                </a>
                <?php if ( $recovery_keys_count > 0 ) : ?>
                  <span class="pdt-2fa-recovery-count">
                    <?php
                    /* translators: %d: Remaining recovery key count. */
                    printf( esc_html__( '(%d keys available)', 'polanger-admin-menu-manager' ), absint( $recovery_keys_count ) );
                    ?>
                  </span>
                <?php endif; ?>
              <?php endif; ?>
            </div>
          <?php endif; ?>
        </div>

        <div class="pdt-2fa-back">
          <a href="<?php echo esc_url( pdt_get_2fa_back_to_login_url( $pending ) ); ?>">
            &larr; <?php esc_html_e( 'Back to login', 'polanger-admin-menu-manager' ); ?>
          </a>
        </div>
      </div>
      <?php wp_footer(); ?>
    </body>
    </html>
    <?php
  }

  /**
   * Generate a 6-digit verification code
   */
  private function generate_code() {
    return str_pad( wp_rand( 0, 999999 ), 6, '0', STR_PAD_LEFT );
  }

  /**
   * Store pending authentication
   */
  private function store_pending_auth( $user_id, $code ) {
    $expiry = $this->settings->get_2fa_code_expiry() * MINUTE_IN_SECONDS;
    
    set_transient( 'pdt_2fa_code_' . $user_id, array(
      'code'       => wp_hash( $code ),
      'attempts'   => 0,
      'created_at' => time(),
    ), $expiry );
  }

  /**
   * Send verification code via email
   * 
   * @return bool True if email was sent successfully
   */
  private function send_code_email( $user, $code ) {
    $site_name = get_bloginfo( 'name' );
    $expiry_minutes = $this->settings->get_2fa_code_expiry();
    
    $subject = sprintf(
      /* translators: %s: Site name */
      __( '[%s] Your verification code', 'polanger-admin-menu-manager' ),
      $site_name
    );

    $message = sprintf(
      /* translators: 1: User display name, 2: Verification code, 3: Expiry minutes, 4: Site name */
      __( "Hello %1\$s,\n\nYour verification code is: %2\$s\n\nThis code will expire in %3\$d minutes.\n\nIf you did not attempt to log in, please ignore this email or contact the site administrator.\n\n— %4\$s", 'polanger-admin-menu-manager' ),
      $user->display_name,
      $code,
      $expiry_minutes,
      $site_name
    );

    // Return the result of wp_mail for graceful degradation.
    try {
      return (bool) wp_mail( $user->user_email, $subject, $message );
    } catch ( Throwable $exception ) {
      $this->log_security_event( '2fa_email_runtime_error', 'critical', array(
        'user_id' => $user->ID,
        'email'   => $user->user_email,
        'reason'  => $exception->getMessage(),
      ) );
      return false;
    }
  }

  /**
   * Handle resend code action
   */
  public function handle_resend_code() {
    // phpcs:ignore WordPress.Security.NonceVerification.Recommended
    $action = isset( $_GET['action'] ) ? sanitize_text_field( wp_unslash( $_GET['action'] ) ) : '';
    if ( '2fa_resend' !== $action ) {
      return;
    }
    
    // phpcs:ignore WordPress.Security.NonceVerification.Recommended
    $token = isset( $_GET['token'] ) ? sanitize_text_field( wp_unslash( $_GET['token'] ) ) : '';
    // phpcs:ignore WordPress.Security.NonceVerification.Recommended
    $nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : '';
    if ( empty( $token ) || ! wp_verify_nonce( $nonce, 'pdt_2fa_resend_' . $token ) ) {
      wp_safe_redirect( pdt_get_2fa_back_to_login_url() );
      exit;
    }
    $pending = get_transient( 'pdt_2fa_pending_' . $token );
    
    if ( ! $pending ) {
      wp_safe_redirect( pdt_get_2fa_back_to_login_url() );
      exit;
    }
    
    // Check cooldown (60 seconds)
    $last_sent = get_transient( 'pdt_2fa_resend_cooldown_' . $pending['user_id'] );
    if ( $last_sent ) {
      $wait_time = self::RESEND_COOLDOWN - ( time() - $last_sent );
      if ( $wait_time > 0 ) {
        wp_safe_redirect(
          pdt_get_2fa_challenge_url(
            $pending,
            array(
              'action' => '2fa_verify',
              'token'  => $token,
              'resend' => 'cooldown',
              'wait'   => $wait_time,
            )
          )
        );
        exit;
      }
    }
    
    // Generate new code and send
    $user = get_userdata( $pending['user_id'] );
    if ( ! $user ) {
      delete_transient( 'pdt_2fa_pending_' . $token );
      wp_safe_redirect( pdt_get_2fa_back_to_login_url( $pending ) );
      exit;
    }
    $code = $this->generate_code();
    $this->store_pending_auth( $user->ID, $code );
    
    $email_sent = $this->send_code_email( $user, $code );
    
    // Set cooldown
    set_transient( 'pdt_2fa_resend_cooldown_' . $user->ID, time(), self::RESEND_COOLDOWN );
    
    // Log resend
    $this->log_security_event( '2fa_code_resent', 'info', array(
      'user_id' => $user->ID,
      'success' => $email_sent,
    ) );
    
    wp_safe_redirect(
      pdt_get_2fa_challenge_url(
        $pending,
        array(
          'action' => '2fa_verify',
          'token'  => $token,
          'resend' => $email_sent ? 'success' : 'failed',
        )
      )
    );
    exit;
  }

  /**
   * Verify the submitted code (supports both regular code and recovery keys)
   */
  private function verify_code( $token, $submitted_code ) {
    // IMPORTANT: Get pending context FIRST before using in filter
    $pending = get_transient( 'pdt_2fa_pending_' . $token );
    
    if ( ! $pending ) {
      return new WP_Error( 'expired', __( 'Verification session expired. Please try logging in again.', 'polanger-admin-menu-manager' ) );
    }

    // Allow external factor providers (like Authenticator) to verify their own codes
    $provider_result = apply_filters( 'pdt_2fa_verify_submitted_factor', null, $pending, $submitted_code, $this );
    if ( is_bool( $provider_result ) ) {
      return $provider_result;
    }

    // ═══════════════════════════════════════════════════════════════
    // LAYER 5: RECOVERY KEY CHECK
    // If submitted code matches a recovery key, use it and invalidate
    // ═══════════════════════════════════════════════════════════════
    $recovery_result = $this->verify_recovery_key( $pending['user_id'], $submitted_code );
    if ( $recovery_result === true ) {
      $this->log_security_event( '2fa_recovery_key_used', 'warning', array(
        'user_id' => $pending['user_id'],
        'reason'  => 'User logged in using a recovery key',
      ) );
      return true;
    }

    $stored = get_transient( 'pdt_2fa_code_' . $pending['user_id'] );
    
    if ( ! $stored ) {
      return new WP_Error( 'expired', __( 'Verification code expired. Please try logging in again.', 'polanger-admin-menu-manager' ) );
    }

    $max_attempts = $this->settings->get_2fa_max_attempts();

    // Check attempts
    if ( $stored['attempts'] >= $max_attempts ) {
      delete_transient( 'pdt_2fa_pending_' . $token );
      delete_transient( 'pdt_2fa_code_' . $pending['user_id'] );
      $this->set_2fa_lockout( $pending['user_id'] );
      return new WP_Error( 'too_many_attempts', $this->get_lockout_message( $pending['user_id'] ) );
    }

    // Verify code
    if ( ! hash_equals( (string) $stored['code'], (string) wp_hash( $submitted_code ) ) ) {
      // Increment attempts
      $stored['attempts']++;
      set_transient( 'pdt_2fa_code_' . $pending['user_id'], $stored, $this->settings->get_2fa_code_expiry() * MINUTE_IN_SECONDS );

      if ( $stored['attempts'] >= $max_attempts ) {
        delete_transient( 'pdt_2fa_pending_' . $token );
        delete_transient( 'pdt_2fa_code_' . $pending['user_id'] );
        $this->set_2fa_lockout( $pending['user_id'] );
        return new WP_Error( 'too_many_attempts', $this->get_lockout_message( $pending['user_id'] ) );
      }

      $remaining = $max_attempts - $stored['attempts'];
      return new WP_Error( 
        'invalid_code', 
        sprintf( 
          /* translators: %d: Remaining attempts */
          __( 'Invalid verification code or recovery key. %d attempts remaining.', 'polanger-admin-menu-manager' ), 
          $remaining 
        ) 
      );
    }

    return true;
  }

  /**
   * Set lockout transient for failed 2FA attempts.
   *
   * @param int $user_id User ID.
   * @return void
   */
  private function set_2fa_lockout( $user_id ) {
    $lockout_seconds = $this->settings->get_2fa_lockout_minutes() * MINUTE_IN_SECONDS;
    set_transient( 'pdt_2fa_lockout_' . $user_id, time(), $lockout_seconds );
  }

  /**
   * Get remaining lockout time for user in seconds.
   *
   * @param int $user_id User ID.
   * @return int
   */
  private function get_lockout_remaining_seconds( $user_id ) {
    $lockout_started = get_transient( 'pdt_2fa_lockout_' . $user_id );
    if ( false === $lockout_started ) {
      return 0;
    }

    $lockout_seconds = $this->settings->get_2fa_lockout_minutes() * MINUTE_IN_SECONDS;
    $elapsed = time() - absint( $lockout_started );
    $remaining = $lockout_seconds - $elapsed;

    return max( 0, $remaining );
  }

  /**
   * Build lockout message for users.
   *
   * @param int $user_id User ID.
   * @return string
   */
  private function get_lockout_message( $user_id ) {
    $remaining_seconds = $this->get_lockout_remaining_seconds( $user_id );
    $remaining_minutes = max( 1, (int) ceil( $remaining_seconds / MINUTE_IN_SECONDS ) );

    return sprintf(
      /* translators: %d: Remaining lockout time in minutes. */
      __( 'Too many failed attempts. Login is temporarily locked. Please try again in %d minutes.', 'polanger-admin-menu-manager' ),
      $remaining_minutes
    );
  }

  /**
   * Get session token for tracking 2FA flow
   */
  private function get_session_token() {
    if ( ! isset( $_COOKIE['pdt_2fa_session'] ) ) {
      $token = wp_generate_password( 32, false );
      setcookie( 'pdt_2fa_session', $token, time() + HOUR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true );
      return $token;
    }
    return sanitize_text_field( wp_unslash( $_COOKIE['pdt_2fa_session'] ) );
  }

  /**
   * Mask email address for display
   */
  private function mask_email( $email ) {
    $parts = explode( '@', $email );
    $name = $parts[0];
    $domain = $parts[1];
    
    $name_length = strlen( $name );
    if ( $name_length <= 2 ) {
      $masked_name = str_repeat( '*', $name_length );
    } else {
      $masked_name = substr( $name, 0, 2 ) . str_repeat( '*', $name_length - 2 );
    }
    
    return $masked_name . '@' . $domain;
  }

  // ═══════════════════════════════════════════════════════════════════════
  // RECOVERY KEYS SYSTEM
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Generate recovery keys for a user
   * Called automatically when 2FA is first enabled for the user
   * 
   * @param int $user_id User ID
   * @return array Plain text recovery keys (only shown once!)
   */
  public function generate_recovery_keys( $user_id ) {
    $keys = array();
    $hashed_keys = array();
    
    for ( $i = 0; $i < self::RECOVERY_CODE_COUNT; $i++ ) {
      // Generate 10-character alphanumeric key
      $key = strtoupper( wp_generate_password( 10, false ) );
      $keys[] = $key;
      
      // Store hashed version
      $hashed_keys[] = hash_hmac( 'sha256', $key, wp_salt( 'auth' ) );
    }
    
    // Store hashed keys in user meta
    update_user_meta( $user_id, 'pdt_2fa_recovery_keys', $hashed_keys );
    update_user_meta( $user_id, 'pdt_2fa_recovery_keys_generated', time() );
    
    $this->log_security_event( '2fa_recovery_keys_generated', 'info', array(
      'user_id' => $user_id,
      'count'   => self::RECOVERY_CODE_COUNT,
    ) );
    
    // Return plain text keys - these are shown ONLY ONCE to the user
    return $keys;
  }

  /**
   * Verify a recovery key
   * 
   * @param int $user_id User ID
   * @param string $submitted_key The key submitted by user
   * @return bool True if valid, false otherwise
   */
  private function verify_recovery_key( $user_id, $submitted_key ) {
    // Recovery keys are 10 chars, regular codes are 6 digits
    if ( strlen( $submitted_key ) !== 10 ) {
      return false;
    }
    
    $stored_keys = get_user_meta( $user_id, 'pdt_2fa_recovery_keys', true );
    
    if ( empty( $stored_keys ) || ! is_array( $stored_keys ) ) {
      return false;
    }
    
    $submitted_hash = hash_hmac( 'sha256', strtoupper( $submitted_key ), wp_salt( 'auth' ) );
    
    foreach ( $stored_keys as $index => $stored_hash ) {
      if ( hash_equals( $stored_hash, $submitted_hash ) ) {
        // Key matches - invalidate it (one-time use)
        unset( $stored_keys[ $index ] );
        $stored_keys = array_values( $stored_keys ); // Re-index array
        update_user_meta( $user_id, 'pdt_2fa_recovery_keys', $stored_keys );
        
        return true;
      }
    }
    
    return false;
  }

  /**
   * Get remaining recovery keys count for a user
   */
  public function get_recovery_keys_count( $user_id ) {
    $keys = get_user_meta( $user_id, 'pdt_2fa_recovery_keys', true );
    return is_array( $keys ) ? count( $keys ) : 0;
  }

  /**
   * Check if user has recovery keys
   */
  public function user_has_recovery_keys( $user_id ) {
    return $this->get_recovery_keys_count( $user_id ) > 0;
  }

  // ═══════════════════════════════════════════════════════════════════════
  // SECURITY LOGGING
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Log security event to Activity Log
   */
  private function log_security_event( $action, $severity, $metadata = array() ) {
    global $pdt_activity_log;

    if ( $pdt_activity_log && method_exists( $pdt_activity_log, 'is_enabled' ) && ! $pdt_activity_log->is_enabled() ) {
      return;
    }
    
    if ( ! $pdt_activity_log || ( ! method_exists( $pdt_activity_log, 'log_event' ) && ! method_exists( $pdt_activity_log, 'log' ) ) ) {
      // Fallback: store in options if activity log not available
      $logs = get_option( 'pdt_2fa_security_logs', array() );
      $logs[] = array(
        'action'    => $action,
        'severity'  => $severity,
        'metadata'  => $metadata,
        'timestamp' => current_time( 'mysql' ),
        'ip'        => $this->get_client_ip(),
      );
      
      // Keep only last 100 logs
      if ( count( $logs ) > 100 ) {
        $logs = array_slice( $logs, -100 );
      }
      
      update_option( 'pdt_2fa_security_logs', $logs );
      return;
    }
    
    // Use Activity Log if available.
    if ( method_exists( $pdt_activity_log, 'log_event' ) ) {
      $user_id = isset( $metadata['user_id'] ) ? absint( $metadata['user_id'] ) : 0;
      $pdt_activity_log->log_event( $action, $user_id, $metadata );
      return;
    }

    $object_name = sprintf(
      /* translators: %s: Security event action */
      __( '2FA Security Event: %s', 'polanger-admin-menu-manager' ),
      $action
    );
    $pdt_activity_log->log( $action, 'security', null, $object_name, $metadata );
  }

  /**
   * Get client IP address
   */
  private function get_client_ip() {
    $remote_addr = '';
    if ( ! empty( $_SERVER['REMOTE_ADDR'] ) ) {
      $remote_addr = sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) );
      if ( ! filter_var( $remote_addr, FILTER_VALIDATE_IP ) ) {
        $remote_addr = '';
      }
    }

    if ( empty( $remote_addr ) ) {
      return '';
    }

    if ( $this->is_trusted_proxy( $remote_addr ) && ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
      $forwarded_for = sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_FORWARDED_FOR'] ) );
      $candidates = array_map( 'trim', explode( ',', $forwarded_for ) );

      foreach ( $candidates as $candidate ) {
        if ( filter_var( $candidate, FILTER_VALIDATE_IP ) ) {
          return $candidate;
        }
      }
    }

    return $remote_addr;
  }

  /**
   * Check whether a remote address is a trusted proxy.
   *
   * @param string $ip Remote IP.
   * @return bool
   */
  private function is_trusted_proxy( $ip ) {
    $trusted_proxies = apply_filters( 'pdt_2fa_trusted_proxies', array() );

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

    return in_array( $ip, $trusted_proxies, true );
  }
}
