<?php
/**
 * Email Verification Feature
 *
 * Handles email verification requirement for new user registrations.
 *
 * @package GuardDog
 * @since 1.9.0
 */

if (!defined('ABSPATH')) exit; // Exit if accessed directly

class Guard_Dog_Email_Verification {

    /**
     * Option key for email verification enabled setting.
     * @var string
     */
    const OPTION_ENABLED = 'guard_dog_email_verification_enabled';

    /**
     * Option key for verification expiry hours.
     * @var string
     */
    const OPTION_EXPIRY = 'guard_dog_email_verification_expiry';

    /**
     * Option key for bypass admin-created users.
     * @var string
     */
    const OPTION_BYPASS_ADMIN = 'guard_dog_email_verification_bypass_admin';

    /**
     * Option key for resend limit per hour.
     * @var string
     */
    const OPTION_RESEND_LIMIT = 'guard_dog_email_verification_resend_limit';

    /**
     * Option key for resend daily limit.
     * @var string
     */
    const OPTION_RESEND_DAILY_LIMIT = 'guard_dog_email_verification_resend_daily_limit';

    /**
     * Instance of the Activity Log handler.
     *
     * @var Guard_Dog_Activity_Log|null
     * @since 1.9.0
     */
    private $activity_log = null;

    /**
     * Instance of the Email Provider.
     *
     * @var Guard_Dog_Email_Provider|null
     * @since 1.9.0
     */
    private $email_provider = null;

    /**
     * Constructor.
     *
     * @since 1.9.0
     * @param Guard_Dog_Activity_Log|null $activity_log Instance of the activity logger.
     * @param Guard_Dog_Email_Provider|null $email_provider Instance of the email provider.
     */
    public function __construct($activity_log = null, $email_provider = null) {
        $this->activity_log = $activity_log;
        $this->email_provider = $email_provider;
        $this->add_hooks();
    }

    /**
     * Refresh hooks based on current enabled status.
     *
     * Useful when the enabled status changes after initialization.
     * Public so tests can call it after changing settings.
     *
     * @since 1.9.0
     */
    public function refresh_hooks() {
        // Remove all hooks first
        $this->remove_hooks();

        // Re-add if enabled
        $this->add_hooks();
    }

    /**
     * Adds the necessary WordPress hooks.
     *
     * @since 1.9.0
     */
    private function add_hooks() {
        // Only add hooks if feature is enabled
        if (!get_option(self::OPTION_ENABLED, false)) {
            return;
        }

        // Remove WordPress default notification on register_new_user hook
        // Use init hook to ensure WordPress default filters are loaded first
        add_action('init', [$this, 'remove_default_user_notification'], 5);
        
        // Also disable via filter as backup
        add_filter('wp_send_new_user_notification_to_user', [$this, 'disable_default_user_notification'], 5, 2);

        // Add password fields to registration form
        add_action('register_form', [$this, 'add_password_fields_to_registration_form']);


        // Hook into registration to capture and validate password from POST
        add_action('register_post', [$this, 'capture_registration_password'], 10, 3);

        // Registration hooks
        add_action('user_register', [$this, 'set_user_password_from_registration'], 5, 1); // Priority 5 to run before handle_user_registration
        add_action('user_register', [$this, 'handle_user_registration'], 10, 1);

        // Authentication hooks - priority 30 to run after password check
        add_filter('authenticate', [$this, 'check_email_verification'], 30, 3);

        // Display verification success message on login page
        add_filter('wp_login_errors', [$this, 'display_verification_success_message'], 10, 2);

        // Email change hook
        add_action('profile_update', [$this, 'handle_email_change'], 10, 2);

        // Admin post handlers
        add_action('admin_post_guard_dog_verify_email', [$this, 'handle_verification_link']);
        add_action('admin_post_nopriv_guard_dog_verify_email', [$this, 'handle_verification_link']);
        add_action('admin_post_guard_dog_resend_verification', [$this, 'handle_resend_verification']);
        add_action('admin_post_nopriv_guard_dog_resend_verification', [$this, 'handle_resend_verification']);
        add_action('admin_post_guard_dog_admin_verify_user', [$this, 'handle_admin_verify_user']);

        // User list columns
        add_filter('manage_users_columns', [$this, 'add_verification_column']);
        add_filter('manage_users_custom_column', [$this, 'render_verification_column'], 10, 3);
    }

    /**
     * Remove all WordPress hooks.
     *
     * @since 1.9.0
     */
    private function remove_hooks() {
        remove_action('init', [$this, 'remove_default_user_notification'], 5);
        remove_filter('wp_send_new_user_notification_to_user', [$this, 'disable_default_user_notification'], 5);
        remove_action('register_form', [$this, 'add_password_fields_to_registration_form']);
        remove_action('register_post', [$this, 'capture_registration_password'], 10);
        remove_action('user_register', [$this, 'set_user_password_from_registration'], 5);
        remove_action('user_register', [$this, 'handle_user_registration'], 10);
        remove_filter('authenticate', [$this, 'check_email_verification'], 30);
        remove_filter('wp_login_errors', [$this, 'display_verification_success_message'], 10);
        remove_action('profile_update', [$this, 'handle_email_change'], 10);
        remove_action('admin_post_guard_dog_verify_email', [$this, 'handle_verification_link']);
        remove_action('admin_post_nopriv_guard_dog_verify_email', [$this, 'handle_verification_link']);
        remove_action('admin_post_guard_dog_resend_verification', [$this, 'handle_resend_verification']);
        remove_action('admin_post_nopriv_guard_dog_resend_verification', [$this, 'handle_resend_verification']);
        remove_action('admin_post_guard_dog_admin_verify_user', [$this, 'handle_admin_verify_user']);
        remove_filter('manage_users_columns', [$this, 'add_verification_column']);
        remove_filter('manage_users_custom_column', [$this, 'render_verification_column'], 10);
    }

    /**
     * Remove WordPress default user notification action.
     * Called on init to ensure WordPress default filters are loaded first.
     *
     * @since 1.9.0
     * @return void
     */
    public function remove_default_user_notification() {
        // Remove WordPress default notification
        remove_action('register_new_user', 'wp_send_new_user_notifications');
        // Add our own handler that only sends admin notification
        add_action('register_new_user', [$this, 'handle_register_new_user'], 10, 1);
    }

    /**
     * Handle register_new_user action to prevent default user notification.
     * We send our own verification email instead.
     *
     * @since 1.9.0
     * @param int $user_id The user ID.
     * @return void
     */
    public function handle_register_new_user($user_id) {
        // Only send admin notification, skip user notification (we send verification email instead)
        // Check if we should bypass for admin-created users
        if (get_option(self::OPTION_BYPASS_ADMIN, true) && $this->is_admin_created_user()) {
            // Allow default notifications for admin-created users
            wp_send_new_user_notifications($user_id, 'both');
        } else {
            // Only send admin notification, skip user notification
            wp_send_new_user_notifications($user_id, 'admin');
        }
    }

    /**
     * Disable WordPress default user registration email.
     * We send our own verification email instead.
     *
     * @since 1.9.0
     * @param bool $send Whether to send the email.
     * @param WP_User $user User object for new user.
     * @return bool False to disable default email.
     */
    public function disable_default_user_notification($send, $user) {
        // Disable default user notification - we send verification email instead
        return false;
    }

    /**
     * Add password fields to registration form.
     *
     * @since 1.9.0
     * @return void
     */
    public function add_password_fields_to_registration_form() {
        wp_enqueue_script('password-strength-meter');
        wp_enqueue_style('guard-dog-user-registration', GUARD_DOG_URL . 'assets/css/guard-dog-user-registration.css', [], GUARD_DOG_VERSION);
        
        // Enqueue password policy script if password policy is enabled or if we just want to show requirements
        // We'll always show requirements based on current settings
        if (file_exists(GUARD_DOG_PATH . 'assets/js/guard-dog-password-policy.js')) {
            wp_enqueue_script('guard-dog-password-policy', GUARD_DOG_URL . 'assets/js/guard-dog-password-policy.js', ['jquery', 'password-strength-meter'], GUARD_DOG_VERSION, true);
            
            // Localize script with password policy settings (even if not enforced, show what's configured)
            wp_localize_script('guard-dog-password-policy', 'guardDogPasswordPolicy', [
                'minLength' => get_option('guard_dog_password_min_length', 12),
                'requireUppercase' => get_option('guard_dog_password_require_uppercase', true),
                'requireLowercase' => get_option('guard_dog_password_require_lowercase', true),
                'requireNumbers' => get_option('guard_dog_password_require_numbers', true),
                'requireSpecial' => get_option('guard_dog_password_require_special', true),
                'blockUsername' => get_option('guard_dog_password_block_username', true),
                'blockEmail' => get_option('guard_dog_password_block_email', true),
                'ajaxUrl' => admin_url('admin-ajax.php'),
                'nonce' => wp_create_nonce('guard_dog_validate_password'),
            ]);
        }
        
        ?>
        <style>
            /* Hide default WordPress registration message since we require password and send verification email */
            #reg_passmail {
                display: none !important;
            }
        </style>
        <p>
            <label for="pass1"><?php esc_html_e('Password', 'guard-dog'); ?> <span class="required">*</span></label>
            <input type="password" name="pass1" id="pass1" class="input password-input" size="20" value="" autocomplete="new-password" required="required" />
        </p>
        <p>
            <label for="pass2"><?php esc_html_e('Repeat Password', 'guard-dog'); ?> <span class="required">*</span></label>
            <input type="password" name="pass2" id="pass2" class="input" size="20" value="" autocomplete="new-password" required="required" />
        </p>
        <!-- Password requirements will be inserted here by JavaScript (below pass2) -->
        <div id="guard-dog-password-requirements" class="guard-dog-password-requirements" style="display: none;"></div>
        <!-- Password match message -->
        <div id="guard-dog-password-match" class="guard-dog-password-match" style="display: none;"></div>
        <div id="pass-strength-result" class="hide-if-no-js" aria-live="polite"></div>
        <?php
    }

    /**
     * Capture password from registration form POST data.
     *
     * @since 1.9.0
     * @param string $sanitized_user_login Sanitized username.
     * @param string $user_email User email.
     * @param WP_Error $errors Errors object.
     * @return void
     */
    public function capture_registration_password($sanitized_user_login, $user_email, $errors) {
        // phpcs:disable WordPress.Security.NonceVerification.Missing
        // Nonce verification is not applicable in this context:
        // 1. This hook runs during WordPress core registration (register_post action)
        // 2. WordPress core registration form doesn't include Guard Dog nonces
        // 3. WordPress core handles its own CSRF protection for the registration form
        // 4. We're only validating password requirements, not performing privileged actions
        // 5. All data is properly sanitized before use

        // Validate password fields are present and match
        if (!isset($_POST['pass1']) || empty($_POST['pass1'])) {
            $errors->add('empty_password', __('<strong>Error:</strong> Please enter a password.', 'guard-dog'));
            return;
        }

        if (!isset($_POST['pass2']) || empty($_POST['pass2'])) {
            $errors->add('empty_password', __('<strong>Error:</strong> Please confirm your password.', 'guard-dog'));
            return;
        }

        $pass1 = sanitize_text_field(wp_unslash($_POST['pass1']));
        $pass2 = sanitize_text_field(wp_unslash($_POST['pass2']));

        // phpcs:enable WordPress.Security.NonceVerification.Missing

        if ($pass1 !== $pass2) {
            $errors->add('password_mismatch', __('<strong>Error:</strong> Passwords do not match.', 'guard-dog'));
            return;
        }

        // Store password temporarily in a global array keyed by username+email hash
        // This will be retrieved in set_user_password_from_registration
        if (!isset($GLOBALS['guard_dog_reg_passwords'])) {
            $GLOBALS['guard_dog_reg_passwords'] = [];
        }
        $GLOBALS['guard_dog_reg_passwords'][md5($sanitized_user_login . $user_email)] = $pass1;
    }

    /**
     * Set user password from registration form POST data.
     * Runs on user_register hook with priority 5 (before handle_user_registration).
     *
     * @since 1.9.0
     * @param int $user_id The user ID.
     * @return void
     */
    public function set_user_password_from_registration($user_id) {
        $user = get_userdata($user_id);
        if (!$user) {
            return;
        }

        // Retrieve password from global array
        if (!isset($GLOBALS['guard_dog_reg_passwords'])) {
            return;
        }

        $password_key = md5($user->user_login . $user->user_email);
        if (!isset($GLOBALS['guard_dog_reg_passwords'][$password_key])) {
            return;
        }

        $password = $GLOBALS['guard_dog_reg_passwords'][$password_key];
        
        // Update user password
        wp_set_password($password, $user_id);
        
        // Clean up
        unset($GLOBALS['guard_dog_reg_passwords'][$password_key]);
        
        // Store password in history if password policy is enabled
        // Get the hashed password from user data after password update
        if (class_exists('Guard_Dog_Password_Validator')) {
            $user_data = get_userdata($user_id);
            if ($user_data && !empty($user_data->user_pass)) {
                Guard_Dog_Password_Validator::store_password_history($user_id, $user_data->user_pass);
            }
        }
    }

    /**
     * Handle new user registration.
     *
     * @since 1.9.0
     * @param int $user_id The user ID.
     * @return void
     */
    public function handle_user_registration($user_id) {
        // Skip if user was created by admin and bypass is enabled
        if (get_option(self::OPTION_BYPASS_ADMIN, true) && $this->is_admin_created_user()) {
            // Mark as verified automatically
            $this->mark_user_verified($user_id);
            return;
        }

        // Send verification email
        $this->send_verification_email($user_id);
    }

    /**
     * Send verification email to user.
     *
     * @since 1.9.0
     * @param int $user_id The user ID.
     * @return bool Success status.
     */
    public function send_verification_email($user_id) {
        $user = get_userdata($user_id);
        if (!$user) {
            return false;
        }

        // Generate verification token
        $token_data = $this->generate_verification_token($user_id);
        if (!$token_data) {
            return false;
        }

        // Build verification URL
        $verification_url = add_query_arg([
            'action' => 'guard_dog_verify_email',
            'token' => $token_data['token'],
            'key' => $token_data['key'],
        ], admin_url('admin-post.php'));

        // Get expiry hours
        $expiry_hours = get_option(self::OPTION_EXPIRY, 48);

        $subject = null;
        if ( class_exists( 'Guard_Dog_Email_Template_Customizer' ) ) {
            $customizer = new Guard_Dog_Email_Template_Customizer( array( 'template_type' => 'email-verification' ) );
            $subject    = $customizer->get_email_subject( array( 'user' => $user, 'site_name' => get_bloginfo( 'name' ), 'name' => $user->display_name ) );
        }
        if ( $subject === null || $subject === '' ) {
            // Translators: %s is the site name.
            $subject = sprintf( __( '[%s] Verify Your Email Address', 'guard-dog' ), get_bloginfo( 'name' ) );
        }

        // Try to load email template
        $email_template_path = GUARD_DOG_PATH . 'includes/views/emails/email-verification.php';
        if (file_exists($email_template_path)) {
            $message = Guard_Dog_View_Loader::load_partial('emails/email-verification', [
                'user_name' => $user->display_name,
                'verification_link' => $verification_url,
                'expires_in' => $expiry_hours,
                'site_name' => get_bloginfo('name'),
                'site_url' => home_url(),
            ], 'includes/views');
        } else {
            // Fallback simple email
            $message = sprintf(
            // Translators: %1$s is the user display name, %2$s is the verification link URL, %3$d is the expiration hours, %4$s is the site name.
                __('Hello %1$s,

Please verify your email address by clicking the following link:

%2$s

This link will expire in %3$d hours.

If you did not create an account, please ignore this email.

---
%4$s', 'guard-dog'),
                esc_html($user->display_name),
                esc_url($verification_url),
                $expiry_hours,
                get_bloginfo('name')
            );
        }

        // Try plain text version
        $plain_text_path = GUARD_DOG_PATH . 'includes/views/emails/plain/email-verification-plain.php';
        $text_message = '';
        if (file_exists($plain_text_path)) {
            $text_message = Guard_Dog_View_Loader::load_partial('emails/plain/email-verification-plain', [
                'user_name' => $user->display_name,
                'verification_link' => $verification_url,
                'expires_in' => $expiry_hours,
                'site_name' => get_bloginfo('name'),
                'site_url' => home_url(),
            ], 'includes/views');
        }

        // Send email via Guard Dog Email Provider if configured, otherwise wp_mail
        if ($this->email_provider && $this->email_provider->is_configured()) {
            $result = $this->email_provider->send_email($user->user_email, $subject, $message, $text_message);
            if (!$result['success']) {
                // Log error but don't block registration
                if ($this->activity_log) {
                    $this->activity_log->add_log_entry('email_verification_sent', [
                        'user_id' => $user_id,
                        'username' => $user->user_login,
                        'details' => 'Failed: ' . $result['message'],
                    ]);
                }
                // Log the failure for debugging
                if (function_exists('Guard_Dog') && Guard_Dog()->debug) {
                    Guard_Dog()->debug->error('Email verification email failed to send', 'email_verification', [
                        'user_id' => $user_id,
                        'email' => $user->user_email,
                        'error' => $result['message'],
                    ]);
                }
                // Fallback to wp_mail only if not rate limited (429)
                // Rate limit errors should not fallback to wp_mail as it will also hit rate limit
                if (strpos($result['message'], '429') === false && strpos($result['message'], 'rate limit') === false) {
                    wp_mail($user->user_email, $subject, $message);
                }
            } else {
                // Log success
                if ($this->activity_log) {
                    $this->activity_log->add_log_entry('email_verification_sent', [
                        'user_id' => $user_id,
                        'username' => $user->user_login,
                    ]);
                }
                if (function_exists('Guard_Dog') && Guard_Dog()->debug) {
                    Guard_Dog()->debug->info('Email verification email sent successfully', 'email_verification', [
                        'user_id' => $user_id,
                        'email' => $user->user_email,
                    ]);
                }
            }
        } else {
            // Use wp_mail fallback
            wp_mail($user->user_email, $subject, $message);
            if ($this->activity_log) {
                $this->activity_log->add_log_entry('email_verification_sent', [
                    'user_id' => $user_id,
                    'username' => $user->user_login,
                    'details' => 'Sent via wp_mail (provider not configured)',
                ]);
            }
        }

        return true;
    }

    /**
     * Generate verification token for user.
     *
     * @since 1.9.0
     * @param int $user_id The user ID.
     * @return array|false Array with 'token' and 'key', or false on failure.
     */
    private function generate_verification_token($user_id) {
        global $wpdb;
        
        $user = get_userdata($user_id);
        if (!$user) {
            return false;
        }

        // Generate secure tokens
        $token = bin2hex(random_bytes(32)); // 64 character hex string
        $key = bin2hex(random_bytes(32)); // 64 character hex string for database lookup

        // Hash tokens for storage
        $hashed_token = wp_hash($token);
        $hashed_key = wp_hash($key);

        // Calculate expiry time
        $expiry_hours = get_option(self::OPTION_EXPIRY, 48);
        $expires_at = gmdate('Y-m-d H:i:s', time() + ($expiry_hours * HOUR_IN_SECONDS));

        $table_name = Guard_Dog()->db->get_email_verification_table_name();
        $existing_verification = $this->get_verification_status($user_id);

        // Ensure table exists before using it (only check once per request)
        // Use transient to prevent repeated checks/installations during the same request
        $checked_transient = 'guard_dog_table_check_' . get_current_blog_id();
        if (!get_transient($checked_transient)) {
            if (!Guard_Dog()->db->tables_exist()) {
                Guard_Dog()->db->install_tables();
            }
            // Set transient for 1 minute to prevent repeated checks
            set_transient($checked_transient, true, MINUTE_IN_SECONDS);
        }

        // Delete any existing verification records for this user
        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
        // Caching is not appropriate for email verification operations as they need to be real-time and accurate.
        // This is a necessary database delete operation for security functionality.
        $wpdb->delete(
            $table_name,
            ['user_id' => $user_id],
            ['%d']
        );
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching

        // Insert new verification record
        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
        // Caching is not appropriate for email verification operations as they need to be real-time and accurate.
        // This is a necessary database insert operation for security functionality.
        $result = $wpdb->insert(
            $table_name,
            [
                'user_id' => $user_id,
                'verification_token' => $hashed_token,
                'verification_key' => $hashed_key,
                'email' => $user->user_email,
                'created_at' => current_time('mysql'),
                'expires_at' => $expires_at,
                'verified_at' => null,
                'resend_count' => isset($existing_verification->resend_count) ? (int) $existing_verification->resend_count : 0,
                'last_resend_at' => $existing_verification->last_resend_at ?? null,
            ],
            ['%d', '%s', '%s', '%s', '%s', '%s', '%s', '%d', '%s']
        );
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching

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

        return [
            'token' => $token,
            'key' => $key,
        ];
    }

    /**
     * Handle verification link click.
     *
     * @since 1.9.0
     * @return void
     */
    public function handle_verification_link() {
        // Verify nonce if present
        if (isset($_GET['_wpnonce']) && !wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'])), 'guard_dog_verify_email')) {
            wp_die(esc_html__('Security check failed.', 'guard-dog'), esc_html__('Verification Failed', 'guard-dog'), ['response' => 403]);
        }

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

        if (empty($token) || empty($key)) {
            wp_die(esc_html__('Invalid verification link.', 'guard-dog'), esc_html__('Verification Failed', 'guard-dog'), ['response' => 400]);
        }

        $result = $this->verify_user_email($token, $key);
        
        if ($result['success']) {
            // Password was already set during registration, so just redirect to login
            $redirect_url = wp_login_url();
            $redirect_url = add_query_arg('verified', '1', $redirect_url);
            wp_safe_redirect($redirect_url);
            exit;
        } else {
            wp_die(
                esc_html($result['message']),
                esc_html__('Verification Failed', 'guard-dog'),
                ['response' => 400]
            );
        }
    }

    /**
     * Verify user email with token and key.
     *
     * @since 1.9.0
     * @param string $token The verification token.
     * @param string $key The verification key.
     * @return array Result array with 'success' boolean and 'message' string.
     */
    private function verify_user_email($token, $key) {
        global $wpdb;

        $hashed_token = wp_hash($token);
        $hashed_key = wp_hash($key);

        $table_name = Guard_Dog()->db->get_email_verification_table_name();

        // Find verification record
        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching
        // Table names are validated and sanitized, then escaped with esc_sql() for Plugin Check compliance.
        // Caching is not appropriate for email verification operations as they need to be real-time and accurate.
        // This is a necessary database query for security validation functionality.
        $escaped_table_name = esc_sql( $table_name );
        $verification = $wpdb->get_row(
            $wpdb->prepare(
                "SELECT * FROM $escaped_table_name WHERE verification_token = %s AND verification_key = %s",
                $hashed_token,
                $hashed_key
            )
        );
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching

        if (!$verification) {
            if ($this->activity_log) {
                $this->activity_log->add_log_entry('email_verification_failed', [
                    'details' => 'Invalid token/key combination',
                ]);
            }
            return [
                'success' => false,
                'message' => __('Invalid verification link.', 'guard-dog'),
            ];
        }

        // Check if already verified
        if (!empty($verification->verified_at)) {
            return [
                'success' => false,
                'message' => __('This email address has already been verified.', 'guard-dog'),
            ];
        }

        // Check if expired
        $expires_timestamp = strtotime($verification->expires_at);
        if ($expires_timestamp < time()) {
            if ($this->activity_log) {
                $this->activity_log->add_log_entry('email_verification_expired', [
                    'user_id' => $verification->user_id,
                ]);
            }
            return [
                'success' => false,
                'message' => __('This verification link has expired. Please request a new one.', 'guard-dog'),
            ];
        }

        // Verify the user
        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching
        // Caching is not appropriate for email verification operations as they need to be real-time and accurate.
        // This is a necessary database update operation for security functionality.
        $wpdb->update(
            $table_name,
            ['verified_at' => current_time('mysql')],
            ['id' => $verification->id],
            ['%s'],
            ['%d']
        );
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching

        if ($this->activity_log) {
            $this->activity_log->add_log_entry('email_verification_verified', [
                'user_id' => $verification->user_id,
            ]);
        }

        return [
            'success' => true,
            'message' => __('Email address verified successfully. You can now set your password.', 'guard-dog'),
            'user_id' => $verification->user_id,
        ];
    }

    /**
     * Check if user email is verified during authentication.
     *
     * @since 1.9.0
     * @param WP_User|WP_Error|null $user The user object or error.
     * @param string $username The username.
     * @param string $password The password.
     * @return WP_User|WP_Error The user object or error.
     */
    public function check_email_verification($user, $username, $password) {
        // If user authentication failed, don't interfere
        if (!$user || is_wp_error($user)) {
            return $user;
        }

        // Check if user is verified
        if (!$this->is_user_verified($user->ID)) {
            // Log failed login attempt
            if ($this->activity_log) {
                $this->activity_log->add_log_entry('email_verification_failed', [
                    'user_id' => $user->ID,
                    'username' => $username,
                    'ip_address' => Guard_Dog_Utils::get_user_ip(),
                    'details' => 'Login blocked - email not verified',
                ]);
            }

            $resend_url = wp_nonce_url(
                admin_url('admin-post.php?action=guard_dog_resend_verification&user_id=' . $user->ID),
                'guard_dog_resend_verification'
            );
            $error_message = sprintf(
            // Translators: %s is the URL to resend verification email.
                __('Please verify your email address before logging in. <a href="%s">Resend verification email</a>', 'guard-dog'),
                esc_url($resend_url)
            );
            return new WP_Error(
                'email_not_verified',
                $error_message
            );
        }

        return $user;
    }

    /**
     * Handle email change - require re-verification.
     *
     * @since 1.9.0
     * @param int $user_id The user ID.
     * @param WP_User $old_user_data The old user data.
     * @return void
     */
    public function handle_email_change($user_id, $old_user_data) {
        $user = get_userdata($user_id);
        if (!$user) {
            return;
        }

        // Check if email changed
        if ($user->user_email !== $old_user_data->user_email) {
            // Mark user as unverified
            $this->mark_user_unverified($user_id);
            // Send new verification email
            $this->send_verification_email($user_id);
        }
    }

    /**
     * Handle resend verification email request.
     *
     * @since 1.9.0
     * @return void
     */
    public function handle_resend_verification() {
        // Verify nonce
        if (!isset($_GET['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'])), 'guard_dog_resend_verification')) {
            wp_die(esc_html__('Security check failed.', 'guard-dog'), esc_html__('Verification Failed', 'guard-dog'), ['response' => 403]);
        }

        $user_id = isset($_GET['user_id']) ? absint($_GET['user_id']) : 0;
        if (!$user_id) {
            wp_die(esc_html__('Invalid user ID.', 'guard-dog'), esc_html__('Error', 'guard-dog'), ['response' => 400]);
        }

        // Check rate limits
        if (!$this->can_resend_verification($user_id)) {
            wp_die(
                esc_html__('Too many resend requests. Please try again later.', 'guard-dog'),
                esc_html__('Rate Limit Exceeded', 'guard-dog'),
                ['response' => 429]
            );
        }

        // Resend email
        $this->send_verification_email($user_id);
        $this->increment_resend_count($user_id);

        if ($this->activity_log) {
            $this->activity_log->add_log_entry('email_verification_resend', [
                'user_id' => $user_id,
            ]);
        }

        // Redirect with success message
        $redirect_url = wp_login_url();
        $redirect_url = add_query_arg('verification_resent', '1', $redirect_url);
        wp_safe_redirect($redirect_url);
        exit;
    }

    /**
     * Check if user can resend verification email (rate limiting).
     *
     * @since 1.9.0
     * @param int $user_id The user ID.
     * @return bool True if can resend, false otherwise.
     */
    private function can_resend_verification($user_id) {
        global $wpdb;

        $table_name = Guard_Dog()->db->get_email_verification_table_name();

        // Get current verification record
        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching
        // Table names are validated and sanitized, then escaped with esc_sql() for Plugin Check compliance.
        // Caching is not appropriate for email verification rate limiting as it needs to be real-time and accurate.
        // This is a necessary database query for security rate limiting functionality.
        $escaped_table_name = esc_sql( $table_name );
        $verification = $wpdb->get_row(
            $wpdb->prepare("SELECT * FROM $escaped_table_name WHERE user_id = %d ORDER BY id DESC LIMIT 1", $user_id)
        );
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching

        if (!$verification) {
            return true; // No previous attempts
        }

        $resend_limit = get_option(self::OPTION_RESEND_LIMIT, 3);
        $daily_limit = get_option(self::OPTION_RESEND_DAILY_LIMIT, 5);

        // Check hourly limit
        if ($verification->last_resend_at) {
            $last_resend_timestamp = strtotime($verification->last_resend_at);
            $one_hour_ago = time() - HOUR_IN_SECONDS;
            
            if ($last_resend_timestamp > $one_hour_ago) {
                // Check count in last hour
                // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching
                // Table names are validated and sanitized, then escaped with esc_sql() for Plugin Check compliance.
                // Caching is not appropriate for email verification rate limiting as it needs to be real-time and accurate.
                // This is a necessary database query for security rate limiting functionality.
                $recent_count = $wpdb->get_var(
                    $wpdb->prepare(
                        "SELECT resend_count FROM $escaped_table_name WHERE user_id = %d AND last_resend_at > %s ORDER BY id DESC LIMIT 1",
                        $user_id,
                        gmdate('Y-m-d H:i:s', $one_hour_ago)
                    )
                );
                // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching
                
                if ($recent_count >= $resend_limit) {
                    return false;
                }
            }
        }

        // Check daily limit
        $one_day_ago = time() - DAY_IN_SECONDS;
        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching
        // Table names are validated and sanitized, then escaped with esc_sql() for Plugin Check compliance.
        // Caching is not appropriate for email verification rate limiting as it needs to be real-time and accurate.
        // This is a necessary database query for security rate limiting functionality.
        $daily_count = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(*) FROM $escaped_table_name WHERE user_id = %d AND created_at > %s",
                $user_id,
                gmdate('Y-m-d H:i:s', $one_day_ago)
            )
        );
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching

        return $daily_count < $daily_limit;
    }

    /**
     * Increment resend count for user.
     *
     * @since 1.9.0
     * @param int $user_id The user ID.
     * @return void
     */
    private function increment_resend_count($user_id) {
        global $wpdb;

        $table_name = Guard_Dog()->db->get_email_verification_table_name();

        // Get current record
        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching
        // Table names are validated and sanitized, then escaped with esc_sql() for Plugin Check compliance.
        // Caching is not appropriate for email verification operations as they need to be real-time and accurate.
        // This is a necessary database query for security functionality.
        $escaped_table_name = esc_sql( $table_name );
        $verification = $wpdb->get_row(
            $wpdb->prepare("SELECT * FROM $escaped_table_name WHERE user_id = %d ORDER BY id DESC LIMIT 1", $user_id)
        );
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching

        if ($verification) {
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching
            // Caching is not appropriate for email verification operations as they need to be real-time and accurate.
            // This is a necessary database update operation for security rate limiting functionality.
            $wpdb->update(
                $table_name,
                [
                    'resend_count' => $verification->resend_count + 1,
                    'last_resend_at' => current_time('mysql'),
                ],
                ['id' => $verification->id],
                ['%d', '%s'],
                ['%d']
            );
            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching
        }
    }

    /**
     * Check if user email is verified.
     *
     * @since 1.9.0
     * @param int $user_id The user ID.
     * @return bool True if verified, false otherwise.
     */
    public function is_user_verified($user_id) {
        global $wpdb;

        $table_name = Guard_Dog()->db->get_email_verification_table_name();

        // First check if user has ANY verification record (to distinguish between new users and grandfathered users)
        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching
        // Table names are validated and sanitized, then escaped with esc_sql() for Plugin Check compliance.
        // Caching is not appropriate for email verification status checks as they need to be real-time and accurate.
        // This is a necessary database query for security authentication functionality.
        $escaped_table_name = esc_sql( $table_name );
        $has_record = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(*) FROM $escaped_table_name WHERE user_id = %d",
                $user_id
            )
        );
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching

        // If no verification record exists, check user registration date
        // If user was created before email verification feature, assume verified (grandfathered)
        // If user was created after, assume not verified (new user)
        if (!$has_record) {
            $user = get_userdata($user_id);
            if (!$user) {
                return false;
            }
            
            // Get when email verification feature was added (when it was enabled or plugin version 1.9.0)
            // For now, check if user was created recently (within last 24 hours) - if so, require verification
            $user_registered = strtotime($user->user_registered);
            $feature_activation_threshold = time() - DAY_IN_SECONDS; // 24 hours ago
            
            // If user was created recently, they likely need verification
            // Otherwise, assume grandfathered
            if ($user_registered > $feature_activation_threshold) {
                return false; // New user, needs verification
            }
            
            return true; // Older user, grandfathered
        }

        // Check if user has a verified record (get the most recent one)
        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching
        // Table names are validated and sanitized, then escaped with esc_sql() for Plugin Check compliance.
        // Caching is not appropriate for email verification status checks as they need to be real-time and accurate.
        // This is a necessary database query for security authentication functionality.
        $verification = $wpdb->get_row(
            $wpdb->prepare(
                "SELECT verified_at FROM $escaped_table_name WHERE user_id = %d ORDER BY id DESC LIMIT 1",
                $user_id
            )
        );
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching

        // If record exists but verified_at is NULL, user is not verified
        if ($verification) {
            return !empty($verification->verified_at);
        }

        // This shouldn't happen if we got here (has_record would be 0), but just in case
        return false;
    }

    /**
     * Get verification status for user.
     *
     * @since 1.9.0
     * @param int $user_id The user ID.
     * @return array|false Verification record or false if not found.
     */
    public function get_verification_status($user_id) {
        global $wpdb;

        $table_name = Guard_Dog()->db->get_email_verification_table_name();

        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching
        // Table names are validated and sanitized, then escaped with esc_sql() for Plugin Check compliance.
        // Caching is not appropriate for email verification status checks as they need to be real-time and accurate.
        // This is a necessary database query for security functionality.
        $escaped_table_name = esc_sql( $table_name );
        return $wpdb->get_row(
            $wpdb->prepare("SELECT * FROM $escaped_table_name WHERE user_id = %d ORDER BY id DESC LIMIT 1", $user_id)
        );
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching
    }

    /**
     * Mark user as verified manually (admin action).
     *
     * @since 1.9.0
     * @param int $user_id The user ID.
     * @return bool Success status.
     */
    public function mark_user_verified($user_id) {
        global $wpdb;

        $table_name = Guard_Dog()->db->get_email_verification_table_name();
        $user = get_userdata($user_id);

        if (!$user) {
            return false;
        }

        // Get or create verification record
        $verification = $this->get_verification_status($user_id);

        if ($verification) {
            // Update existing record
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching
            // Caching is not appropriate for email verification operations as they need to be real-time and accurate.
            // This is a necessary database update operation for security functionality.
            $result = $wpdb->update(
                $table_name,
                ['verified_at' => current_time('mysql')],
                ['id' => $verification->id],
                ['%s'],
                ['%d']
            );
            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching
        } else {
            // Create new record marked as verified
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
            // Caching is not appropriate for email verification operations as they need to be real-time and accurate.
            // This is a necessary database insert operation for security functionality.
            $result = $wpdb->insert(
                $table_name,
                [
                    'user_id' => $user_id,
                    'verification_token' => wp_hash(wp_generate_password(64, true, true)),
                    'verification_key' => wp_hash(wp_generate_password(64, true, true)),
                    'email' => $user->user_email,
                    'created_at' => current_time('mysql'),
                    'expires_at' => current_time('mysql'),
                    'verified_at' => current_time('mysql'),
                    'resend_count' => 0,
                    'last_resend_at' => null,
                ],
                ['%d', '%s', '%s', '%s', '%s', '%s', '%s', '%d', '%s']
            );
            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
        }

        if ($result !== false && $this->activity_log) {
            $this->activity_log->add_log_entry('email_verification_admin_verified', [
                'user_id' => $user_id,
            ]);
        }

        return $result !== false;
    }

    /**
     * Mark user as unverified.
     *
     * @since 1.9.0
     * @param int $user_id The user ID.
     * @return void
     */
    private function mark_user_unverified($user_id) {
        global $wpdb;

        $table_name = Guard_Dog()->db->get_email_verification_table_name();

        // Delete existing verification records
        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
        // Caching is not appropriate for email verification operations as they need to be real-time and accurate.
        // This is a necessary database delete operation for security functionality.
        $wpdb->delete(
            $table_name,
            ['user_id' => $user_id],
            ['%d']
        );
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
    }

    /**
     * Check if user was created by admin.
     *
     * @since 1.9.0
     * @return bool True if admin-created, false otherwise.
     */
    private function is_admin_created_user() {
        // Check if current user is admin and we're in admin area
        $is_admin_created = is_admin() && current_user_can('create_users');

        /**
         * Filter whether the current user creation context is admin-created.
         *
         * This filter allows tests and other code to override the admin context check.
         *
         * @since 1.9.0
         * @param bool $is_admin_created Whether this is an admin-created user.
         */
        return apply_filters('guard_dog_is_admin_created_user', $is_admin_created);
    }

    /**
     * Add verification status column to user list.
     *
     * @since 1.9.0
     * @param array $columns Existing columns.
     * @return array Modified columns.
     */
    public function add_verification_column($columns) {
        $columns['email_verified'] = __('Email Verified', 'guard-dog');
        return $columns;
    }

    /**
     * Render verification status column content.
     *
     * @since 1.9.0
     * @param string $value The column value.
     * @param string $column_name The column name.
     * @param int $user_id The user ID.
     * @return string The column content.
     */
    public function render_verification_column($value, $column_name, $user_id) {
        if ($column_name !== 'email_verified') {
            return $value;
        }

        $is_verified = $this->is_user_verified($user_id);
        $verification = $this->get_verification_status($user_id);

        if ($is_verified) {
            $status = '<span style="color: green;">' . esc_html__('Verified', 'guard-dog') . '</span>';
        } else {
            $status = '<span style="color: red;">' . esc_html__('Not Verified', 'guard-dog') . '</span>';
            
            if (current_user_can('edit_users')) {
                $verify_url = wp_nonce_url(
                    admin_url('admin-post.php?action=guard_dog_admin_verify_user&user_id=' . $user_id),
                    'guard_dog_admin_verify_user_' . $user_id
                );
                $resend_url = wp_nonce_url(
                    admin_url('admin-post.php?action=guard_dog_resend_verification&user_id=' . $user_id),
                    'guard_dog_resend_verification'
                );
                $status .= ' <a href="' . esc_url($verify_url) . '">' . esc_html__('Verify', 'guard-dog') . '</a>';
                $status .= ' | <a href="' . esc_url($resend_url) . '">' . esc_html__('Resend', 'guard-dog') . '</a>';
            }
        }

        return $status;
    }

    /**
     * Display verification success message on login page.
     *
     * @since 1.9.0
     * @param WP_Error $errors WP_Error object.
     * @param string $redirect_to Redirect destination URL.
     * @return WP_Error Modified WP_Error object.
     */
    public function display_verification_success_message($errors, $redirect_to) {
        // phpcs:disable WordPress.Security.NonceVerification.Recommended
        // Nonce verification is not required in this context:
        // 1. This function only displays informational messages, does not process data
        // 2. GET parameters are used only for conditional message display
        // 3. No privileged actions or data modifications occur
        // 4. All input is properly sanitized before use
        // 5. Messages are displayed via WordPress's WP_Error API (safe output)

        // Show different message on password reset page vs login page
        $action = isset($_GET['action']) ? sanitize_text_field(wp_unslash($_GET['action'])) : '';

        if (isset($_GET['verified']) && '1' === $_GET['verified']) {
            // phpcs:enable WordPress.Security.NonceVerification.Recommended
            if ($action === 'rp') {
                // On password reset page, show message that email is verified and they can set password
                $errors->add(
                    'email_verified',
                    __('Your email address has been verified. Please set your password below.', 'guard-dog'),
                    'message'
                );
            } else {
                // On login page, show standard message
                $errors->add(
                    'email_verified',
                    __('Your email address has been verified successfully. You can now log in.', 'guard-dog'),
                    'message'
                );
            }
        }
        return $errors;
    }

    /**
     * Handle admin manual verification action.
     *
     * @since 1.9.0
     * @return void
     */
    public function handle_admin_verify_user() {
        // Check permissions
        if (!current_user_can('edit_users')) {
            wp_die(esc_html__('You do not have permission to perform this action.', 'guard-dog'), esc_html__('Permission Denied', 'guard-dog'), ['response' => 403]);
        }

        // Verify nonce
        $user_id = isset($_GET['user_id']) ? absint($_GET['user_id']) : 0;
        $nonce = isset($_GET['_wpnonce']) ? sanitize_text_field(wp_unslash($_GET['_wpnonce'])) : '';
        if (!$user_id || !wp_verify_nonce($nonce, 'guard_dog_admin_verify_user_' . $user_id)) {
            wp_die(esc_html__('Security check failed.', 'guard-dog'), esc_html__('Verification Failed', 'guard-dog'), ['response' => 403]);
        }

        // Verify user
        if ($this->mark_user_verified($user_id)) {
            wp_safe_redirect(admin_url('users.php?verified=1'));
            exit;
        } else {
            wp_die(esc_html__('Failed to verify user.', 'guard-dog'), esc_html__('Error', 'guard-dog'), ['response' => 500]);
        }
    }

} // End class Guard_Dog_Email_Verification
