<?php

/**
 * Users functionality for Wuclean plugin.
 *
 * @package Wuclean
 */
if ( !defined( 'ABSPATH' ) ) {
    exit;
}
if ( !defined( 'PT_WUCLEAN_ORPHANED_CUSTOMER_DATA_KEY' ) ) {
    define( 'PT_WUCLEAN_ORPHANED_CUSTOMER_DATA_KEY', 'pt_wuclean_orphaned_customer_data' );
}
if ( !defined( 'PT_WUCLEAN_ORPHANED_CUSTOMER_DATA_OPTION' ) ) {
    define( 'PT_WUCLEAN_ORPHANED_CUSTOMER_DATA_OPTION', 'pt_wuclean_orphaned_customer_data_cache' );
}
/**
 * Store orphaned customer data for 24 hours with option fallback.
 *
 * @param array $data Orphaned customer data payload.
 */
function pt_wuclean_store_orphaned_customer_data(  $data  ) {
    if ( !is_array( $data ) ) {
        $data = array();
    }
    set_transient( PT_WUCLEAN_ORPHANED_CUSTOMER_DATA_KEY, $data, DAY_IN_SECONDS );
    update_option( PT_WUCLEAN_ORPHANED_CUSTOMER_DATA_OPTION, array(
        'data'      => $data,
        'stored_at' => time(),
        'expires'   => time() + DAY_IN_SECONDS,
    ), false );
}

/**
 * Retrieve orphaned customer data, with option fallback if transient expired.
 *
 * @return array
 */
function pt_wuclean_get_orphaned_customer_data() {
    $data = get_transient( PT_WUCLEAN_ORPHANED_CUSTOMER_DATA_KEY );
    if ( false !== $data && is_array( $data ) ) {
        return $data;
    }
    $cached = get_option( PT_WUCLEAN_ORPHANED_CUSTOMER_DATA_OPTION, array() );
    if ( is_array( $cached ) && isset( $cached['data'] ) ) {
        $expires = ( isset( $cached['expires'] ) ? intval( $cached['expires'] ) : 0 );
        if ( 0 === $expires || $expires > time() ) {
            $payload = ( is_array( $cached['data'] ) ? $cached['data'] : array() );
            set_transient( PT_WUCLEAN_ORPHANED_CUSTOMER_DATA_KEY, $payload, DAY_IN_SECONDS );
            return $payload;
        }
    }
    return array();
}

/**
 * Clear stored orphaned customer data.
 */
function pt_wuclean_delete_orphaned_customer_data() {
    delete_transient( PT_WUCLEAN_ORPHANED_CUSTOMER_DATA_KEY );
    delete_option( PT_WUCLEAN_ORPHANED_CUSTOMER_DATA_OPTION );
}

/**
 * Check and set persistent purchase flag for user (HPOS-only)
 * This flag is set once when we detect the user has ever spent money, and never removed
 *
 * @param int $user_id User ID.
 * @return bool True if user has ever purchased (flag exists or was just set).
 */
function pt_wuclean_check_and_set_purchase_flag(  $user_id  ) {
    // Check if flag already exists.
    $ever_purchased = get_user_meta( $user_id, '_pt_wuclean_ever_purchased', true );
    if ( '1' === $ever_purchased ) {
        return true;
        // Flag already set.
    }
    // Check WooCommerce's paying_customer flag (most reliable for historical purchases).
    // This flag is set once when a customer completes their first paid order and never removed.
    // Perfect for detecting customers whose orders may have been deleted over time.
    $paying_customer = get_user_meta( $user_id, 'paying_customer', true );
    if ( '1' === $paying_customer || 1 === (int) $paying_customer ) {
        update_user_meta( $user_id, '_pt_wuclean_ever_purchased', '1' );
        return true;
    }
    // Check WooCommerce's lifetime value (wc_money_spent).
    $money_spent = get_user_meta( $user_id, 'wc_money_spent', true );
    // If they've spent any money, set our persistent flag (one-way only).
    if ( $money_spent && floatval( $money_spent ) > 0 ) {
        update_user_meta( $user_id, '_pt_wuclean_ever_purchased', '1' );
        return true;
    }
    // Fallback: Check for actual completed/processing orders.
    // This covers cases where wc_money_spent wasn't set (imported/fabricated databases).
    if ( function_exists( 'wc_get_orders' ) ) {
        $orders = wc_get_orders( array(
            'customer_id' => $user_id,
            'limit'       => 1,
            'status'      => array('completed', 'processing', 'on-hold'),
            'return'      => 'ids',
        ) );
        if ( !empty( $orders ) ) {
            // User has at least one order, set the persistent flag.
            update_user_meta( $user_id, '_pt_wuclean_ever_purchased', '1' );
            return true;
        }
    }
    return false;
    // No purchase history found.
}

// User protection meta keys.
define( 'PT_WUCLEAN_USER_PROTECT_META', '_pt_wuclean_protect_user' );
define( 'PT_WUCLEAN_USER_PROTECT_ORDERS_META', '_pt_wuclean_protect_orders' );
/**
 * Users page with Settings, Flagged Users, and User Logs tabs
 */
function pt_wuclean_render_users_page() {
    // Defense-in-depth: the admin menu is capability-gated, but verify again before reading request input.
    if ( !current_user_can( pt_wuclean_get_required_capability() ) ) {
        wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'wuclean-database-cleaner-for-woocommerce' ) );
    }
    // Tab query arg: settings is the default; other tabs require a valid tab nonce (see nav links).
    $current_tab = 'settings';
    $valid_tabs = array(
        'settings',
        'flagged',
        'protected',
        'orphaned',
        'logs'
    );
    if ( isset( $_GET['tab'] ) ) {
        $tab_value = sanitize_text_field( wp_unslash( $_GET['tab'] ) );
        if ( !in_array( $tab_value, $valid_tabs, true ) ) {
            $current_tab = 'settings';
        } elseif ( 'settings' === $tab_value ) {
            $current_tab = 'settings';
        } elseif ( isset( $_GET['_wpnonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'wuclean-users-tab' ) ) {
            $current_tab = $tab_value;
        }
    }
    echo '<div class="wrap">';
    echo '<h1>👥 ' . esc_html__( 'Customers', 'wuclean-database-cleaner-for-woocommerce' ) . '</h1>';
    // Tab navigation.
    echo '<nav class="nav-tab-wrapper">';
    echo '<a href="' . esc_url( add_query_arg( array(
        'page'     => 'wuclean-users',
        'tab'      => 'settings',
        '_wpnonce' => wp_create_nonce( 'wuclean-users-tab' ),
    ), admin_url( 'admin.php' ) ) ) . '" class="nav-tab' . (( 'settings' === $current_tab ? ' nav-tab-active' : '' )) . '">' . esc_html__( 'Settings', 'wuclean-database-cleaner-for-woocommerce' ) . '</a>';
    echo '<a href="' . esc_url( add_query_arg( array(
        'page'     => 'wuclean-users',
        'tab'      => 'flagged',
        '_wpnonce' => wp_create_nonce( 'wuclean-users-tab' ),
    ), admin_url( 'admin.php' ) ) ) . '" class="nav-tab' . (( 'flagged' === $current_tab ? ' nav-tab-active' : '' )) . '">' . esc_html__( 'Flagged Customers', 'wuclean-database-cleaner-for-woocommerce' ) . '</a>';
    echo '<a href="' . esc_url( add_query_arg( array(
        'page'     => 'wuclean-users',
        'tab'      => 'protected',
        '_wpnonce' => wp_create_nonce( 'wuclean-users-tab' ),
    ), admin_url( 'admin.php' ) ) ) . '" class="nav-tab' . (( 'protected' === $current_tab ? ' nav-tab-active' : '' )) . '">' . esc_html__( 'Protected Customers', 'wuclean-database-cleaner-for-woocommerce' ) . '</a>';
    echo '<a href="' . esc_url( add_query_arg( array(
        'page'     => 'wuclean-users',
        'tab'      => 'orphaned',
        '_wpnonce' => wp_create_nonce( 'wuclean-users-tab' ),
    ), admin_url( 'admin.php' ) ) ) . '" class="nav-tab' . (( 'orphaned' === $current_tab ? ' nav-tab-active' : '' )) . '">' . esc_html__( 'Orphaned Customer Data', 'wuclean-database-cleaner-for-woocommerce' ) . '</a>';
    echo '<a href="' . esc_url( add_query_arg( array(
        'page'     => 'wuclean-users',
        'tab'      => 'logs',
        '_wpnonce' => wp_create_nonce( 'wuclean-users-tab' ),
    ), admin_url( 'admin.php' ) ) ) . '" class="nav-tab' . (( 'logs' === $current_tab ? ' nav-tab-active' : '' )) . '">' . esc_html__( 'Customer Logs', 'wuclean-database-cleaner-for-woocommerce' ) . '</a>';
    echo '</nav>';
    // Standard inner wrapper for all tabs (max width + side padding).
    echo '<div class="pt-wuclean-page-inner" style="max-width: 1600px; box-sizing: border-box;">';
    if ( 'settings' === $current_tab ) {
        pt_wuclean_render_users_settings_tab();
    } elseif ( 'flagged' === $current_tab ) {
        pt_wuclean_render_flagged_users_tab();
    } elseif ( 'protected' === $current_tab ) {
        pt_wuclean_render_flagged_users_tab();
    } elseif ( 'orphaned' === $current_tab ) {
        pt_wuclean_render_orphaned_customer_data_tab();
    } elseif ( 'logs' === $current_tab ) {
        pt_wuclean_render_user_logs_tab();
    }
    echo '</div>';
    // Close inner wrapper.
    echo '</div>';
}

/**
 * Localized variables for user management scripts.
 *
 * @return array
 */
function pt_wuclean_get_users_script_vars() {
    /* translators: %d: number of newly flagged customers */
    $scan_completed_flagged = __( 'Scan completed! %d customer(s) flagged. Redirecting to Flagged Customers tab...', 'wuclean-database-cleaner-for-woocommerce' );
    /* translators: %d: number of orphaned items */
    $scan_complete_found = __( 'Scan complete! Found %d orphaned item(s). Redirecting...', 'wuclean-database-cleaner-for-woocommerce' );
    /* translators: %d: number of customers selected for email */
    $confirm_send_email = __( 'Send inactivity email to %d selected customer(s)?', 'wuclean-database-cleaner-for-woocommerce' );
    return array(
        'user_scan_nonce'                   => wp_create_nonce( 'pt_wuclean_user_scan' ),
        'orphaned_customer_scan_nonce'      => wp_create_nonce( 'pt_wuclean_orphaned_customer_scan' ),
        'flagged_users_nonce'               => wp_create_nonce( 'pt_wuclean_flagged_users' ),
        'flagged_users_url'                 => add_query_arg( array(
            'page'     => 'wuclean-users',
            'tab'      => 'flagged',
            '_wpnonce' => wp_create_nonce( 'wuclean-users-tab' ),
        ), admin_url( 'admin.php' ) ),
        'orphaned_customer_data_url'        => add_query_arg( array(
            'page'     => 'wuclean-users',
            'tab'      => 'orphaned',
            '_wpnonce' => wp_create_nonce( 'wuclean-users-tab' ),
        ), admin_url( 'admin.php' ) ),
        'ajaxurl'                           => admin_url( 'admin-ajax.php' ),
        'scan_completed_flagged'            => $scan_completed_flagged,
        'scan_completed_none'               => __( 'Scan completed! No customers match the current flagging criteria.', 'wuclean-database-cleaner-for-woocommerce' ),
        'scan_completed_no_new_redirecting' => __( 'Scan completed! No new customers match the current flagging criteria. Redirecting to show existing flagged customers...', 'wuclean-database-cleaner-for-woocommerce' ),
        'scanning_label'                    => __( 'Scanning...', 'wuclean-database-cleaner-for-woocommerce' ),
        'scan_progress_message'             => __( 'Scanning database for orphaned customer data...', 'wuclean-database-cleaner-for-woocommerce' ),
        'scan_complete_found'               => $scan_complete_found,
        'scan_complete_none'                => __( 'Scan complete! No orphaned customer data found. Redirecting...', 'wuclean-database-cleaner-for-woocommerce' ),
        'scan_failed'                       => __( 'Scan failed', 'wuclean-database-cleaner-for-woocommerce' ),
        'scan_failed_retry'                 => __( 'Scan failed. Please try again.', 'wuclean-database-cleaner-for-woocommerce' ),
        'select_user_warning'               => __( 'Please select at least one customer to email.', 'wuclean-database-cleaner-for-woocommerce' ),
        'confirm_send_email'                => $confirm_send_email,
        'sending_label'                     => __( 'Sending...', 'wuclean-database-cleaner-for-woocommerce' ),
        'error_prefix'                      => __( 'Error:', 'wuclean-database-cleaner-for-woocommerce' ),
        'unknown_error'                     => __( 'Unknown error', 'wuclean-database-cleaner-for-woocommerce' ),
        'email_failed'                      => __( 'Failed to send emails. Please try again.', 'wuclean-database-cleaner-for-woocommerce' ),
    );
}

/**
 * Users Settings Tab
 */
function pt_wuclean_render_users_settings_tab() {
    if ( !current_user_can( pt_wuclean_get_required_capability() ) ) {
        wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'wuclean-database-cleaner-for-woocommerce' ) );
    }
    // Handle form submission.
    if ( isset( $_POST['pt_wuclean_settings_nonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['pt_wuclean_settings_nonce'] ) ), 'pt_wuclean_save_settings' ) ) {
        $settings = get_option( 'pt_wuclean_settings', array() );
        // Free user settings - always available.
        $settings['user_target_roles'] = ( isset( $_POST['user_target_roles'] ) ? array_map( 'sanitize_text_field', wp_unslash( $_POST['user_target_roles'] ) ) : array() );
        // Flag settings - only save days if checkbox is checked (available to free users).
        $settings['user_delete_new_no_order_days'] = ( !empty( $_POST['enable_user_delete_new_no_order'] ) ? intval( $_POST['user_delete_new_no_order_days'] ?? 90 ) : 0 );
        $settings['user_delete_inactive_days'] = ( !empty( $_POST['enable_user_delete_inactive'] ) ? intval( $_POST['user_delete_inactive_days'] ?? 365 ) : 0 );
        // Save settings and reschedule crons if needed.
        update_option( 'pt_wuclean_settings', $settings, false );
        echo '<div class="notice notice-success"><p>' . esc_html__( 'Customer cleanup settings saved successfully!', 'wuclean-database-cleaner-for-woocommerce' ) . '</p></div>';
    }
    $settings = get_option( 'pt_wuclean_settings', array() );
    // Free user settings - always loaded.
    $user_target_roles = $settings['user_target_roles'] ?? array();
    $user_delete_new_no_order_days = $settings['user_delete_new_no_order_days'] ?? 0;
    $user_delete_inactive_days = $settings['user_delete_inactive_days'] ?? 0;
    $user_new_customer_email_notification = 0;
    $user_customer_email_notification = 0;
    echo '<div style="margin-top:2em; max-width: 1600px;">';
    echo '<h2>⚙️ ' . esc_html__( 'Customer Cleanup Settings', 'wuclean-database-cleaner-for-woocommerce' ) . '</h2>';
    // Info box.
    echo '<div style="max-width: 1600px;background: #e7f3ff; border: 1px solid #2196f3; border-radius: 4px; padding: 15px; margin: 20px 0;">';
    if ( pt_wuclean_fs()->is_not_paying() ) {
        // Free version: Show basic feature description.
        echo '<p style="margin: 0 0 10px 0;"><strong>ℹ️ ' . esc_html__( 'About Customer Cleanup:', 'wuclean-database-cleaner-for-woocommerce' ) . '</strong> ' . esc_html__( 'Easily detect fake/inactive accounts and orphaned customer data. Manually send selected customers reactivation emails before deleting their account and use SQL and your favorite database cleaner to remove orphaned user meta, or consider automating cleanup with Wuclean Pro👑', 'wuclean-database-cleaner-for-woocommerce' ) . '</p>';
    }
    echo '</div>';
    echo '<form method="post" class="wuclean">';
    wp_nonce_field( 'pt_wuclean_save_settings', 'pt_wuclean_settings_nonce' );
    // Settings card.
    echo '<div style="background: #fff; border: 1px solid #ccd0d4; border-radius: 4px; padding: 20px; margin: 20px 0; max-width: 1600px;">';
    echo '<h3 style="margin-top: 0;">📋 ' . esc_html__( 'Shared Scan Settings (Manual + Scheduled)', 'wuclean-database-cleaner-for-woocommerce' ) . '</h3>';
    echo '<table class="form-table" style="table-layout: fixed;"><tbody>';
    // Target Customer Roles.
    echo '<tr><th scope="row" style="width: 300px;">' . esc_html__( 'Target customer roles', 'wuclean-database-cleaner-for-woocommerce' ) . ' <span class="pt-wuclean-tooltip">?<span class="pt-wuclean-tooltip-text">' . esc_html__( 'Select which customer roles to target for cleanup. Only Customer roles and custom customer roles are shown. Administrators and Shop Managers are automatically excluded for safety.', 'wuclean-database-cleaner-for-woocommerce' ) . '</span></span></th><td>';
    $roles = wp_roles()->get_names();
    // Get customer role as reference for customer-like capabilities.
    $customer_role = get_role( 'customer' );
    $customer_has_read = $customer_role && isset( $customer_role->capabilities['read'] ) && $customer_role->capabilities['read'];
    // Define admin capabilities that should exclude a role from being customer-like.
    // These capabilities indicate the role has administrative or content management permissions.
    $admin_capabilities = array(
        'manage_options',
        'edit_posts',
        'edit_pages',
        'manage_woocommerce',
        'edit_products',
        'manage_categories'
    );
    // Exclude non-customer roles (safety exclusions).
    $excluded_roles = array(
        'administrator',
        'shop_manager',
        'editor',
        'author',
        'contributor',
        'subscriber'
    );
    foreach ( $roles as $role_value => $role_name ) {
        // Skip explicitly excluded roles.
        if ( in_array( $role_value, $excluded_roles, true ) ) {
            continue;
        }
        // Get the role object to check capabilities.
        $role_obj = get_role( $role_value );
        if ( !$role_obj ) {
            continue;
            // Skip if role doesn't exist.
        }
        // Check if role has customer-like capabilities.
        // Must have "read" capability (basic requirement for all WordPress users).
        $has_read = isset( $role_obj->capabilities['read'] ) && $role_obj->capabilities['read'];
        if ( !$has_read ) {
            continue;
            // Skip roles without read capability.
        }
        // Check if role has any admin capabilities (exclude admin-like roles).
        $has_admin_cap = false;
        foreach ( $admin_capabilities as $admin_cap ) {
            if ( isset( $role_obj->capabilities[$admin_cap] ) && $role_obj->capabilities[$admin_cap] ) {
                $has_admin_cap = true;
                break;
            }
        }
        if ( $has_admin_cap ) {
            continue;
            // Skip roles with admin capabilities.
        }
        // Role passed all checks - it's customer-like, show it.
        $checked = ( in_array( $role_value, $user_target_roles, true ) ? 'checked' : '' );
        // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- 'checked' is a safe literal string
        echo '<label><input type="checkbox" name="user_target_roles[]" value="' . esc_attr( $role_value ) . '" ' . $checked . '> ' . esc_html( $role_name ) . '</label><br>';
    }
    echo '</td></tr>';
    // Flag new customers without orders.
    echo '<tr><th scope="row" style="width: 300px;">' . esc_html__( 'Flag new customers without orders', 'wuclean-database-cleaner-for-woocommerce' ) . ' <span class="pt-wuclean-tooltip">?<span class="pt-wuclean-tooltip-text">' . esc_html__( 'Flag customers who registered and have never placed an order.', 'wuclean-database-cleaner-for-woocommerce' ) . '</span></span></th><td>';
    echo '<input type="checkbox" name="enable_user_delete_new_no_order" value="1"' . checked( $user_delete_new_no_order_days > 0, true, false ) . '> ';
    echo '<input type="number" name="user_delete_new_no_order_days" min="1" max="3650" value="' . esc_attr( ( $user_delete_new_no_order_days > 0 ? $user_delete_new_no_order_days : 90 ) ) . '" style="width: 70px;"> ' . esc_html__( 'days without orders', 'wuclean-database-cleaner-for-woocommerce' );
    echo '</td></tr>';
    // Flag inactive customers.
    echo '<tr><th scope="row" style="width: 300px;">' . esc_html__( 'Flag inactive customers', 'wuclean-database-cleaner-for-woocommerce' ) . ' <span class="pt-wuclean-tooltip">?<span class="pt-wuclean-tooltip-text">' . esc_html__( 'Flag customers who have not placed an order recently.', 'wuclean-database-cleaner-for-woocommerce' ) . '</span></span></th><td>';
    echo '<input type="checkbox" name="enable_user_delete_inactive" value="1"' . checked( $user_delete_inactive_days > 0, true, false ) . '> ';
    echo '<input type="number" name="user_delete_inactive_days" min="1" max="3650" value="' . esc_attr( ( $user_delete_inactive_days > 0 ? $user_delete_inactive_days : 365 ) ) . '" style="width: 70px;"> ' . esc_html__( 'days since last order', 'wuclean-database-cleaner-for-woocommerce' );
    echo '</td></tr>';
    echo '</tbody></table>';
    echo '<p style="margin-bottom: 0;">';
    echo '<input type="submit" class="button button-primary" value="' . esc_attr__( 'Save Settings', 'wuclean-database-cleaner-for-woocommerce' ) . '">';
    echo '</p>';
    echo '</div>';
    // Close settings card.
    echo '</form>';
    // Manual Customer Scan section.
    echo '<div style="background: #fff; border: 1px solid #ccd0d4; border-radius: 4px; padding: 20px; margin: 20px 0; max-width: 1600px;">';
    echo '<h3 style="margin-top: 0;">🔍 ' . esc_html__( 'Manual Customer Scans', 'wuclean-database-cleaner-for-woocommerce' ) . '</h3>';
    // Customer flagging scan.
    echo '<div style="margin-bottom: 20px;">';
    echo '<h4 style="margin-bottom:0.5em;">' . esc_html__( 'Flag Customers Scan', 'wuclean-database-cleaner-for-woocommerce' ) . '</h4>';
    echo '<p style="margin-bottom: 15px;">' . esc_html__( 'Flag customers based on the roles and flag settings above (new customers without orders and inactive customers).', 'wuclean-database-cleaner-for-woocommerce' ) . '</p>';
    echo '<p style="margin-bottom: 15px; font-size: 13px; color: #666;"><em>' . esc_html__( 'Note: Emails will not be sent automatically. Customers will not be deleted automatically.', 'wuclean-database-cleaner-for-woocommerce' ) . '</em></p>';
    echo '<button type="button" id="pt-start-user-scan" class="button button-primary" style="background: #2196f3; border-color: #2196f3;">🔍 ' . esc_html__( 'Start Customer Scan', 'wuclean-database-cleaner-for-woocommerce' ) . '</button>';
    echo '<button type="button" id="pt-stop-user-scan" class="button" style="display: none; margin-left: 10px; background: #dc3232; border-color: #dc3232; color: #fff;">⏹️ ' . esc_html__( 'Stop Scan', 'wuclean-database-cleaner-for-woocommerce' ) . '</button>';
    echo '<div id="pt-user-scan-progress" style="display: none; margin-top: 20px;">';
    echo '<div style="background: #f0f0f0; border: 1px solid #ddd; border-radius: 4px; padding: 15px;">';
    echo '<div style="margin-bottom: 8px;"><strong>' . esc_html__( 'Scan Progress:', 'wuclean-database-cleaner-for-woocommerce' ) . '</strong></div>';
    echo '<div style="background: #fff; border: 1px solid #ddd; border-radius: 2px; height: 25px; position: relative;">';
    echo '<div id="pt-user-progress-bar" style="background: #2196f3; height: 100%; width: 0%; transition: width 0.3s ease; border-radius: 2px;"></div>';
    echo '</div>';
    echo '<div id="pt-user-progress-text" style="margin-top: 8px; font-size: 13px; color: #666;">' . esc_html__( 'Initializing...', 'wuclean-database-cleaner-for-woocommerce' ) . '</div>';
    echo '</div>';
    echo '</div>';
    echo '</div>';
    // Orphaned Customer Data Scan.
    echo '<div style="margin-bottom: 0; border-top: 1px solid #ddd; padding-top: 20px;">';
    echo '<h4 style="margin-bottom:0.5em;">🗑️ ' . esc_html__( 'Orphaned Customer Data Scan', 'wuclean-database-cleaner-for-woocommerce' ) . '</h4>';
    echo '<p style="margin-bottom: 15px;">' . esc_html__( 'Detects orphaned usermeta and customer analytics from improperly deleted customers. Orphaned product reviews and comments are automatically reassigned to guest to preserve product ratings.', 'wuclean-database-cleaner-for-woocommerce' ) . '</p>';
    echo '<button type="button" id="pt-start-orphaned-customer-scan" class="button button-primary" style="background: #2196f3; border-color: #2196f3;">🔍 ' . esc_html__( 'Search for Orphaned Customer Data', 'wuclean-database-cleaner-for-woocommerce' ) . '</button>';
    echo '<button type="button" id="pt-stop-orphaned-customer-scan" class="button" style="display:none; margin-left:10px; background:#dc3232; border-color:#dc3232; color:#fff;">⏹️ ' . esc_html__( 'Stop Search', 'wuclean-database-cleaner-for-woocommerce' ) . '</button>';
    echo '<div id="pt-orphaned-customer-scan-progress" style="display: none; margin-top: 20px;">';
    echo '<div style="background: #f0f0f0; border: 1px solid #ddd; border-radius: 4px; padding: 15px;">';
    echo '<div style="margin-bottom: 8px;"><strong>' . esc_html__( 'Scan Progress:', 'wuclean-database-cleaner-for-woocommerce' ) . '</strong></div>';
    echo '<div style="background: #fff; border: 1px solid #ddd; border-radius: 2px; height: 25px; position: relative;">';
    echo '<div id="pt-orphaned-customer-progress-bar" style="background: #ff9800; height: 100%; width: 0%; transition: width 0.3s ease; border-radius: 2px;"></div>';
    echo '</div>';
    echo '<div id="pt-orphaned-customer-progress-text" style="margin-top: 8px; font-size: 13px; color: #666;"></div>';
    echo '</div></div>';
    echo '</div>';
    echo '</div>';
    // Close manual scan card.
    // Free version: progressive disclosure for automation benefits (Settings tab only).
    $pro_benefits = array(
        __( 'Schedule automatic customer cleanup every few minutes, daily, weekly or monthly and receive email notifications summarising key actions', 'wuclean-database-cleaner-for-woocommerce' ),
        __( 'Automatically detect new customers (incl fake registrations) who have not ordered within your time limit (30 days for example)', 'wuclean-database-cleaner-for-woocommerce' ),
        __( 'Automatically send custom emails to these new customers inviting them to place an order to avoid account deletion', 'wuclean-database-cleaner-for-woocommerce' ),
        __( 'Automatically delete new customers who do not place an order within your time limit', 'wuclean-database-cleaner-for-woocommerce' ),
        __( 'Automatically detect dormant (inactive) customers who have not ordered for a long time', 'wuclean-database-cleaner-for-woocommerce' ),
        __( 'Automatically send a custom email to these inactive customers inviting them to visit your store again and place an order', 'wuclean-database-cleaner-for-woocommerce' ),
        __( 'Automatically delete inactive customers who do not order anything within your time limit', 'wuclean-database-cleaner-for-woocommerce' ),
        __( 'Automatically detect and delete orphaned customer meta data', 'wuclean-database-cleaner-for-woocommerce' ),
        __( 'Control cleanup batch size and frequency to keep you customer database clean with minimal server impact', 'wuclean-database-cleaner-for-woocommerce' )
    );
    pt_wuclean_render_pro_automation_callout( $pro_benefits, 'customers-settings' );
    echo '</div>';
    // Enqueue JavaScript files.
    wp_enqueue_script(
        'pt-wuclean-common',
        plugin_dir_url( __FILE__ ) . 'assets/js/common.js',
        array('jquery'),
        '1.0.0',
        true
    );
    wp_enqueue_script(
        'pt-wuclean-users',
        plugin_dir_url( __FILE__ ) . 'assets/js/users.js',
        array('jquery', 'pt-wuclean-common'),
        '1.0.0',
        true
    );
    wp_localize_script( 'pt-wuclean-common', 'pt_wuclean_common_vars', pt_wuclean_get_common_script_strings() );
    wp_localize_script( 'pt-wuclean-users', 'pt_wuclean_users_vars', pt_wuclean_get_users_script_vars() );
}

/**
 * Flagged Users Tab
 */
function pt_wuclean_render_flagged_users_tab() {
    if ( !current_user_can( pt_wuclean_get_required_capability() ) ) {
        wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'wuclean-database-cleaner-for-woocommerce' ) );
    }
    // Validate tab nonce before interpreting GET switches for this view (matches Orders flagged tab pattern).
    $users_tab_nonce_valid = isset( $_GET['_wpnonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'wuclean-users-tab' );
    if ( $users_tab_nonce_valid ) {
        $show_protected = isset( $_GET['show_protected'] ) && '1' === sanitize_text_field( wp_unslash( $_GET['show_protected'] ) );
        $current_tab = ( isset( $_GET['tab'] ) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : '' );
        if ( 'protected' === $current_tab ) {
            $show_protected = true;
        }
    } else {
        $show_protected = false;
    }
    $settings = get_option( 'pt_wuclean_settings', array() );
    $batch_size = min( intval( $settings['user_cleanup_batch_size'] ?? 10 ), 10 );
    // Cap at 10 for safety.
    // Show all flagged users: use cutoff 0 so meta_value >= 0 (any valid timestamp). Users with flag date
    // set in the past (e.g. via SQL) must remain visible for manual deletion; a 24h filter hid them.
    $cutoff = 0;
    // Handle per-row customer email send action for flagged users.
    if ( isset( $_POST['send_user_email'] ) && isset( $_POST['pt_wuclean_flagged_users_nonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['pt_wuclean_flagged_users_nonce'] ) ), 'pt_wuclean_flagged_users' ) ) {
        $user_id = ( isset( $_POST['send_user_email'] ) ? absint( $_POST['send_user_email'] ) : 0 );
        $user = ( $user_id ? get_user_by( 'id', $user_id ) : false );
        if ( $user ) {
            $settings = get_option( 'pt_wuclean_settings', array() );
            // Manual per-row send should still work in Free, even though flagged auto-delete days are premium-only.
            $days_until_deletion = intval( $settings['user_delete_flagged_days'] ?? 7 );
            $flag_reason = (string) get_user_meta( $user_id, '_pt_wuclean_flag_reason', true );
            if ( empty( $flag_reason ) ) {
                $flag_reason = (string) get_user_meta( $user_id, '_pt_wuclean_flag_reason', true );
            }
            $sent = pt_wuclean_send_customer_email_for_user( $user_id, $flag_reason, $days_until_deletion );
            if ( $sent ) {
                echo '<div class="notice notice-success"><p>' . esc_html__( 'Email sent successfully.', 'wuclean-database-cleaner-for-woocommerce' ) . '</p></div>';
            } else {
                echo '<div class="notice notice-warning"><p>' . esc_html__( 'No email was sent for this user.', 'wuclean-database-cleaner-for-woocommerce' ) . '</p></div>';
            }
        }
    }
    // Handle unflag all users.
    if ( isset( $_POST['unflag_selected_users'] ) && isset( $_POST['pt_wuclean_flagged_users_nonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['pt_wuclean_flagged_users_nonce'] ) ), 'pt_wuclean_flagged_users' ) ) {
        // Use direct SQL delete for efficiency - universal reset but limited to selected users.
        global $wpdb;
        $selected_users = ( isset( $_POST['select_users'] ) ? array_map( 'intval', $_POST['select_users'] ) : array() );
        // Safety cap: prevent timeouts and abusive POST payloads.
        $selected_users = array_values( array_unique( array_filter( array_map( 'absint', $selected_users ) ) ) );
        $selected_users = array_slice( $selected_users, 0, 100 );
        $unflagged_users_count = 0;
        // Track which meta rows we cleared via checking flagged_at existence.
        foreach ( $selected_users as $user_id ) {
            // Skip if the user isn't currently flagged.
            if ( empty( get_user_meta( $user_id, '_pt_wuclean_flagged_at', true ) ) ) {
                continue;
            }
            delete_user_meta( $user_id, '_pt_wuclean_flagged_at' );
            delete_user_meta( $user_id, '_pt_wuclean_flag_reason' );
            delete_user_meta( $user_id, '_pt_wuclean_inactivity_email_sent' );
            delete_user_meta( $user_id, '_pt_wuclean_new_customer_email_sent' );
            ++$unflagged_users_count;
        }
        // Add a single summary log entry for performance.
        if ( $unflagged_users_count > 0 ) {
            $logs = get_option( 'pt_wuclean_user_logs', array() );
            $log_limit = ( isset( $settings['user_log_limit'] ) ? max( 0, min( intval( $settings['user_log_limit'] ), pt_wuclean_Plugin::DEFAULT_USER_LOG_LIMIT ) ) : pt_wuclean_Plugin::DEFAULT_USER_LOG_LIMIT );
            if ( $log_limit > 0 ) {
                /* translators: %d: number of users. */
                $state_msg = sprintf( __( 'Unflagged %d users (selected reset)', 'wuclean-database-cleaner-for-woocommerce' ), $unflagged_users_count );
                $logs[] = array(
                    'user_id'   => 0,
                    'username'  => 'Bulk Action',
                    'name'      => '-',
                    'email'     => '-',
                    'state'     => $state_msg,
                    'timestamp' => current_time( 'mysql' ),
                );
                // Prune logs to limit.
                if ( count( $logs ) > $log_limit ) {
                    $logs = array_slice( $logs, -$log_limit );
                }
                update_option( 'pt_wuclean_user_logs', $logs, false );
            }
            // Redirect to refresh the page and show updated state.
            if ( !headers_sent() ) {
                $redirect_url = add_query_arg( array(
                    'page'                     => 'wuclean-users',
                    'tab'                      => 'flagged',
                    'unflagged_selected_count' => $unflagged_users_count,
                    '_wpnonce'                 => wp_create_nonce( 'wuclean-users-tab' ),
                ), admin_url( 'admin.php' ) );
                wp_safe_redirect( $redirect_url );
                exit;
            }
        } else {
            echo '<div class="updated notice"><p>' . esc_html__( 'No flagged users were unflagged. Select flagged users and try again.', 'wuclean-database-cleaner-for-woocommerce' ) . '</p></div>';
        }
    }
    if ( isset( $_POST['unflag_all_users'] ) && isset( $_POST['pt_wuclean_flagged_users_nonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['pt_wuclean_flagged_users_nonce'] ) ), 'pt_wuclean_flagged_users' ) ) {
        // Use direct SQL DELETE for efficiency - this is a universal reset.
        global $wpdb;
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Bulk delete operation requires direct query for efficiency.
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Bulk delete operation requires direct query for efficiency.
        // Delete all flagging metadata in one go.
        $deleted_flagged = $wpdb->delete( $wpdb->usermeta, array(
            'meta_key' => '_pt_wuclean_flagged_at',
        ) );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Bulk delete operation requires direct query for efficiency.
        $deleted_reason = $wpdb->delete( $wpdb->usermeta, array(
            'meta_key' => '_pt_wuclean_flag_reason',
        ) );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Bulk delete operation requires direct query for efficiency.
        $deleted_email_sent = $wpdb->delete( $wpdb->usermeta, array(
            'meta_key' => '_pt_wuclean_inactivity_email_sent',
        ) );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Bulk delete operation requires direct query for efficiency.
        $deleted_new_customer_email_sent = $wpdb->delete( $wpdb->usermeta, array(
            'meta_key' => '_pt_wuclean_new_customer_email_sent',
        ) );
        // Add a single summary log entry instead of logging each user individually (for performance).
        if ( $deleted_flagged > 0 ) {
            $logs = get_option( 'pt_wuclean_user_logs', array() );
            $log_limit = ( isset( $settings['user_log_limit'] ) ? max( 0, min( intval( $settings['user_log_limit'] ), pt_wuclean_Plugin::DEFAULT_USER_LOG_LIMIT ) ) : pt_wuclean_Plugin::DEFAULT_USER_LOG_LIMIT );
            if ( $log_limit > 0 ) {
                /* translators: %d: number of users. */
                $state_msg = sprintf( __( 'Unflagged %d users (bulk reset)', 'wuclean-database-cleaner-for-woocommerce' ), $deleted_flagged );
                $logs[] = array(
                    'user_id'   => 0,
                    'username'  => 'Bulk Action',
                    'name'      => '-',
                    'email'     => '-',
                    'state'     => $state_msg,
                    'timestamp' => current_time( 'mysql' ),
                );
                // Prune logs to limit.
                if ( count( $logs ) > $log_limit ) {
                    $logs = array_slice( $logs, -$log_limit );
                }
                update_option( 'pt_wuclean_user_logs', $logs, false );
            }
        }
        // Total deleted rows.
        $unflagged_count = $deleted_flagged;
        if ( $unflagged_count > 0 ) {
            // Redirect to refresh the page and show updated state.
            if ( !headers_sent() ) {
                $redirect_url = add_query_arg( array(
                    'page'            => 'wuclean-users',
                    'tab'             => 'flagged',
                    'unflagged_count' => $unflagged_count,
                    '_wpnonce'        => wp_create_nonce( 'wuclean-users-tab' ),
                ), admin_url( 'admin.php' ) );
                wp_safe_redirect( $redirect_url );
                exit;
            } else {
                // Fallback: show message and add JavaScript refresh.
                wp_enqueue_script( 'jquery' );
                $reload_script = 'setTimeout(function() { window.location.reload(); }, 1000);';
                wp_add_inline_script( 'jquery', $reload_script );
                echo '<div class="notice notice-info"><p>' . esc_html__( 'Page will refresh in 1 second to show updated data.', 'wuclean-database-cleaner-for-woocommerce' ) . '</p></div>';
            }
        } else {
            /* translators: %d: number of flagged users. */
            $found_msg = sprintf( __( 'Found %d flagged users, but no metadata was removed. This may indicate a database issue.', 'wuclean-database-cleaner-for-woocommerce' ), $total_found );
            echo '<div class="notice notice-warning"><p>' . esc_html( $found_msg ) . '</p></div>';
        }
    }
    // Show success message if users were unflagged (GET args only when tab nonce is valid; matches Orders pattern).
    if ( $users_tab_nonce_valid && isset( $_GET['unflagged_count'] ) && is_numeric( $_GET['unflagged_count'] ) ) {
        $unflagged_count = intval( $_GET['unflagged_count'] );
        if ( $unflagged_count > 0 ) {
            /* translators: %d: number of metadata entries. */
            $unflagged_msg = sprintf( __( '%d metadata entries have been removed. All flagged users have been reset and may be flagged again when the cleanup routine runs.', 'wuclean-database-cleaner-for-woocommerce' ), $unflagged_count );
            echo '<div class="notice notice-success is-dismissible"><p>' . esc_html( $unflagged_msg ) . '</p></div>';
        }
    }
    // Show success message if selected users were unflagged.
    if ( $users_tab_nonce_valid && isset( $_GET['unflagged_selected_count'] ) && is_numeric( $_GET['unflagged_selected_count'] ) ) {
        $unflagged_selected_count = intval( $_GET['unflagged_selected_count'] );
        if ( $unflagged_selected_count > 0 ) {
            $unflagged_selected_msg = sprintf( 
                /* translators: %d: number of users. */
                __( '%d selected user(s) have been unflagged.', 'wuclean-database-cleaner-for-woocommerce' ),
                $unflagged_selected_count
             );
            echo '<div class="notice notice-success is-dismissible"><p>' . esc_html( $unflagged_selected_msg ) . '</p></div>';
        }
    }
    // Search and pagination setup - only trust search parameter when nonce is valid or user has capability.
    $search_query = '';
    if ( isset( $_GET['flagged_user_search'] ) ) {
        $nonce_valid = false;
        if ( isset( $_GET['_wpnonce'] ) ) {
            $nonce = sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) );
            $nonce_valid = wp_verify_nonce( $nonce, 'wuclean-users-tab' );
        }
        if ( $nonce_valid || current_user_can( pt_wuclean_get_required_capability() ) ) {
            $search_query = sanitize_text_field( wp_unslash( $_GET['flagged_user_search'] ) );
        }
    }
    $displayed_users = array();
    if ( $show_protected ) {
        // Fetch protected users.
        $all_users = get_users( array(
            'fields' => 'all',
        ) );
        foreach ( $all_users as $user ) {
            // Validate user object.
            if ( !isset( $user->ID ) || !isset( $user->user_login ) ) {
                continue;
            }
            if ( pt_wuclean_is_user_protected( $user->ID ) ) {
                $order_stats = pt_wuclean_get_user_order_stats( $user->ID );
                $displayed_users[] = array(
                    'user_id'      => $user->ID,
                    'username'     => ( !empty( $user->user_login ) ? $user->user_login : 'User #' . $user->ID ),
                    'email'        => ( !empty( $user->user_email ) ? $user->user_email : '-' ),
                    'roles'        => ( !empty( $user->roles ) ? implode( ', ', $user->roles ) : '-' ),
                    'registered'   => ( !empty( $user->user_registered ) ? gmdate( 'Y-m-d', strtotime( $user->user_registered ) ) : '-' ),
                    'orders'       => $order_stats['orders'],
                    'flag_reason'  => esc_html__( 'Protected from cleanup', 'wuclean-database-cleaner-for-woocommerce' ),
                    'should_check' => false,
                );
            }
        }
    } else {
        // Fetch flagged users (existing logic).
        $all_flagged_users = pt_wuclean_get_flagged_users( 1000, $cutoff );
        if ( !empty( $all_flagged_users ) ) {
            foreach ( $all_flagged_users as $user ) {
                // Validate user object.
                if ( !isset( $user->ID ) || !isset( $user->user_login ) ) {
                    continue;
                }
                if ( pt_wuclean_is_user_protected( $user->ID ) ) {
                    continue;
                    // Skip protected users.
                }
                $order_stats = pt_wuclean_get_user_order_stats( $user->ID );
                // Get the flag reason from user meta.
                $flag_reason = get_user_meta( $user->ID, '_pt_wuclean_flag_reason', true );
                if ( empty( $flag_reason ) ) {
                    // Fallback: determine reason based on user data.
                    $user_age_days = (time() - strtotime( $user->user_registered )) / (24 * 3600);
                    if ( 0 === (int) $order_stats['orders'] ) {
                        $flag_reason = sprintf( 'New user no order for %.0f days', $order_stats['last_order_days'] );
                    } else {
                        $flag_reason = sprintf( 'Inactive user no order for %.0f days', $order_stats['last_order_days'] );
                    }
                }
                // Get the email sent date from user meta (check both email types, use most recent).
                $inactivity_email_sent_timestamp = get_user_meta( $user->ID, '_pt_wuclean_inactivity_email_sent', true );
                $new_customer_email_sent_timestamp = get_user_meta( $user->ID, '_pt_wuclean_new_customer_email_sent', true );
                $email_sent_timestamp = max( ( $inactivity_email_sent_timestamp ? intval( $inactivity_email_sent_timestamp ) : 0 ), ( $new_customer_email_sent_timestamp ? intval( $new_customer_email_sent_timestamp ) : 0 ) );
                $email_sent_date = '-';
                if ( $email_sent_timestamp > 0 ) {
                    $email_sent_date = gmdate( 'Y-m-d', $email_sent_timestamp );
                }
                $displayed_users[] = array(
                    'user_id'         => $user->ID,
                    'username'        => ( !empty( $user->user_login ) ? $user->user_login : 'User #' . $user->ID ),
                    'email'           => ( !empty( $user->user_email ) ? $user->user_email : '-' ),
                    'roles'           => ( !empty( $user->roles ) ? implode( ', ', $user->roles ) : '-' ),
                    'registered'      => ( !empty( $user->user_registered ) ? gmdate( 'Y-m-d', strtotime( $user->user_registered ) ) : '-' ),
                    'orders'          => $order_stats['orders'],
                    'flag_reason'     => $flag_reason,
                    'email_sent_date' => $email_sent_date,
                    'should_check'    => false,
                );
            }
        }
    }
    // Search filter.
    if ( '' !== $search_query ) {
        $displayed_users = array_filter( $displayed_users, function ( $user ) use($search_query, $show_protected) {
            if ( $show_protected ) {
                // For protected users, search in username, email, roles, registered, and status.
                foreach ( array(
                    'username',
                    'email',
                    'roles',
                    'registered'
                ) as $field ) {
                    if ( isset( $user[$field] ) && stripos( wp_strip_all_tags( (string) $user[$field] ), $search_query ) !== false ) {
                        return true;
                    }
                }
                // Also search in the status field (which shows "Protected").
                if ( stripos( 'Protected', $search_query ) !== false ) {
                    return true;
                }
            } else {
                // For flagged users, search in all fields including flag_reason.
                foreach ( array(
                    'username',
                    'email',
                    'roles',
                    'registered',
                    'flag_reason'
                ) as $field ) {
                    if ( isset( $user[$field] ) && stripos( wp_strip_all_tags( (string) $user[$field] ), $search_query ) !== false ) {
                        return true;
                    }
                }
            }
            return false;
        } );
    }
    $total_users = count( $displayed_users );
    $per_page = 25;
    // Standard pagination size for consistency.
    $page = ( isset( $_GET['flagged_user_page'] ) ? max( 1, absint( $_GET['flagged_user_page'] ) ) : 1 );
    $total_pages = max( 1, ceil( $total_users / $per_page ) );
    $paged_users = array_slice( array_values( $displayed_users ), ($page - 1) * $per_page, $per_page );
    echo '<div style="max-width: 1600px; margin-top: 2em;">';
    // Dynamic header based on view.
    if ( $show_protected ) {
        echo '<h2>🛡️ ' . esc_html__( 'Protected Customers', 'wuclean-database-cleaner-for-woocommerce' ) . '</h2>';
        echo '<div style="background: #e7f5e8; border: 1px solid #46b450; border-radius: 4px; padding: 15px; margin: 20px 0;">';
        echo '<p style="margin: 0;"><strong>ℹ️ ' . esc_html__( 'About Protected Users:', 'wuclean-database-cleaner-for-woocommerce' ) . '</strong> ' . esc_html__( 'These users are protected from being flagged. User protection is controlled via the user profile setting "Protect user from cleanup".', 'wuclean-database-cleaner-for-woocommerce' ) . '</p>';
        echo '</div>';
    } else {
        echo '<h2>🚩 ' . esc_html__( 'Flagged Customers', 'wuclean-database-cleaner-for-woocommerce' ) . '</h2>';
        echo '<div style="background: #fff3cd; border: 1px solid #ffc107; border-radius: 4px; padding: 15px; margin: 20px 0;">';
        if ( pt_wuclean_fs()->is_not_paying() ) {
            // Free version: Show basic feature description.
            echo '<p style="margin: 0;"><strong>ℹ️ ' . esc_html__( 'About Flagged Customers:', 'wuclean-database-cleaner-for-woocommerce' ) . '</strong> ' . esc_html__( 'Customers listed here have been flagged for cleanup. If you wish to try and reactivate the customer, use the Send Now button to send your custom email to the customer. Click the username to delete or protect the customer. All of this can be automated with Wuclean Pro👑', 'wuclean-database-cleaner-for-woocommerce' ) . '</p>';
        }
        echo '</div>';
    }
    // Search form - standardized pattern.
    echo '<form method="get" action="' . esc_url( admin_url( 'admin.php' ) ) . '" style="margin-bottom: 20px;">';
    echo '<input type="hidden" name="page" value="wuclean-users">';
    echo '<input type="hidden" name="tab" value="' . esc_attr( ( $show_protected ? 'protected' : 'flagged' ) ) . '">';
    echo '<input type="hidden" name="_wpnonce" value="' . esc_attr( wp_create_nonce( 'wuclean-users-tab' ) ) . '">';
    if ( $show_protected ) {
        echo '<input type="hidden" name="show_protected" value="1">';
    }
    $search_placeholder = ( $show_protected ? esc_attr__( 'Search by Username, Email, Role, or Registration Date', 'wuclean-database-cleaner-for-woocommerce' ) : esc_attr__( 'Search by Username, Email, Role, Registration Date, or Reason', 'wuclean-database-cleaner-for-woocommerce' ) );
    echo '<input type="text" name="flagged_user_search" value="' . esc_attr( $search_query ) . '" placeholder="' . esc_attr( $search_placeholder ) . '" style="width: 400px; padding: 5px; height: 30px; box-sizing: border-box;">';
    echo '<input type="submit" value="' . esc_attr__( 'Search', 'wuclean-database-cleaner-for-woocommerce' ) . '" class="button">';
    if ( !empty( $search_query ) ) {
        $clear_args = array(
            'page'     => 'wuclean-users',
            'tab'      => ( $show_protected ? 'protected' : 'flagged' ),
            '_wpnonce' => wp_create_nonce( 'wuclean-users-tab' ),
        );
        if ( $show_protected ) {
            $clear_args['show_protected'] = '1';
        }
        $clear_url = add_query_arg( $clear_args, admin_url( 'admin.php' ) );
        echo '<a href="' . esc_url( $clear_url ) . '" class="button">' . esc_html__( 'Clear Search', 'wuclean-database-cleaner-for-woocommerce' ) . '</a>';
    }
    echo '</form>';
    // Pagination info.
    if ( $total_users > 0 ) {
        $flagged_users_limit = 2000;
        echo '<div style="margin-bottom: 10px;">';
        if ( $show_protected ) {
            if ( !empty( $search_query ) ) {
                $filtered_message = sprintf(
                    /* translators: %1$d: start number, %2$d: end number, %3$d: total count. */
                    esc_html__( 'Showing %1$d-%2$d of %3$d protected users (filtered by search)', 'wuclean-database-cleaner-for-woocommerce' ),
                    ($page - 1) * $per_page + 1,
                    min( $page * $per_page, $total_users ),
                    $total_users
                );
                echo '<span>' . esc_html( $filtered_message ) . '</span>';
            } else {
                $users_message = sprintf(
                    /* translators: %1$d: start number, %2$d: end number, %3$d: total count. */
                    esc_html__( 'Showing %1$d-%2$d of %3$d protected users', 'wuclean-database-cleaner-for-woocommerce' ),
                    ($page - 1) * $per_page + 1,
                    min( $page * $per_page, $total_users ),
                    $total_users
                );
                echo '<span>' . esc_html( $users_message ) . '</span>';
            }
        } elseif ( !empty( $search_query ) ) {
            $filtered_message = sprintf(
                /* translators: %1$d: start number, %2$d: end number, %3$d: total count, %4$d: maximum number of users shown. */
                esc_html__( 'Showing %1$d-%2$d of %3$d users (filtered by search) (%4$d limit)', 'wuclean-database-cleaner-for-woocommerce' ),
                ($page - 1) * $per_page + 1,
                min( $page * $per_page, $total_users ),
                $total_users,
                $flagged_users_limit
            );
            echo '<span>' . esc_html( $filtered_message ) . '</span>';
        } else {
            $users_message = sprintf(
                /* translators: %1$d: start number, %2$d: end number, %3$d: total count, %4$d: maximum number of users shown. */
                esc_html__( 'Showing %1$d-%2$d of %3$d users (%4$d limit)', 'wuclean-database-cleaner-for-woocommerce' ),
                ($page - 1) * $per_page + 1,
                min( $page * $per_page, $total_users ),
                $total_users,
                $flagged_users_limit
            );
            echo '<span>' . esc_html( $users_message ) . '</span>';
        }
        echo '</div>';
    } else {
        // Show message when no users exist.
        echo '<div style="margin-bottom: 10px;">';
        if ( $show_protected ) {
            echo '<span>' . esc_html__( 'No protected users found.', 'wuclean-database-cleaner-for-woocommerce' ) . '</span>';
        } else {
            echo '<span>' . esc_html__( 'No flagged users found.', 'wuclean-database-cleaner-for-woocommerce' ) . '</span>';
        }
        echo '</div>';
    }
    echo '<form method="post" action="">';
    wp_nonce_field( 'pt_wuclean_flagged_users', 'pt_wuclean_flagged_users_nonce' );
    // Bulk action buttons (flagged view only).
    if ( !$show_protected ) {
        echo '<div style="margin-bottom: 20px;">';
        echo '<div style="display: flex; gap: 10px; align-items: center; flex-wrap: wrap;">';
        echo '<button type="submit" name="unflag_selected_users" value="1" class="button button-secondary" style="background: #f7f7f7; border-color: #ccc; color: #555; margin-left: 5px;" data-confirm-message="' . esc_attr( __( 'Are you sure you want to unflag the selected users? This will clear flagging data for the selected users and they may be flagged again when the cleanup routine runs.', 'wuclean-database-cleaner-for-woocommerce' ) ) . '">' . esc_html__( 'Unflag Selected Users', 'wuclean-database-cleaner-for-woocommerce' ) . '</button>';
        echo '<button type="submit" name="unflag_all_users" value="1" class="button button-secondary" style="background: #f7f7f7; border-color: #ccc; color: #555; margin-left: 5px;" data-confirm-message="' . esc_attr( __( 'Are you sure you want to unflag ALL users? This will clear all flagging data and users may be flagged again when the cleanup routine runs.', 'wuclean-database-cleaner-for-woocommerce' ) ) . '">' . esc_html__( 'Unflag All Users', 'wuclean-database-cleaner-for-woocommerce' ) . '</button>';
        echo '<span id="flagged-selected-count" style="margin-left: 10px; color: #666;"></span>';
        echo '</div>';
        echo '</div>';
    }
    // Bulk action buttons (protected view).
    if ( $show_protected ) {
    }
    // Main table - updated styling (always show table structure).
    echo '<table class="wp-list-table widefat fixed striped" style="margin-top: 20px;">';
    echo '<thead>';
    echo '<tr>';
    $select_title = ( $show_protected ? esc_attr__( 'Select users to unprotect', 'wuclean-database-cleaner-for-woocommerce' ) : esc_attr__( 'Select users to protect or delete', 'wuclean-database-cleaner-for-woocommerce' ) );
    if ( !$show_protected ) {
        echo '<th style="text-align: left; width: 60px;" title="' . esc_attr( $select_title ) . '">' . esc_html__( 'Select', 'wuclean-database-cleaner-for-woocommerce' ) . ' <input type="checkbox" id="ptac-master-toggle" title="' . esc_attr__( 'Select all users on this page', 'wuclean-database-cleaner-for-woocommerce' ) . '"></th>';
    }
    if ( $show_protected ) {
    }
    echo '<th style="text-align: left; width: 100px;" title="' . esc_attr__( 'User role(s)', 'wuclean-database-cleaner-for-woocommerce' ) . '">' . esc_html__( 'Role', 'wuclean-database-cleaner-for-woocommerce' ) . '</th>';
    echo '<th style="text-align: left; width: 130px;" title="' . esc_attr__( 'Username (login name)', 'wuclean-database-cleaner-for-woocommerce' ) . '">' . esc_html__( 'Username', 'wuclean-database-cleaner-for-woocommerce' ) . '</th>';
    echo '<th style="text-align: left; width: 180px;" title="' . esc_attr__( 'User email address', 'wuclean-database-cleaner-for-woocommerce' ) . '">' . esc_html__( 'Email', 'wuclean-database-cleaner-for-woocommerce' ) . '</th>';
    echo '<th style="text-align: left; width: 80px;" title="' . esc_attr__( 'Date user registered', 'wuclean-database-cleaner-for-woocommerce' ) . '">' . esc_html__( 'Registered', 'wuclean-database-cleaner-for-woocommerce' ) . '</th>';
    echo '<th style="text-align: right; width: 40px;" title="' . esc_attr__( 'Number of orders', 'wuclean-database-cleaner-for-woocommerce' ) . '">' . esc_html__( 'Orders', 'wuclean-database-cleaner-for-woocommerce' ) . '</th>';
    if ( $show_protected ) {
        echo '<th style="text-align: left; width: 180px;" title="' . esc_attr__( 'Protection status', 'wuclean-database-cleaner-for-woocommerce' ) . '">' . esc_html__( 'Status', 'wuclean-database-cleaner-for-woocommerce' ) . '</th>';
    } else {
        echo '<th style="text-align: left; width: 200px;" title="' . esc_attr__( 'Reason why this user was flagged for cleanup', 'wuclean-database-cleaner-for-woocommerce' ) . '">' . esc_html__( 'Reason', 'wuclean-database-cleaner-for-woocommerce' ) . '</th>';
        echo '<th style="text-align: left; width: 120px;" title="' . esc_attr__( 'Send a customer warning email for this row', 'wuclean-database-cleaner-for-woocommerce' ) . '">' . esc_html__( 'Send Email', 'wuclean-database-cleaner-for-woocommerce' ) . '</th>';
        echo '<th style="text-align: left; width: 80px;" title="' . esc_attr__( 'Date inactivity email was sent', 'wuclean-database-cleaner-for-woocommerce' ) . '">' . esc_html__( 'Email Sent', 'wuclean-database-cleaner-for-woocommerce' ) . '</th>';
    }
    echo '</tr>';
    echo '</thead>';
    echo '<tbody>';
    if ( !empty( $paged_users ) ) {
        $row_index = 0;
        foreach ( $paged_users as $user_data ) {
            // Defensive check: ensure required fields exist.
            if ( !isset( $user_data['user_id'] ) ) {
                continue;
            }
            $bg_color = ( 0 === $row_index % 2 ? '#ffffff' : '#f9f9f9' );
            echo '<tr style="background-color: ' . esc_attr( $bg_color ) . '; transition: background-color 0.2s;" onmouseover="this.style.backgroundColor=\'#e8f4f8\'" onmouseout="this.style.backgroundColor=\'' . esc_attr( $bg_color ) . '\'">';
            ++$row_index;
            if ( !$show_protected ) {
                echo '<td style="text-align: left;"><input type="checkbox" class="flagged-user-checkbox" name="select_users[]" value="' . esc_attr( $user_data['user_id'] ) . '"></td>';
            }
            if ( $show_protected ) {
            }
            echo '<td>' . esc_html( $user_data['roles'] ?? '-' ) . '</td>';
            $username_label = $user_data['username'] ?? 'User #' . $user_data['user_id'];
            if ( $show_protected ) {
                // Protected tab: open the user's profile (edit screen) in a new tab.
                $profile_url = get_edit_user_link( (int) $user_data['user_id'] );
                if ( $profile_url ) {
                    echo '<td><a href="' . esc_url( $profile_url ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $username_label ) . '</a></td>';
                } else {
                    echo '<td>' . esc_html( $username_label ) . '</td>';
                }
            } else {
                // Flagged tab: search Users list by username (legacy behavior).
                $user_search_url = add_query_arg( array(
                    's' => $username_label,
                ), admin_url( 'users.php' ) );
                echo '<td><a href="' . esc_url( $user_search_url ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $username_label ) . '</a></td>';
            }
            echo '<td>' . esc_html( $user_data['email'] ?? '-' ) . '</td>';
            echo '<td>' . esc_html( $user_data['registered'] ?? '-' ) . '</td>';
            // Make order count clickable to show user's orders.
            $orders_count = $user_data['orders'] ?? 0;
            $orders_url = admin_url( 'edit.php?post_status=all&post_type=shop_order&_customer_user=' . $user_data['user_id'] );
            if ( $orders_count > 0 ) {
                echo '<td style="text-align: right;"><a href="' . esc_url( $orders_url ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $orders_count ) . '</a></td>';
            } else {
                echo '<td style="text-align: right;">' . esc_html( $orders_count ) . '</td>';
            }
            if ( $show_protected ) {
                echo '<td style="text-align: left;">';
                echo '<span style="display: inline-block; margin-right: 10px;">' . esc_html__( 'Protected', 'wuclean-database-cleaner-for-woocommerce' ) . '</span>';
                echo '</td>';
            } else {
                $flag_reason = (string) ($user_data['flag_reason'] ?? '');
                echo '<td>' . esc_html( ( '' !== $flag_reason ? $flag_reason : '-' ) ) . '</td>';
                echo '<td style="text-align: left;">';
                echo '<button type="submit" name="send_user_email" value="' . esc_attr( $user_data['user_id'] ) . '" class="button button-small" style="white-space: nowrap;">' . esc_html__( 'Send Now', 'wuclean-database-cleaner-for-woocommerce' ) . '</button>';
                echo '</td>';
                echo '<td>' . esc_html( $user_data['email_sent_date'] ?? '-' ) . '</td>';
            }
            echo '</tr>';
        }
    } else {
        // Show appropriate empty message based on context.
        if ( $show_protected ) {
            $colspan = 6;
        } else {
            // Flagged view always includes Select + 8 data columns (matches thead/body above).
            $colspan = 9;
        }
        echo '<tr><td colspan="' . absint( $colspan ) . '" style="text-align: center; padding: 20px;">';
        if ( $show_protected ) {
            echo esc_html__( 'No protected users found.', 'wuclean-database-cleaner-for-woocommerce' );
        } else {
            echo esc_html__( 'No users found matching the current criteria.', 'wuclean-database-cleaner-for-woocommerce' );
        }
        echo '</td></tr>';
    }
    echo '</tbody>';
    echo '</table>';
    // Pagination - using centralized function.
    $preserve_params = array(
        'page'     => 'wuclean-users',
        'tab'      => ( $show_protected ? 'protected' : 'flagged' ),
        '_wpnonce' => wp_create_nonce( 'wuclean-users-tab' ),
    );
    if ( !empty( $search_query ) ) {
        $preserve_params['flagged_user_search'] = $search_query;
    }
    if ( $show_protected ) {
        $preserve_params['show_protected'] = '1';
    }
    pt_wuclean_render_pagination( array(
        'current_page'    => $page,
        'total_pages'     => $total_pages,
        'base_url'        => admin_url( 'admin.php' ),
        'page_param'      => 'flagged_user_page',
        'preserve_params' => $preserve_params,
    ) );
    // Enqueue JavaScript files for flagged users tab.
    wp_enqueue_script(
        'pt-wuclean-common',
        plugin_dir_url( __FILE__ ) . 'assets/js/common.js',
        array('jquery'),
        '1.0.0',
        true
    );
    wp_enqueue_script(
        'pt-wuclean-users',
        plugin_dir_url( __FILE__ ) . 'assets/js/users.js',
        array('jquery', 'pt-wuclean-common'),
        '1.0.0',
        true
    );
    wp_localize_script( 'pt-wuclean-common', 'pt_wuclean_common_vars', pt_wuclean_get_common_script_strings() );
    wp_localize_script( 'pt-wuclean-users', 'pt_wuclean_users_vars', pt_wuclean_get_users_script_vars() );
    echo '</div>';
}

/**
 * User Logs Tab
 */
function pt_wuclean_render_user_logs_tab() {
    if ( !current_user_can( pt_wuclean_get_required_capability() ) ) {
        wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'wuclean-database-cleaner-for-woocommerce' ) );
    }
    // Handle clear logs action.
    if ( isset( $_POST['clear_user_logs'] ) && isset( $_POST['pt_wuclean_clear_logs_nonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['pt_wuclean_clear_logs_nonce'] ) ), 'pt_wuclean_clear_user_logs' ) ) {
        delete_option( 'pt_wuclean_user_logs' );
        echo '<div class="notice notice-success"><p>' . esc_html__( 'User logs cleared successfully.', 'wuclean-database-cleaner-for-woocommerce' ) . '</p></div>';
    }
    $settings = get_option( 'pt_wuclean_settings', array() );
    $log_limit = ( isset( $settings['user_log_limit'] ) ? max( 0, min( intval( $settings['user_log_limit'] ), pt_wuclean_Plugin::DEFAULT_USER_LOG_LIMIT ) ) : pt_wuclean_Plugin::DEFAULT_USER_LOG_LIMIT );
    // Get user logs from WordPress options.
    $all_logs = get_option( 'pt_wuclean_user_logs', array() );
    // Sort logs by timestamp (newest first).
    usort( $all_logs, function ( $a, $b ) {
        return strtotime( $b['timestamp'] ) - strtotime( $a['timestamp'] );
    } );
    // Apply log limit if set.
    if ( 0 < $log_limit ) {
        $all_logs = array_slice( $all_logs, 0, $log_limit );
    }
    // Get search query - only trust search parameter when nonce is valid or user has capability.
    $search_query = '';
    if ( isset( $_GET['user_log_search'] ) ) {
        $nonce_valid = false;
        if ( isset( $_GET['_wpnonce'] ) ) {
            $nonce = sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) );
            $nonce_valid = wp_verify_nonce( $nonce, 'wuclean-users-tab' );
        }
        if ( $nonce_valid || current_user_can( pt_wuclean_get_required_capability() ) ) {
            $search_query = sanitize_text_field( wp_unslash( $_GET['user_log_search'] ) );
        }
    }
    // Apply search filter if needed.
    $logs = $all_logs;
    if ( '' !== $search_query ) {
        $logs = array_filter( $logs, function ( $log ) use($search_query) {
            foreach ( array(
                'user_id',
                'username',
                'name',
                'email'
            ) as $field ) {
                if ( isset( $log[$field] ) && stripos( (string) $log[$field], $search_query ) !== false ) {
                    return true;
                }
            }
            return false;
        } );
    }
    // Pagination setup.
    $total_logs = count( $logs );
    $per_page = 25;
    // Standard pagination size for consistency.
    $page = ( isset( $_GET['user_log_page'] ) ? max( 1, intval( $_GET['user_log_page'] ) ) : 1 );
    $total_pages = max( 1, ceil( $total_logs / $per_page ) );
    $page = min( $page, $total_pages );
    $paged_logs = array_slice( $logs, ($page - 1) * $per_page, $per_page );
    echo '<div style="max-width: 1600px; margin-top: 2em;">';
    echo '<h2>📝 ' . esc_html__( 'User Logs', 'wuclean-database-cleaner-for-woocommerce' ) . '</h2>';
    // Info box.
    echo '<div style="background: #e7f3ff; border: 1px solid #2196f3; border-radius: 4px; padding: 15px; margin: 20px 0;">';
    echo '<p style="margin: 0;"><strong>ℹ️ ' . esc_html__( 'About User Logs:', 'wuclean-database-cleaner-for-woocommerce' ) . '</strong> ' . esc_html__( 'This log tracks all customer cleanup actions including flags, protections, and deletions.', 'wuclean-database-cleaner-for-woocommerce' ) . '</p>';
    echo '</div>';
    // No logs stored at all (same pattern as order logs: no search UI yet).
    if ( empty( $all_logs ) ) {
        echo '<div style="background: #fff; border: 1px solid #ccd0d4; border-radius: 4px; padding: 20px; margin: 20px 0; text-align: center;">';
        echo '<p style="margin: 0; color: #646970;">' . esc_html__( 'No user logs found.', 'wuclean-database-cleaner-for-woocommerce' ) . '</p>';
        echo '<p style="margin: 10px 0 0 0; color: #646970; font-size: 13px;">' . esc_html__( 'Any new user actions will be logged here.', 'wuclean-database-cleaner-for-woocommerce' ) . '</p>';
        echo '</div>';
        echo '</div>';
        return;
    }
    // Search form - standardized pattern (always shown when logs exist; matches order logs tab).
    echo '<form method="get" action="' . esc_url( admin_url( 'admin.php' ) ) . '" style="margin-bottom: 20px;">';
    echo '<input type="hidden" name="page" value="wuclean-users">';
    echo '<input type="hidden" name="tab" value="logs">';
    echo '<input type="hidden" name="_wpnonce" value="' . esc_attr( wp_create_nonce( 'wuclean-users-tab' ) ) . '">';
    $search_placeholder = esc_attr__( 'Search by User ID, Username, Name, or Email', 'wuclean-database-cleaner-for-woocommerce' );
    echo '<input type="text" name="user_log_search" value="' . esc_attr( $search_query ) . '" placeholder="' . esc_attr( $search_placeholder ) . '" style="width: 400px; padding: 5px; height: 30px; box-sizing: border-box;">';
    echo '<input type="submit" value="' . esc_attr__( 'Search', 'wuclean-database-cleaner-for-woocommerce' ) . '" class="button">';
    if ( !empty( $search_query ) ) {
        $clear_url = add_query_arg( array(
            'page'     => 'wuclean-users',
            'tab'      => 'logs',
            '_wpnonce' => wp_create_nonce( 'wuclean-users-tab' ),
        ), admin_url( 'admin.php' ) );
        echo '<a href="' . esc_url( $clear_url ) . '" class="button">' . esc_html__( 'Clear Search', 'wuclean-database-cleaner-for-woocommerce' ) . '</a>';
    }
    echo '</form>';
    // Show count and pagination info.
    if ( $total_logs > 0 ) {
        echo '<div style="margin-bottom: 10px;">';
        if ( !empty( $search_query ) ) {
            $filtered_message = sprintf(
                /* translators: %1$d: start number, %2$d: end number, %3$d: total count. */
                esc_html__( 'Showing %1$d-%2$d of %3$d logs (filtered by search)', 'wuclean-database-cleaner-for-woocommerce' ),
                ($page - 1) * $per_page + 1,
                min( $page * $per_page, $total_logs ),
                $total_logs
            );
            echo '<span>' . esc_html( $filtered_message ) . '</span>';
        } else {
            $logs_message = sprintf(
                /* translators: %1$d: start number, %2$d: end number, %3$d: total count. */
                esc_html__( 'Showing %1$d-%2$d of %3$d logs', 'wuclean-database-cleaner-for-woocommerce' ),
                ($page - 1) * $per_page + 1,
                min( $page * $per_page, $total_logs ),
                $total_logs
            );
            echo '<span>' . esc_html( $logs_message ) . '</span>';
        }
        echo '</div>';
    }
    // Clear logs button.
    if ( $total_logs > 0 ) {
        echo '<div style="margin-bottom: 20px;">';
        echo '<form method="post" style="display: inline;">';
        wp_nonce_field( 'pt_wuclean_clear_user_logs', 'pt_wuclean_clear_logs_nonce' );
        echo '<button type="submit" name="clear_user_logs" class="button button-secondary" data-confirm-message="' . esc_attr( __( 'Are you sure you want to clear all user logs? This action cannot be undone.', 'wuclean-database-cleaner-for-woocommerce' ) ) . '">' . esc_html__( 'Clear All Logs', 'wuclean-database-cleaner-for-woocommerce' ) . '</button>';
        echo '</form>';
        echo '</div>';
    }
    // Main table (order logs pattern: empty search results keep search visible + in-table message).
    echo '<table class="wp-list-table widefat fixed striped">';
    echo '<thead><tr>';
    echo '<th style="width: 50px;">' . esc_html__( 'User ID', 'wuclean-database-cleaner-for-woocommerce' ) . '</th>';
    echo '<th style="width: 150px;">' . esc_html__( 'Username', 'wuclean-database-cleaner-for-woocommerce' ) . '</th>';
    echo '<th>' . esc_html__( 'Name', 'wuclean-database-cleaner-for-woocommerce' ) . '</th>';
    echo '<th>' . esc_html__( 'Email', 'wuclean-database-cleaner-for-woocommerce' ) . '</th>';
    echo '<th style="width: 120px;">' . esc_html__( 'Action', 'wuclean-database-cleaner-for-woocommerce' ) . '</th>';
    echo '<th style="width: 120px;">' . esc_html__( 'Timestamp', 'wuclean-database-cleaner-for-woocommerce' ) . '</th>';
    echo '</tr></thead>';
    echo '<tbody>';
    if ( !empty( $paged_logs ) ) {
        $row_index = 0;
        foreach ( $paged_logs as $log ) {
            // Use stored username from log entry instead of trying to fetch from deleted user.
            $username = ( !empty( $log['username'] ) ? $log['username'] : 'Unknown' );
            $bg_color = ( 0 === $row_index % 2 ? '#ffffff' : '#f9f9f9' );
            echo '<tr style="background-color: ' . esc_attr( $bg_color ) . '; transition: background-color 0.2s;" onmouseover="this.style.backgroundColor=\'#e8f4f8\'" onmouseout="this.style.backgroundColor=\'' . esc_attr( $bg_color ) . '\'">';
            ++$row_index;
            echo '<td>' . esc_html( $log['user_id'] ) . '</td>';
            echo '<td>' . esc_html( $username ) . '</td>';
            echo '<td>' . esc_html( $log['name'] ?? 'N/A' ) . '</td>';
            echo '<td>' . esc_html( $log['email'] ?? 'N/A' ) . '</td>';
            echo '<td>' . esc_html( $log['state'] ?? 'N/A' ) . '</td>';
            echo '<td>' . esc_html( $log['timestamp'] ) . '</td>';
            echo '</tr>';
        }
    } elseif ( '' !== $search_query ) {
        echo '<tr><td colspan="6" style="text-align: center; padding: 20px;">';
        echo esc_html__( 'No logs match your search criteria.', 'wuclean-database-cleaner-for-woocommerce' );
        $clear_url = add_query_arg( array(
            'page'     => 'wuclean-users',
            'tab'      => 'logs',
            '_wpnonce' => wp_create_nonce( 'wuclean-users-tab' ),
        ), admin_url( 'admin.php' ) );
        echo '<br><a href="' . esc_url( $clear_url ) . '" class="button" style="margin-top: 10px;">' . esc_html__( 'Clear Search', 'wuclean-database-cleaner-for-woocommerce' ) . '</a>';
        echo '</td></tr>';
    } else {
        echo '<tr><td colspan="6" style="text-align: center; padding: 20px;">' . esc_html__( 'No user logs found.', 'wuclean-database-cleaner-for-woocommerce' ) . '</td></tr>';
    }
    echo '</tbody>';
    echo '</table>';
    // Pagination - using centralized function (matches order logs: always render after table).
    $preserve_params = array(
        'page'     => 'wuclean-users',
        'tab'      => 'logs',
        '_wpnonce' => wp_create_nonce( 'wuclean-users-tab' ),
    );
    if ( !empty( $search_query ) ) {
        $preserve_params['user_log_search'] = $search_query;
    }
    pt_wuclean_render_pagination( array(
        'current_page'    => $page,
        'total_pages'     => $total_pages,
        'base_url'        => admin_url( 'admin.php' ),
        'page_param'      => 'user_log_page',
        'preserve_params' => $preserve_params,
    ) );
    // Enqueue common JavaScript so the pagination "Go" button works on this tab.
    wp_enqueue_script(
        'pt-wuclean-common',
        plugin_dir_url( __FILE__ ) . 'assets/js/common.js',
        array('jquery'),
        '1.0.0',
        true
    );
    wp_localize_script( 'pt-wuclean-common', 'pt_wuclean_common_vars', pt_wuclean_get_common_script_strings() );
    echo '</div>';
}

/**
 * Count total flagged users (excluding protected)
 *
 * @return int Number of flagged users
 */
function pt_wuclean_count_flagged_users() {
    global $wpdb;
    // Get all flagged user IDs.
    $flagged_user_ids = $wpdb->get_col( "SELECT user_id FROM {$wpdb->usermeta} WHERE meta_key = '_pt_wuclean_flagged_at'" );
    if ( empty( $flagged_user_ids ) ) {
        return 0;
    }
    // Exclude protected users.
    $protected_users = get_option( 'pt_wuclean_protected_users', array() );
    $flagged_user_ids = array_diff( $flagged_user_ids, $protected_users );
    return count( $flagged_user_ids );
}

/**
 * Flag users for cleanup.
 *
 * @param bool $is_manual Whether this is a manual scan (bypasses enable check).
 */
function pt_wuclean_flag_users_for_cleanup(  $is_manual = false  ) {
    $settings = get_option( 'pt_wuclean_settings', array() );
    // Only run automatic cleanup if user cleanup is enabled (manual scans bypass this).
    if ( !$is_manual && empty( $settings['enable_user_cleanup'] ) ) {
        return;
        // Exit early if user cleanup is disabled.
    }
    $batch_size = intval( $settings['user_cleanup_batch_size'] ?? 10 );
    $target_roles_setting_exists = is_array( $settings ) && array_key_exists( 'user_target_roles', $settings );
    $raw_target_roles = ( $target_roles_setting_exists ? (array) $settings['user_target_roles'] : array('customer') );
    $target_roles = array_filter( array_map( 'sanitize_key', $raw_target_roles ) );
    // Ensure administrator and shop_manager are never included in target roles.
    $excluded_roles = array('administrator', 'shop_manager');
    $target_roles = array_diff( $target_roles, $excluded_roles );
    // If no roles are selected, skip the scan (manual scans should report zero results).
    if ( empty( $target_roles ) ) {
        if ( $is_manual ) {
            return array(
                'total_checked'           => 0,
                'flagged_count'           => 0,
                'skipped_protected'       => 0,
                'skipped_already_flagged' => 0,
            );
        }
        return;
    }
    $delete_new_no_order_days = intval( $settings['user_delete_new_no_order_days'] ?? 0 );
    $delete_inactive_days = intval( $settings['user_delete_inactive_days'] ?? 0 );
    // Check if orphaned data detection is enabled for scheduled scans.
    $detect_orphaned = !$is_manual && ($settings['user_detect_orphaned_data'] ?? 0);
    // If no cleanup criteria are set AND orphaned detection is not enabled, return.
    if ( $delete_new_no_order_days <= 0 && $delete_inactive_days <= 0 && !$detect_orphaned ) {
        return;
    }
    // If no cleanup criteria but orphaned detection is enabled, run only the orphaned scan.
    if ( $delete_new_no_order_days <= 0 && $delete_inactive_days <= 0 && $detect_orphaned ) {
        return;
    }
    $now = time();
    $flagged_users = array();
    $skipped_protected = 0;
    $skipped_already_flagged = 0;
    $total_checked = 0;
    // Get last scan position from previous run (offset persistence).
    // This allows us to cycle through ALL users over multiple cron runs.
    $last_offset = get_option( 'pt_wuclean_user_scan_offset', 0 );
    $offset = intval( $last_offset );
    $search_batch_size = 50;
    // Search in safe chunks of 50 users at a time.
    $max_to_check = 1000;
    // Conservative limit for shared hosting.
    // Keep searching until we find enough new users to flag or run out of users.
    $flagged_count = count( $flagged_users );
    while ( $flagged_count < $batch_size && $total_checked < $max_to_check ) {
        $result = pt_wuclean_find_users_to_flag(
            $search_batch_size,
            $offset,
            $target_roles,
            $delete_new_no_order_days,
            $delete_inactive_days
        );
        $users = $result['users'] ?? array();
        $users_checked = $result['checked'] ?? 0;
        if ( empty( $users ) && 0 === $users_checked ) {
            // Reached end of users - reset offset to start from beginning on next run.
            update_option( 'pt_wuclean_user_scan_offset', 0, false );
            break;
        }
        // Update offset by the number of users actually checked.
        $offset += $users_checked;
        $total_checked += $users_checked;
        foreach ( $users as $user_id ) {
            ++$total_checked;
            if ( pt_wuclean_is_user_protected( $user_id ) ) {
                ++$skipped_protected;
                continue;
            }
            if ( get_user_meta( $user_id, '_pt_wuclean_flagged_at', true ) ) {
                ++$skipped_already_flagged;
                continue;
            }
            // Found a new user to flag.
            update_user_meta( $user_id, '_pt_wuclean_flagged_at', $now );
            // Store the flagging reason.
            $user = get_userdata( $user_id );
            if ( $user ) {
                $flag_reason = pt_wuclean_determine_user_flag_reason( $user_id, $delete_new_no_order_days, $delete_inactive_days );
                if ( $flag_reason ) {
                    update_user_meta( $user_id, '_pt_wuclean_flag_reason', $flag_reason );
                }
                // Log the flagging action.
                pt_wuclean_log_user_action( $user_id, 'Flagged for cleanup', $user );
            }
            $flagged_users[] = $user_id;
            $flagged_count = count( $flagged_users );
            if ( $batch_size <= $flagged_count ) {
                break;
            }
        }
        $flagged_count = count( $flagged_users );
    }
    // Save progress for next cron run (offset persistence).
    // Only update if we checked users and haven't reached the end.
    if ( $total_checked > 0 && $max_to_check <= $total_checked ) {
        // Hit the limit - save position to continue from here next time.
        update_option( 'pt_wuclean_user_scan_offset', $offset, false );
    } elseif ( $total_checked > 0 && $max_to_check > $total_checked ) {
        // Checked fewer than max - likely reached end, save current offset anyway.
        update_option( 'pt_wuclean_user_scan_offset', $offset, false );
    }
    // Return statistics for manual scans.
    if ( $is_manual ) {
        return array(
            'total_checked'           => $total_checked,
            'flagged_count'           => count( $flagged_users ),
            'skipped_protected'       => $skipped_protected,
            'skipped_already_flagged' => $skipped_already_flagged,
        );
    }
}

/**
 * Find users to flag.
 *
 * @param int   $batch_size               Number of users to return per batch.
 * @param int   $offset                   Offset for pagination.
 * @param array $target_roles             Roles to include.
 * @param int   $delete_new_no_order_days Days after which new users without orders are flagged.
 * @param int   $delete_inactive_days     Days of inactivity after which users are flagged.
 * @return array{users: list<int>, checked: int}
 */
function pt_wuclean_find_users_to_flag(
    $batch_size,
    $offset,
    $target_roles,
    $delete_new_no_order_days,
    $delete_inactive_days
) {
    global $wpdb;
    if ( empty( $target_roles ) ) {
        return array(
            'users'   => array(),
            'checked' => 0,
        );
    }
    $now = time();
    $users_to_flag = array();
    // Build role query with exact role matching.
    // WordPress stores roles as serialized arrays: a:1:{s:8:"customer";b:1;}.
    // We need to match the exact serialized key format: s:LENGTH:"rolename".
    $role_placeholders = array();
    $role_args = array();
    foreach ( $target_roles as $role ) {
        $role_length = strlen( $role );
        $role_placeholders[] = 'um.meta_value LIKE %s';
        $role_args[] = '%s:' . $role_length . ':"' . $wpdb->esc_like( $role ) . '"%';
    }
    $role_clause = ( !empty( $role_placeholders ) ? ' AND (' . implode( ' OR ', $role_placeholders ) . ')' : '' );
    // Get users with target roles.
    $meta_key = $wpdb->prefix . 'capabilities';
    // Build query args: meta_key + role_args (dynamic count) + LIMIT + OFFSET.
    $query_args = array_merge( array($meta_key), $role_args, array($batch_size * 2, $offset) );
    // PCP-compliant because prepare() is *directly* inside get_results() with SQL string inlined.
    // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $role_clause contains safe placeholder tokens (%s) that are part of the SQL structure; table names are $wpdb->users and $wpdb->usermeta.
    // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- Dynamic placeholders: {$role_clause} contains count($role_placeholders) of %s; total placeholders = 1 + count($role_args) + 2; params = ...$query_args.
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Batch user lookup by role.
    // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- $role_clause contains only static 'um.meta_value LIKE %s' fragments; role data is escaped with $wpdb->esc_like() and passed via ...$query_args into prepare().
    $users = $wpdb->get_results( 
        // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- Dynamic placeholders in $role_clause; total = 1 + count($role_args) + 2, passed via ...$query_args.
        $wpdb->prepare( "SELECT DISTINCT u.ID, u.user_registered \n             FROM {$wpdb->users} u \n             INNER JOIN {$wpdb->usermeta} um ON u.ID = um.user_id \n             WHERE um.meta_key = %s\n             {$role_clause}\n             ORDER BY u.user_registered ASC \n             LIMIT %d OFFSET %d", ...$query_args )
     );
    // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
    $checked = 0;
    $skipped_purchased = 0;
    $skipped_not_old_enough = 0;
    foreach ( $users as $user ) {
        $user_id = $user->ID;
        $registration_date = strtotime( $user->user_registered );
        $user_age_days = ($now - $registration_date) / (24 * 3600);
        ++$checked;
        $should_flag = false;
        // Check and set persistent purchase flag.
        $ever_purchased = pt_wuclean_check_and_set_purchase_flag( $user_id );
        // Check new users without orders.
        // Only flag users who have NEVER made a purchase (persistent check).
        if ( 0 < $delete_new_no_order_days && $delete_new_no_order_days <= $user_age_days ) {
            // Only flag if user has NEVER purchased.
            if ( !$ever_purchased ) {
                $should_flag = true;
            } else {
                ++$skipped_purchased;
            }
        } else {
            ++$skipped_not_old_enough;
        }
        // Check inactive users (only if not already flagged as new user without orders).
        if ( 0 < $delete_inactive_days && !$should_flag ) {
            $last_order_date = pt_wuclean_get_user_last_order_date( $user_id );
            if ( $last_order_date ) {
                // Check if they're inactive.
                $days_since_last_order = ($now - $last_order_date) / (24 * 3600);
                if ( $delete_inactive_days <= $days_since_last_order ) {
                    $should_flag = true;
                }
            } elseif ( $delete_inactive_days <= $user_age_days ) {
                // User has no orders at all, check if they're old enough.
                $should_flag = true;
            }
        }
        if ( $should_flag ) {
            // Check if user is protected before adding to flag list.
            if ( pt_wuclean_is_user_protected( $user_id ) ) {
                continue;
            }
            $users_to_flag[] = $user_id;
            if ( $batch_size <= count( $users_to_flag ) ) {
                break;
            }
        }
    }
    // Return both users to flag and count of users actually checked.
    // This is needed to correctly increment the offset in manual scans.
    return array(
        'users'   => $users_to_flag,
        'checked' => $checked,
    );
}

/**
 * Check if user has orders (HPOS-only).
 *
 * @param int $user_id User ID.
 * @return bool
 */
function pt_wuclean_user_has_orders(  $user_id  ) {
    $args = array(
        'customer_id' => $user_id,
        'limit'       => 1,
        'status'      => array(
            'processing',
            'completed',
            'on-hold',
            'pending'
        ),
        'return'      => 'ids',
    );
    $order_ids = wc_get_orders( $args );
    return count( $order_ids ) > 0;
}

/**
 * Get user's last order date (HPOS-only).
 *
 * @param int $user_id User ID.
 * @return int|null Timestamp or null.
 */
function pt_wuclean_get_user_last_order_date(  $user_id  ) {
    $args = array(
        'customer_id' => $user_id,
        'limit'       => 1,
        'orderby'     => 'date',
        'order'       => 'DESC',
        'status'      => array(
            'processing',
            'completed',
            'on-hold',
            'pending'
        ),
        'return'      => 'objects',
    );
    $orders = wc_get_orders( $args );
    if ( !empty( $orders ) ) {
        $order = $orders[0];
        $date = $order->get_date_created();
        return ( $date ? $date->getTimestamp() : null );
    }
    return null;
}

/**
 * Get user order statistics (HPOS-only).
 *
 * @param int $user_id User ID.
 * @return array{last_order: string, orders: int, spent: float}
 */
function pt_wuclean_get_user_order_stats(  $user_id  ) {
    $args = array(
        'customer_id' => $user_id,
        'limit'       => -1,
        'status'      => array(
            'processing',
            'completed',
            'on-hold',
            'pending'
        ),
        'return'      => 'objects',
    );
    $orders = wc_get_orders( $args );
    $stats = array(
        'last_order' => '',
        'orders'     => count( $orders ),
        'spent'      => 0,
    );
    if ( !empty( $orders ) ) {
        $last_order = null;
        $total_spent = 0;
        foreach ( $orders as $order ) {
            $total_spent += $order->get_total();
            if ( !$last_order || $order->get_date_created() > $last_order->get_date_created() ) {
                $last_order = $order;
            }
        }
        if ( $last_order ) {
            $date = $last_order->get_date_created();
            $stats['last_order'] = ( $date ? $date->date( 'Y-m-d H:i:s' ) : '' );
        }
        $stats['spent'] = $total_spent;
    }
    return $stats;
}

/**
 * Get flagged users.
 *
 * @param int $batch_size Batch size.
 * @param int $cutoff     Unix timestamp cutoff.
 * @return \WP_User[]
 */
function pt_wuclean_get_flagged_users(  $batch_size, $cutoff  ) {
    global $wpdb;
    $meta_key = '_pt_wuclean_flagged_at';
    // Ensure cutoff is an integer timestamp.
    $cutoff = intval( $cutoff );
    // Look for users flagged AFTER the cutoff time (within the last X hours).
    $user_ids = $wpdb->get_col( $wpdb->prepare(
        "SELECT user_id\n\t\t\tFROM {$wpdb->usermeta}\n\t\t\tWHERE meta_key = %s AND meta_value >= %d\n\t\t\tORDER BY ( meta_value + 0 ) DESC, user_id DESC\n\t\t\tLIMIT %d",
        $meta_key,
        $cutoff,
        $batch_size * 2
    ) );
    // Convert user IDs to user objects.
    $user_objects = array();
    foreach ( $user_ids as $user_id ) {
        $user = get_userdata( $user_id );
        if ( $user ) {
            $user_objects[] = $user;
        }
    }
    return $user_objects;
}

/**
 * Check if user is protected.
 *
 * @param int $user_id User ID.
 * @return bool
 */
function pt_wuclean_is_user_protected(  $user_id  ) {
    return get_user_meta( $user_id, PT_WUCLEAN_USER_PROTECT_META, true ) === '1';
}

/**
 * Log user action.
 *
 * @param int           $user_id User ID.
 * @param string        $action  Action name.
 * @param \WP_User|null $user  User object or null.
 */
function pt_wuclean_log_user_action(  $user_id, $action, $user = null  ) {
    $logs = get_option( 'pt_wuclean_user_logs', array() );
    $settings = get_option( 'pt_wuclean_settings', array() );
    $log_limit = ( isset( $settings['user_log_limit'] ) ? max( 0, min( intval( $settings['user_log_limit'] ), pt_wuclean_Plugin::DEFAULT_USER_LOG_LIMIT ) ) : pt_wuclean_Plugin::DEFAULT_USER_LOG_LIMIT );
    // Use provided user object or fetch if not provided.
    if ( !$user ) {
        $user = get_userdata( $user_id );
    }
    $now = current_time( 'mysql' );
    $log_entry = array(
        'user_id'    => $user_id,
        'username'   => '',
        'name'       => '',
        'email'      => '',
        'registered' => '',
        'last_order' => '',
        'orders'     => 0,
        'spent'      => 0,
        'state'      => $action,
        'timestamp'  => $now,
    );
    if ( $user ) {
        $log_entry['username'] = $user->user_login;
        $log_entry['name'] = trim( $user->first_name . ' ' . $user->last_name );
        $log_entry['email'] = $user->user_email;
        $log_entry['registered'] = $user->user_registered;
        // Get order statistics.
        $order_stats = pt_wuclean_get_user_order_stats( $user_id );
        $log_entry['last_order'] = $order_stats['last_order'];
        $log_entry['orders'] = $order_stats['orders'];
        $log_entry['spent'] = $order_stats['spent'];
    }
    $logs[] = $log_entry;
    // Prune logs to limit.
    if ( count( $logs ) > $log_limit ) {
        $logs = array_slice( $logs, -$log_limit );
    }
    update_option( 'pt_wuclean_user_logs', $logs, false );
}

/**
 * Add user protect section to user profile.
 *
 * @param \WP_User $user User object.
 */
function pt_wuclean_add_user_protect_section(  $user  ) {
    if ( !current_user_can( pt_wuclean_get_required_capability() ) ) {
        return;
    }
    $protect_user = get_user_meta( $user->ID, PT_WUCLEAN_USER_PROTECT_META, true );
    $protect_orders = get_user_meta( $user->ID, PT_WUCLEAN_USER_PROTECT_ORDERS_META, true );
    echo '<h3>' . esc_html__( 'Wuclean Protection', 'wuclean-database-cleaner-for-woocommerce' ) . '</h3>';
    echo '<table class="form-table">';
    echo '<tr><th scope="row">' . esc_html__( 'Protect user from cleanup', 'wuclean-database-cleaner-for-woocommerce' ) . '</th><td>';
    echo '<input type="checkbox" name="pt_wuclean_protect_user" value="1"' . checked( $protect_user, '1', false ) . '>';
    echo '<p class="description">' . esc_html__( 'If checked, this user will be protected from user cleanup routines.', 'wuclean-database-cleaner-for-woocommerce' ) . '</p>';
    echo '</td></tr>';
    echo '<tr><th scope="row">' . esc_html__( 'Protect all orders', 'wuclean-database-cleaner-for-woocommerce' ) . '</th><td>';
    echo '<input type="checkbox" name="pt_wuclean_protect_orders" value="1"' . checked( $protect_orders, '1', false ) . '>';
    echo '<p class="description">' . esc_html__( 'If checked, ALL orders for this user will be protected from order cleanup routines. This is different from protecting individual orders.', 'wuclean-database-cleaner-for-woocommerce' ) . '</p>';
    echo '</td></tr>';
    // Show count of individually protected orders (if user-level protection is not enabled).
    if ( !$protect_orders ) {
        $protected_orders_count = pt_wuclean_count_user_protected_orders( $user->ID );
        if ( 0 < $protected_orders_count ) {
            echo '<tr><th scope="row">' . esc_html__( 'Individually protected orders', 'wuclean-database-cleaner-for-woocommerce' ) . '</th><td>';
            echo '<a href="' . esc_url( add_query_arg( array(
                'page'           => 'wuclean-orders',
                'tab'            => 'flagged',
                'show_protected' => '1',
                'user_id'        => $user->ID,
                '_wpnonce'       => wp_create_nonce( 'wuclean-orders-tab' ),
            ), admin_url( 'admin.php' ) ) ) . '" target="_blank" rel="noopener noreferrer">';
            /* translators: %d: number of orders. */
            echo esc_html( sprintf( esc_html__( '%d orders', 'wuclean-database-cleaner-for-woocommerce' ), $protected_orders_count ) );
            echo '</a>';
            echo '<p class="description">' . esc_html__( 'Click the link above to view and manage these protected orders.', 'wuclean-database-cleaner-for-woocommerce' ) . '</p>';
            echo '</td></tr>';
        }
    }
    echo '</table>';
}

/**
 * Save user protect section.
 *
 * @param int $user_id User ID.
 */
function pt_wuclean_save_user_protect_section(  $user_id  ) {
    if ( !current_user_can( pt_wuclean_get_required_capability() ) ) {
        return;
    }
    // Verify nonce - WordPress core hooks verify this, but PHPCS needs explicit check.
    // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- WordPress core handles this
    if ( !isset( $_POST['_wpnonce'] ) || !wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ), 'update-user_' . $user_id ) ) {
        return;
    }
    $user = get_user_by( 'id', $user_id );
    if ( !$user ) {
        return;
    }
    // Handle user protection checkbox.
    if ( isset( $_POST['pt_wuclean_protect_user'] ) ) {
        // User is being protected.
        if ( !pt_wuclean_is_user_protected( $user_id ) ) {
            update_user_meta( $user_id, PT_WUCLEAN_USER_PROTECT_META, '1' );
            // Log the protection action.
            pt_wuclean_log_user_action( $user_id, 'Protected', $user );
        }
    } elseif ( pt_wuclean_is_user_protected( $user_id ) ) {
        // User protection is being removed.
        delete_user_meta( $user_id, PT_WUCLEAN_USER_PROTECT_META );
        // Log the unprotection action.
        pt_wuclean_log_user_action( $user_id, 'Protection Removed', $user );
    }
    // Handle order protection checkbox.
    if ( isset( $_POST['pt_wuclean_protect_orders'] ) ) {
        update_user_meta( $user_id, PT_WUCLEAN_USER_PROTECT_ORDERS_META, '1' );
    } else {
        // Checkbox is unchecked; remove the meta entirely.
        delete_user_meta( $user_id, PT_WUCLEAN_USER_PROTECT_ORDERS_META );
    }
}

/**
 * Count how many orders for a specific user are individually protected.
 *
 * @param int $user_id User ID.
 * @return int Number of individually protected orders.
 */
function pt_wuclean_count_user_protected_orders(  $user_id  ) {
    global $wpdb;
    if ( pt_wuclean_is_hpos_enabled() ) {
        // Use direct SQL query for HPOS - much faster than looping through all orders.
        $orders_meta_table = $wpdb->prefix . 'wc_orders_meta';
        $orders_table = $wpdb->prefix . 'wc_orders';
        $orders_meta_table_escaped = esc_sql( $orders_meta_table );
        $orders_table_escaped = esc_sql( $orders_table );
        // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table names from $wpdb->prefix, escaped with esc_sql(); cannot be placeholders.
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- HPOS table lookup.
        $protected_count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(DISTINCT om.order_id)\n            FROM {$orders_meta_table_escaped} om\n            INNER JOIN {$orders_table_escaped} o ON om.order_id = o.id\n            WHERE o.customer_id = %d\n            AND om.meta_key = '_pt_wuclean_protected'\n            AND om.meta_value = '1'", $user_id ) );
        // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        return intval( $protected_count );
    } else {
        // Use legacy post query.
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $protected_count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->postmeta} pm \n             INNER JOIN {$wpdb->posts} p ON pm.post_id = p.ID \n             WHERE p.post_type = 'shop_order' \n             AND pm.meta_key = '_customer_user' \n             AND pm.meta_value = %d \n             AND EXISTS (\n                 SELECT 1 FROM {$wpdb->postmeta} pm2 \n                 WHERE pm2.post_id = pm.post_id \n                 AND pm2.meta_key = '_pt_wuclean_protected' \n                 AND pm2.meta_value = '1'\n             )", $user_id ) );
        return intval( $protected_count );
    }
}

/**
 * Determine the reason why a user was flagged for cleanup
 *
 * @param int $user_id User ID.
 * @param int $delete_new_no_order_days Days threshold for new users without orders.
 * @param int $delete_inactive_days Days threshold for inactive users.
 * @return string|false Flag reason or false if no reason found.
 */
function pt_wuclean_determine_user_flag_reason(  $user_id, $delete_new_no_order_days, $delete_inactive_days  ) {
    $user = get_userdata( $user_id );
    if ( !$user ) {
        return false;
    }
    $now = time();
    $registration_date = strtotime( $user->user_registered );
    $user_age_days = ($now - $registration_date) / (24 * 3600);
    // Check inactive users first (this should take priority).
    if ( $delete_inactive_days > 0 ) {
        $last_order_date = pt_wuclean_get_user_last_order_date( $user_id );
        if ( $last_order_date ) {
            $days_since_last_order = ($now - $last_order_date) / (24 * 3600);
            if ( $delete_inactive_days <= $days_since_last_order ) {
                return sprintf( 'Inactive user no order for %.0f days', $days_since_last_order );
            }
        } elseif ( $delete_inactive_days <= $user_age_days ) {
            // User has no orders at all, check if they're old enough.
            return sprintf( 'New user no order for %.0f days', $user_age_days );
        }
    }
    // Check new users without orders (only if not already flagged as inactive).
    // Use persistent flag to avoid flagging users whose orders were deleted.
    if ( $delete_new_no_order_days > 0 && $user_age_days >= $delete_new_no_order_days ) {
        $ever_purchased = pt_wuclean_check_and_set_purchase_flag( $user_id );
        if ( !$ever_purchased ) {
            return sprintf( 'New user no order for %.0f days', $user_age_days );
        }
    }
    return false;
}

/**
 * Send a single customer email for a specific flagged user.
 *
 * Free build uses this for the per-row "Send" action in the flagged users table.
 *
 * @param int    $user_id             User ID.
 * @param string $flag_reason         Flag reason text.
 * @param int    $days_until_deletion Days until auto-deletion.
 * @return bool True when an email was triggered.
 */
function pt_wuclean_send_customer_email_for_user(  $user_id, $flag_reason, $days_until_deletion  ) {
    if ( empty( $user_id ) || !function_exists( 'WC' ) ) {
        return false;
    }
    $wc = WC();
    if ( !$wc ) {
        return false;
    }
    // Load WC email stack so WC_Email exists before requiring child classes (admin-ajax is lazy vs full admin).
    $wc->mailer();
    // Child email classes extend WC_Email; requiring their files before WooCommerce loads causes a fatal.
    if ( !class_exists( 'WC_Email' ) ) {
        return false;
    }
    $flag_reason = strtolower( (string) $flag_reason );
    if ( false !== strpos( $flag_reason, 'inactive' ) ) {
        if ( !class_exists( 'Wuclean_WC_Account_Inactivity_Email' ) ) {
            require_once plugin_dir_path( __FILE__ ) . 'emails/class-wuclean-wc-account-inactivity-email.php';
        }
        $mailer = WC()->mailer();
        if ( !$mailer ) {
            return false;
        }
        $emails = $mailer->get_emails();
        if ( !isset( $emails['Wuclean_WC_Account_Inactivity_Email'] ) ) {
            $emails['Wuclean_WC_Account_Inactivity_Email'] = new Wuclean_WC_Account_Inactivity_Email();
        }
        $emails['Wuclean_WC_Account_Inactivity_Email']->trigger( (int) $user_id, $days_until_deletion );
        update_user_meta( $user_id, '_pt_wuclean_inactivity_email_sent', time() );
        return true;
    }
    if ( false !== strpos( $flag_reason, 'new' ) ) {
        if ( !class_exists( 'Wuclean_WC_Account_New_Customer_Email' ) ) {
            require_once plugin_dir_path( __FILE__ ) . 'emails/class-wuclean-wc-account-new-customer-email.php';
        }
        $mailer = WC()->mailer();
        if ( !$mailer ) {
            return false;
        }
        $emails = $mailer->get_emails();
        if ( !isset( $emails['Wuclean_WC_Account_New_Customer_Email'] ) ) {
            $emails['Wuclean_WC_Account_New_Customer_Email'] = new Wuclean_WC_Account_New_Customer_Email();
        }
        $emails['Wuclean_WC_Account_New_Customer_Email']->trigger( (int) $user_id, $days_until_deletion );
        update_user_meta( $user_id, '_pt_wuclean_new_customer_email_sent', time() );
        return true;
    }
    return false;
}

/**
 * Register custom WooCommerce emails (so templates are editable in WooCommerce > Settings > Emails).
 */
add_filter( 'woocommerce_email_classes', 'pt_wuclean_add_account_emails' );
/**
 * Add Wuclean account email classes to WooCommerce.
 *
 * @param array $email_classes Existing email classes.
 * @return array
 */
function pt_wuclean_add_account_emails(  $email_classes  ) {
    require_once plugin_dir_path( __FILE__ ) . 'emails/class-wuclean-wc-account-inactivity-email.php';
    require_once plugin_dir_path( __FILE__ ) . 'emails/class-wuclean-wc-account-new-customer-email.php';
    $email_classes['Wuclean_WC_Account_Inactivity_Email'] = new Wuclean_WC_Account_Inactivity_Email();
    $email_classes['Wuclean_WC_Account_New_Customer_Email'] = new Wuclean_WC_Account_New_Customer_Email();
    return $email_classes;
}

add_action(
    'woocommerce_new_order',
    'pt_wuclean_clear_inactivity_flag_on_order',
    10,
    1
);
/**
 * Auto-clear inactivity flag when customer places a new order.
 *
 * @param int $order_id Order ID.
 */
function pt_wuclean_clear_inactivity_flag_on_order(  $order_id  ) {
    $order = wc_get_order( $order_id );
    if ( !$order ) {
        return;
    }
    $user_id = $order->get_user_id();
    if ( $user_id ) {
        // Clear the flagged status if customer places an order.
        $was_flagged = get_user_meta( $user_id, '_pt_wuclean_flagged_at', true );
        if ( $was_flagged ) {
            delete_user_meta( $user_id, '_pt_wuclean_flagged_at' );
            delete_user_meta( $user_id, '_pt_wuclean_flag_reason' );
            delete_user_meta( $user_id, '_pt_wuclean_inactivity_email_sent' );
            delete_user_meta( $user_id, '_pt_wuclean_new_customer_email_sent' );
            // Log the unflagging.
            $logs = get_option( 'pt_wuclean_user_logs', array() );
            $settings = get_option( 'pt_wuclean_settings', array() );
            $log_limit = ( isset( $settings['user_log_limit'] ) ? max( 0, min( intval( $settings['user_log_limit'] ), pt_wuclean_Plugin::DEFAULT_USER_LOG_LIMIT ) ) : pt_wuclean_Plugin::DEFAULT_USER_LOG_LIMIT );
            if ( $log_limit > 0 ) {
                $logs[] = array(
                    'user_id'   => $user_id,
                    'username'  => $order->get_billing_first_name() . ' ' . $order->get_billing_last_name(),
                    'email'     => $order->get_billing_email(),
                    'action'    => 'Auto-unflagged (new order)',
                    'timestamp' => current_time( 'mysql' ),
                );
                // Prune logs to limit.
                if ( count( $logs ) > $log_limit ) {
                    $logs = array_slice( $logs, -$log_limit );
                }
                update_option( 'pt_wuclean_user_logs', $logs, false );
            }
        }
    }
}

/**
 * Helper function to check user permissions for AJAX calls in users module
 */
function pt_wuclean_users_check_ajax_permissions() {
    if ( !current_user_can( pt_wuclean_get_required_capability() ) ) {
        wp_send_json_error( array(
            'message' => __( 'Insufficient permissions', 'wuclean-database-cleaner-for-woocommerce' ),
        ) );
        exit;
    }
}

/**
 * AJAX handler to start user scan
 */
function pt_wuclean_ajax_start_user_scan() {
    // Clean output buffer to prevent any stray output from corrupting JSON response.
    if ( ob_get_level() ) {
        ob_clean();
    }
    // Check permissions.
    pt_wuclean_users_check_ajax_permissions();
    // Check if nonce is provided.
    if ( !isset( $_POST['nonce'] ) ) {
        wp_send_json_error( array(
            'message' => __( 'Missing nonce parameter', 'wuclean-database-cleaner-for-woocommerce' ),
        ) );
        return;
    }
    if ( !isset( $_POST['nonce'] ) || !wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'pt_wuclean_user_scan' ) ) {
        wp_send_json_error( array(
            'message' => __( 'Invalid nonce', 'wuclean-database-cleaner-for-woocommerce' ),
        ) );
        return;
    }
    // Force fresh settings from database (bypass ALL caching to prevent stale data).
    global $wpdb;
    wp_cache_delete( 'pt_wuclean_settings', 'options' );
    // Get settings directly from database, bypassing all cache layers.
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    $settings_raw = $wpdb->get_var( $wpdb->prepare( "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s LIMIT 1", 'pt_wuclean_settings' ) );
    $settings = ( $settings_raw ? maybe_unserialize( $settings_raw ) : array() );
    $target_roles = $settings['user_target_roles'] ?? array('customer');
    // Exclude administrators and shop managers.
    $excluded_roles = array('administrator', 'shop_manager');
    $target_roles = array_diff( $target_roles, $excluded_roles );
    // If no valid roles, default to customer.
    if ( empty( $target_roles ) ) {
        $target_roles = array('customer');
    }
    // Get total user count for the target roles.
    $args = array(
        'role__in' => $target_roles,
        'fields'   => 'ID',
        'number'   => -1,
    );
    try {
        $user_query = new WP_User_Query($args);
        $total_users = $user_query->get_total();
    } catch ( Exception $e ) {
        /* translators: %s: error message */
        $error_msg = sprintf( __( 'Error counting users: %s', 'wuclean-database-cleaner-for-woocommerce' ), $e->getMessage() );
        wp_send_json_error( array(
            'message' => $error_msg,
        ) );
        return;
    }
    // Initialize progress tracking.
    $progress_data = array(
        'status'        => 'running',
        'total_items'   => $total_users,
        'processed'     => 0,
        'flagged_count' => 0,
        'emails_sent'   => 0,
        'offset'        => 0,
        'is_manual'     => true,
    );
    update_option( 'pt_wuclean_user_scan_progress', $progress_data, false );
    wp_send_json_success( array(
        'message'  => __( 'User scan started', 'wuclean-database-cleaner-for-woocommerce' ),
        'progress' => $progress_data,
    ) );
}

/**
 * AJAX handler to process user scan batch
 */
function pt_wuclean_ajax_process_user_batch() {
    // Clean output buffer to prevent any stray output from corrupting JSON response.
    if ( ob_get_level() ) {
        ob_clean();
    }
    // Check permissions.
    pt_wuclean_users_check_ajax_permissions();
    check_ajax_referer( 'pt_wuclean_user_scan', 'nonce' );
    // Get current progress.
    $progress_data = get_option( 'pt_wuclean_user_scan_progress', array() );
    if ( empty( $progress_data ) ) {
        wp_send_json_error( array(
            'message' => __( 'No user scan in progress', 'wuclean-database-cleaner-for-woocommerce' ),
        ) );
        return;
    }
    // Check if stopped.
    if ( 'stopped' === $progress_data['status'] ) {
        delete_option( 'pt_wuclean_user_scan_progress' );
        wp_send_json_success( array(
            'message'   => __( 'Scan stopped', 'wuclean-database-cleaner-for-woocommerce' ),
            'completed' => true,
            'progress'  => $progress_data,
        ) );
        return;
    }
    // Force fresh settings from database (bypass ALL caching to prevent stale data).
    global $wpdb;
    wp_cache_delete( 'pt_wuclean_settings', 'options' );
    // Get settings directly from database, bypassing all cache layers.
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    $settings_raw = $wpdb->get_var( $wpdb->prepare( "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s LIMIT 1", 'pt_wuclean_settings' ) );
    $settings = ( $settings_raw ? maybe_unserialize( $settings_raw ) : array() );
    $batch_size = intval( $settings['user_cleanup_batch_size'] ?? 10 );
    $is_manual = $progress_data['is_manual'] ?? false;
    // Email notifications are disabled for manual scans.
    $admin_email_enabled = ( !$is_manual ? $settings['user_email_notification'] ?? 0 : 0 );
    $new_customer_email_enabled = ( !$is_manual ? $settings['user_new_customer_email_notification'] ?? 0 : 0 );
    $inactive_customer_email_enabled = ( !$is_manual ? $settings['user_customer_email_notification'] ?? 0 : 0 );
    $customer_email_enabled = $new_customer_email_enabled || $inactive_customer_email_enabled;
    // Process one batch.
    $offset = $progress_data['offset'] ?? 0;
    $target_roles = $settings['user_target_roles'] ?? array('customer');
    $excluded_roles = array('administrator', 'shop_manager');
    $target_roles = array_diff( $target_roles, $excluded_roles );
    $delete_new_no_order_days = intval( $settings['user_delete_new_no_order_days'] ?? 0 );
    $delete_inactive_days = intval( $settings['user_delete_inactive_days'] ?? 0 );
    // Get users to check.
    $result = pt_wuclean_find_users_to_flag(
        $batch_size,
        $offset,
        $target_roles,
        $delete_new_no_order_days,
        $delete_inactive_days
    );
    $users_to_check = $result['users'] ?? array();
    $users_checked = $result['checked'] ?? 0;
    $flagged_in_batch = 0;
    $now = time();
    foreach ( $users_to_check as $user_id ) {
        // Skip if protected or already flagged.
        if ( pt_wuclean_is_user_protected( $user_id ) ) {
            continue;
        }
        if ( get_user_meta( $user_id, '_pt_wuclean_flagged_at', true ) ) {
            continue;
        }
        // Flag the user.
        update_user_meta( $user_id, '_pt_wuclean_flagged_at', $now );
        // Store flag reason.
        $flag_reason = pt_wuclean_determine_user_flag_reason( $user_id, $delete_new_no_order_days, $delete_inactive_days );
        if ( $flag_reason ) {
            update_user_meta( $user_id, '_pt_wuclean_flag_reason', $flag_reason );
        }
        // Log the action.
        $user = get_userdata( $user_id );
        if ( $user ) {
            pt_wuclean_log_user_action( $user_id, 'Flagged for cleanup', $user );
        }
        ++$flagged_in_batch;
        // Send customer email if enabled (only for scheduled scans, not manual).
        if ( $customer_email_enabled ) {
        }
    }
    // Update progress.
    // Use actual number of users checked, not just the batch size.
    $progress_data['processed'] += $users_checked;
    $progress_data['flagged_count'] += $flagged_in_batch;
    $progress_data['offset'] = $offset + $users_checked;
    // Calculate emails sent (both types are handled by the function).
    if ( $customer_email_enabled ) {
        $progress_data['emails_sent'] += $flagged_in_batch;
    }
    // Check if complete.
    // Complete if we've processed all users OR if no users were checked (reached end).
    $is_complete = $progress_data['processed'] >= $progress_data['total_items'] || 0 === $users_checked && empty( $users_to_check );
    if ( $is_complete ) {
        $progress_data['status'] = 'completed';
        // Include stored flagged users count for redirect logic (may include users from previous scans).
        $progress_data['stored_flagged_count'] = pt_wuclean_count_flagged_users();
        update_option( 'pt_wuclean_user_scan_progress', $progress_data, false );
        $completion_message = sprintf( 
            /* translators: 1: users checked, 2: users flagged. */
            __( 'Scan completed! Checked %1$d users, flagged %2$d.', 'wuclean-database-cleaner-for-woocommerce' ),
            $progress_data['processed'],
            $progress_data['flagged_count']
         );
        wp_send_json_success( array(
            'message'   => $completion_message,
            'completed' => true,
            'progress'  => $progress_data,
        ) );
    } else {
        update_option( 'pt_wuclean_user_scan_progress', $progress_data, false );
        // Create detailed progress message.
        $processed = $progress_data['processed'];
        $percentage = ( $progress_data['total_items'] > 0 ? round( $processed / $progress_data['total_items'] * 100 ) : 0 );
        $batch_message = sprintf(
            /* translators: 1: percentage, 2: processed count, 3: total users, 4: newly flagged. */
            __( 'Processing... %1$d%% (%2$d/%3$d users checked). New flags: %4$d', 'wuclean-database-cleaner-for-woocommerce' ),
            $percentage,
            $processed,
            $progress_data['total_items'],
            $progress_data['flagged_count']
        );
        wp_send_json_success( array(
            'message'   => $batch_message,
            'completed' => false,
            'progress'  => $progress_data,
        ) );
    }
}

/**
 * AJAX handler to stop user scan
 */
function pt_wuclean_ajax_stop_user_scan() {
    // Clean output buffer to prevent any stray output from corrupting JSON response.
    if ( ob_get_level() ) {
        ob_clean();
    }
    // Check permissions.
    pt_wuclean_users_check_ajax_permissions();
    check_ajax_referer( 'pt_wuclean_user_scan', 'nonce' );
    // Update progress to stopped.
    $progress_data = get_option( 'pt_wuclean_user_scan_progress', array() );
    if ( !empty( $progress_data ) ) {
        $progress_data['status'] = 'stopped';
        update_option( 'pt_wuclean_user_scan_progress', $progress_data, false );
    }
    wp_send_json_success( array(
        'message'  => 'User scan stopped',
        'progress' => $progress_data,
    ) );
}

/**
 * Render orphaned customer data tab
 */
function pt_wuclean_render_orphaned_customer_data_tab() {
    if ( !current_user_can( pt_wuclean_get_required_capability() ) ) {
        wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'wuclean-database-cleaner-for-woocommerce' ) );
    }
    // Get cached orphaned data.
    $orphaned_data = pt_wuclean_get_orphaned_customer_data();
    $last_scan = get_option( 'pt_wuclean_orphaned_customer_scan_time', '' );
    echo '<div style="max-width: 1600px; margin-top: 2em;">';
    echo '<h2>🗑️ ' . esc_html__( 'Orphaned Customer Data', 'wuclean-database-cleaner-for-woocommerce' ) . '</h2>';
    // Info box.
    echo '<div style="background: #fff3cd; border: 1px solid #ffc107; border-radius: 4px; padding: 15px; margin: 20px 0;">';
    if ( pt_wuclean_fs()->is_not_paying() ) {
        // Free version: Show basic feature description.
        echo '<p style="margin: 0;"><strong>ℹ️ ' . esc_html__( 'About Orphaned Customer Data:', 'wuclean-database-cleaner-for-woocommerce' ) . '</strong> ' . esc_html__( 'This tab shows database records from improperly deleted customers which should be safe to remove from the database using SQL and your favorite database manager, but remember to make backups and reassign orphaned comments to “guest” to preserve social proofs. Automate this with Wuclean Pro👑', 'wuclean-database-cleaner-for-woocommerce' ) . '</p>';
    }
    echo '</p></div>';
    // Empty state.
    if ( empty( $orphaned_data ) || empty( $orphaned_data['items'] ) ) {
        echo '<div style="background: #fff; border: 1px solid #ccd0d4; border-radius: 4px; padding: 20px; margin: 20px 0; text-align: center;">';
        echo '<p style="margin: 0; color: #646970;">' . esc_html__( 'No orphaned customer data found.', 'wuclean-database-cleaner-for-woocommerce' ) . '</p>';
        echo '<p style="margin: 10px 0 0 0; color: #646970; font-size: 13px;">' . esc_html__( 'Enable "Detect orphaned customer data" in Settings to scan for orphaned customer data.', 'wuclean-database-cleaner-for-woocommerce' ) . '</p>';
        echo '</div>';
        echo '</div>';
        return;
    }
    if ( $orphaned_data && !empty( $orphaned_data['items'] ) ) {
        $items = $orphaned_data['items'];
        // Validate tab nonce before trusting any GET-based filters for this view.
        $users_tab_nonce_valid = isset( $_GET['_wpnonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'wuclean-users-tab' );
        // Search handling.
        $search_query = '';
        if ( $users_tab_nonce_valid && isset( $_GET['orphaned_search'] ) ) {
            $search_query = sanitize_text_field( wp_unslash( $_GET['orphaned_search'] ) );
        }
        $filtered_items = $items;
        if ( !empty( $search_query ) ) {
            $filtered_items = array_filter( $items, function ( $item ) use($search_query) {
                $searchable_text = implode( ' ', array($item['type'] ?? '', $item['user_id'] ?? '', $item['details'] ?? '') );
                return stripos( $searchable_text, $search_query ) !== false;
            } );
        }
        // Search form.
        echo '<form method="get" action="' . esc_url( admin_url( 'admin.php' ) ) . '" style="margin-bottom: 20px;">';
        echo '<input type="hidden" name="page" value="wuclean-users">';
        echo '<input type="hidden" name="tab" value="orphaned">';
        echo '<input type="hidden" name="_wpnonce" value="' . esc_attr( wp_create_nonce( 'wuclean-users-tab' ) ) . '">';
        $search_placeholder = esc_attr__( 'Search by User ID, Name, Email, or Type', 'wuclean-database-cleaner-for-woocommerce' );
        echo '<input type="text" name="orphaned_search" value="' . esc_attr( $search_query ) . '" placeholder="' . esc_attr( $search_placeholder ) . '" style="width: 400px; padding: 5px; height: 30px; box-sizing: border-box;">';
        echo '<input type="submit" value="' . esc_attr__( 'Search', 'wuclean-database-cleaner-for-woocommerce' ) . '" class="button">';
        if ( !empty( $search_query ) ) {
            $clear_url = add_query_arg( array(
                'page'     => 'wuclean-users',
                'tab'      => 'orphaned',
                '_wpnonce' => wp_create_nonce( 'wuclean-users-tab' ),
            ), admin_url( 'admin.php' ) );
            echo '<a href="' . esc_url( $clear_url ) . '" class="button">' . esc_html__( 'Clear Search', 'wuclean-database-cleaner-for-woocommerce' ) . '</a>';
        }
        echo '</form>';
        // Pagination.
        $per_page = 25;
        $page = 1;
        if ( $users_tab_nonce_valid && isset( $_GET['orphaned_page'] ) ) {
            $page = max( 1, intval( $_GET['orphaned_page'] ) );
        }
        $total_items = count( $filtered_items );
        $total_pages = max( 1, ceil( $total_items / $per_page ) );
        $paged_items = array_slice( $filtered_items, ($page - 1) * $per_page, $per_page );
        echo '<form method="post">';
        wp_nonce_field( 'pt_wuclean_orphaned_data', 'pt_wuclean_orphaned_nonce' );
        // Bulk action buttons.
        echo '<div style="margin-bottom: 20px;">';
        echo '<div style="display: flex; gap: 10px; align-items: center; flex-wrap: wrap;">';
        echo '</div>';
        echo '</div>';
        // Pagination info (before table).
        if ( $total_items > 0 ) {
            echo '<div style="margin-bottom: 10px;">';
            $start = ($page - 1) * $per_page + 1;
            $end = min( $page * $per_page, $total_items );
            if ( !empty( $search_query ) ) {
                /* translators: %1$d: start number, %2$d: end number, %3$d: total count. */
                $filtered_fmt = __( 'Showing %1$d-%2$d of %3$d items (filtered by search)', 'wuclean-database-cleaner-for-woocommerce' );
                $filtered_message = sprintf(
                    $filtered_fmt,
                    $start,
                    $end,
                    $total_items
                );
                echo '<span>' . esc_html( $filtered_message ) . '</span>';
            } else {
                /* translators: %1$d: start number, %2$d: end number, %3$d: total count. */
                $items_fmt = __( 'Showing %1$d-%2$d of %3$d items', 'wuclean-database-cleaner-for-woocommerce' );
                $items_message = sprintf(
                    $items_fmt,
                    $start,
                    $end,
                    $total_items
                );
                echo '<span>' . esc_html( $items_message ) . '</span>';
            }
            echo '</div>';
        }
        // Table.
        echo '<table class="wp-list-table widefat fixed striped" style="margin-top: 20px;">';
        echo '<thead><tr>';
        echo '<th style="width: 220px; text-align: left;">' . esc_html__( 'Type', 'wuclean-database-cleaner-for-woocommerce' ) . '</th>';
        echo '<th style="width: 80px; text-align: left;">' . esc_html__( 'User ID', 'wuclean-database-cleaner-for-woocommerce' ) . '</th>';
        echo '<th style="text-align: left;">' . esc_html__( 'Details', 'wuclean-database-cleaner-for-woocommerce' ) . '</th>';
        echo '</tr></thead><tbody>';
        if ( !empty( $paged_items ) ) {
            foreach ( $paged_items as $item ) {
                echo '<tr>';
                echo '<td style="text-align: left;">' . esc_html( $item['type'] ) . '</td>';
                echo '<td style="text-align: left;">' . esc_html( $item['user_id'] ) . '</td>';
                echo '<td style="text-align: left;">' . esc_html( $item['details'] ) . '</td>';
                echo '</tr>';
            }
        } else {
            $colspan = 3;
            echo '<tr><td colspan="' . esc_attr( $colspan ) . '" style="text-align: center;">' . esc_html__( 'No orphaned data found.', 'wuclean-database-cleaner-for-woocommerce' ) . '</td></tr>';
        }
        echo '</tbody></table>';
        echo '</form>';
        // Pagination - using centralized function.
        $preserve_params = array(
            'page'     => 'wuclean-users',
            'tab'      => 'orphaned',
            '_wpnonce' => wp_create_nonce( 'wuclean-users-tab' ),
        );
        if ( !empty( $search_query ) ) {
            $preserve_params['orphaned_search'] = $search_query;
        }
        pt_wuclean_render_pagination( array(
            'current_page'    => $page,
            'total_pages'     => $total_pages,
            'base_url'        => admin_url( 'admin.php' ),
            'page_param'      => 'orphaned_page',
            'preserve_params' => $preserve_params,
        ) );
    }
    echo '</div>';
    // Enqueue scripts.
    wp_enqueue_script(
        'pt-wuclean-common',
        plugin_dir_url( __FILE__ ) . 'assets/js/common.js',
        array('jquery'),
        '1.0.0',
        true
    );
}

/**
 * AJAX handler to start orphaned customer data scan (batch processing)
 */
function pt_wuclean_ajax_start_orphaned_customer_scan() {
    // Clean output buffer.
    if ( ob_get_level() ) {
        ob_clean();
    }
    // Check permissions.
    pt_wuclean_users_check_ajax_permissions();
    // Check nonce.
    if ( !isset( $_POST['nonce'] ) || !wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'pt_wuclean_orphaned_customer_scan' ) ) {
        wp_send_json_error( array(
            'message' => __( 'Invalid nonce', 'wuclean-database-cleaner-for-woocommerce' ),
        ) );
        return;
    }
    global $wpdb;
    // Clear old progress data.
    delete_option( 'pt_wuclean_orphaned_customer_scan_progress' );
    wp_cache_delete( 'pt_wuclean_orphaned_customer_scan_progress', 'options' );
    // Get total counts for progress tracking.
    // Step 1: Count orphaned usermeta.
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    $total_orphaned_usermeta = $wpdb->get_var( "SELECT COUNT(DISTINCT um.user_id) \n        FROM {$wpdb->usermeta} um\n        LEFT JOIN {$wpdb->users} u ON um.user_id = u.ID\n        WHERE u.ID IS NULL" );
    // Step 2: Count orphaned customer lookup.
    $customer_lookup_table = $wpdb->prefix . 'wc_customer_lookup';
    $customer_lookup_table_escaped = esc_sql( $customer_lookup_table );
    $total_orphaned_lookup = 0;
    if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $customer_lookup_table ) ) === $customer_lookup_table ) {
        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from $wpdb->prefix, cannot use placeholders.
        $total_orphaned_lookup = $wpdb->get_var( "SELECT COUNT(*) \n            FROM {$customer_lookup_table_escaped} cl\n            LEFT JOIN {$wpdb->users} u ON cl.user_id = u.ID\n            WHERE cl.user_id > 0 AND u.ID IS NULL" );
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
    }
    $total_items = intval( $total_orphaned_usermeta ) + intval( $total_orphaned_lookup );
    // Initialize progress data.
    $progress_data = array(
        'status'                 => 'running',
        'current_step'           => 'usermeta',
        'total_items'            => $total_items,
        'processed'              => 0,
        'total_found'            => 0,
        'usermeta_offset'        => 0,
        'usermeta_total'         => intval( $total_orphaned_usermeta ),
        'customer_lookup_offset' => 0,
        'customer_lookup_total'  => intval( $total_orphaned_lookup ),
        'comments_fixed'         => 0,
        'start_time'             => time(),
        'last_activity'          => time(),
        'message'                => __( 'Initializing orphaned customer data search...', 'wuclean-database-cleaner-for-woocommerce' ),
        'items'                  => array(),
    );
    // Store progress data.
    update_option( 'pt_wuclean_orphaned_customer_scan_progress', $progress_data, false );
    wp_send_json_success( array(
        'message'  => __( 'Orphaned customer data search started successfully.', 'wuclean-database-cleaner-for-woocommerce' ),
        'progress' => $progress_data,
    ) );
}

/**
 * AJAX handler to process next orphaned customer data batch
 */
function pt_wuclean_ajax_process_orphaned_customer_batch() {
    // Clean output buffer to prevent any stray output from corrupting JSON response.
    if ( ob_get_level() ) {
        ob_clean();
    }
    // Check permissions.
    pt_wuclean_users_check_ajax_permissions();
    check_ajax_referer( 'pt_wuclean_orphaned_customer_scan', 'nonce' );
    global $wpdb;
    // Get current progress.
    $progress_data = get_option( 'pt_wuclean_orphaned_customer_scan_progress', array() );
    if ( empty( $progress_data ) ) {
        wp_send_json_error( array(
            'message' => __( 'No orphaned customer data search found.', 'wuclean-database-cleaner-for-woocommerce' ),
        ) );
        return;
    }
    // Check if search is already completed.
    if ( 'completed' === $progress_data['status'] ) {
        wp_send_json_success( array(
            'message'   => __( 'All batches completed.', 'wuclean-database-cleaner-for-woocommerce' ),
            'completed' => true,
            'progress'  => $progress_data,
        ) );
        return;
    }
    // Check if status is running.
    if ( 'running' !== ($progress_data['status'] ?? '') ) {
        wp_send_json_error( array(
            'message' => __( 'Orphaned customer data search is not running.', 'wuclean-database-cleaner-for-woocommerce' ),
        ) );
        return;
    }
    $settings = get_option( 'pt_wuclean_settings', array() );
    // Use user batch size, with fallback to product batch size, then default to 100 for orphaned data scans.
    $batch_size = $settings['user_cleanup_batch_size'] ?? $settings['product_cleanup_batch_size'] ?? 100;
    $current_step = $progress_data['current_step'] ?? 'usermeta';
    $results = array();
    if ( 'usermeta' === $current_step ) {
        // Process orphaned usermeta in batches.
        $offset = $progress_data['usermeta_offset'] ?? 0;
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $orphaned_usermeta = $wpdb->get_results( $wpdb->prepare( "SELECT user_id, COUNT(*) as count \n            FROM {$wpdb->usermeta} um\n            LEFT JOIN {$wpdb->users} u ON um.user_id = u.ID\n            WHERE u.ID IS NULL\n            GROUP BY user_id\n            LIMIT %d OFFSET %d", $batch_size, $offset ) );
        if ( !empty( $orphaned_usermeta ) ) {
            foreach ( $orphaned_usermeta as $row ) {
                $user_id = intval( $row->user_id );
                $meta_count = intval( $row->count );
                // Get identifying information from meta.
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                $billing_first = $wpdb->get_var( $wpdb->prepare( "SELECT meta_value FROM {$wpdb->usermeta} WHERE user_id = %d AND meta_key = 'billing_first_name' LIMIT 1", $user_id ) );
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                $billing_last = $wpdb->get_var( $wpdb->prepare( "SELECT meta_value FROM {$wpdb->usermeta} WHERE user_id = %d AND meta_key = 'billing_last_name' LIMIT 1", $user_id ) );
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                $billing_email = $wpdb->get_var( $wpdb->prepare( "SELECT meta_value FROM {$wpdb->usermeta} WHERE user_id = %d AND meta_key = 'billing_email' LIMIT 1", $user_id ) );
                // Get all meta keys for categorization.
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                $meta_keys = $wpdb->get_col( $wpdb->prepare( "SELECT meta_key FROM {$wpdb->usermeta} WHERE user_id = %d", $user_id ) );
                // Categorize meta keys.
                $billing_shipping = 0;
                $wc_stats = 0;
                $other = 0;
                foreach ( $meta_keys as $key ) {
                    if ( 0 === strpos( $key, 'billing_' ) || 0 === strpos( $key, 'shipping_' ) ) {
                        ++$billing_shipping;
                    } elseif ( in_array( $key, array(
                        '_order_count',
                        '_money_spent',
                        'paying_customer',
                        '_woocommerce_persistent_cart'
                    ), true ) ) {
                        ++$wc_stats;
                    } else {
                        ++$other;
                    }
                }
                // Build identifying info string.
                $name = trim( ($billing_first ?? '') . ' ' . ($billing_last ?? '') );
                $email = $billing_email ?? '';
                $identity = '';
                if ( $name && $email ) {
                    $identity = $name . ' (' . $email . ')';
                } elseif ( $email ) {
                    $identity = $email;
                } elseif ( $name ) {
                    $identity = $name;
                }
                // Build categorization string.
                $categories = sprintf(
                    '%d billing/shipping, %d WC stats, %d other',
                    $billing_shipping,
                    $wc_stats,
                    $other
                );
                // Build final details string.
                $details = sprintf( '%d meta rows', $meta_count );
                if ( $identity ) {
                    $details .= ' - ' . $identity;
                }
                $details .= ' - ' . $categories;
                $results[] = array(
                    'id'      => 'usermeta:' . $user_id,
                    'type'    => 'Orphaned Usermeta',
                    'user_id' => $user_id,
                    'details' => $details,
                );
                ++$progress_data['total_found'];
            }
            $progress_data['usermeta_offset'] = $offset + count( $orphaned_usermeta );
            $progress_data['processed'] += count( $orphaned_usermeta );
            // Store results incrementally.
            $existing_items = $progress_data['items'] ?? array();
            $progress_data['items'] = array_merge( $existing_items, $results );
            // Check if we've processed all usermeta.
            if ( count( $orphaned_usermeta ) < $batch_size || $progress_data['usermeta_total'] <= $progress_data['usermeta_offset'] ) {
                $progress_data['current_step'] = 'customer_lookup';
                $progress_data['message'] = __( 'Processing orphaned customer lookup data...', 'wuclean-database-cleaner-for-woocommerce' );
            } else {
                $progress_data['message'] = sprintf( 
                    /* translators: 1: processed count, 2: total count. */
                    __( 'Processing orphaned usermeta... %1$d/%2$d', 'wuclean-database-cleaner-for-woocommerce' ),
                    min( $progress_data['usermeta_offset'], $progress_data['usermeta_total'] ),
                    $progress_data['usermeta_total']
                 );
            }
        } else {
            // No more usermeta, move to next step.
            $progress_data['current_step'] = 'customer_lookup';
            $progress_data['message'] = __( 'Processing orphaned customer lookup data...', 'wuclean-database-cleaner-for-woocommerce' );
        }
    }
    if ( 'customer_lookup' === $current_step || 'customer_lookup' === $progress_data['current_step'] ) {
        // Process orphaned customer lookup in batches.
        $customer_lookup_table = $wpdb->prefix . 'wc_customer_lookup';
        $customer_lookup_table_escaped = esc_sql( $customer_lookup_table );
        if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $customer_lookup_table ) ) === $customer_lookup_table ) {
            $offset = $progress_data['customer_lookup_offset'] ?? 0;
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from $wpdb->prefix, cannot use placeholders.
            $orphaned_lookup = $wpdb->get_results( $wpdb->prepare( "SELECT cl.user_id, cl.username, cl.email\n                FROM {$customer_lookup_table_escaped} cl\n                LEFT JOIN {$wpdb->users} u ON cl.user_id = u.ID\n                WHERE cl.user_id > 0 AND u.ID IS NULL\n                LIMIT %d OFFSET %d", $batch_size, $offset ) );
            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
            if ( !empty( $orphaned_lookup ) ) {
                foreach ( $orphaned_lookup as $row ) {
                    $results[] = array(
                        'id'      => 'customer_lookup:' . $row->user_id,
                        'type'    => 'Orphaned Customer Analytics',
                        'user_id' => $row->user_id,
                        'details' => sprintf( '%s (%s)', $row->username, $row->email ),
                    );
                    ++$progress_data['total_found'];
                }
                $progress_data['customer_lookup_offset'] = $offset + count( $orphaned_lookup );
                $progress_data['processed'] += count( $orphaned_lookup );
                // Store results incrementally.
                $existing_items = $progress_data['items'] ?? array();
                $progress_data['items'] = array_merge( $existing_items, $results );
                // Check if we've processed all customer lookup.
                if ( count( $orphaned_lookup ) < $batch_size || $progress_data['customer_lookup_total'] <= $progress_data['customer_lookup_offset'] ) {
                    $progress_data['current_step'] = 'comments_fix';
                    $progress_data['message'] = __( 'Fixing orphaned comments...', 'wuclean-database-cleaner-for-woocommerce' );
                } else {
                    $progress_data['message'] = sprintf( 
                        /* translators: 1: processed count, 2: total count. */
                        __( 'Processing orphaned customer lookup... %1$d/%2$d', 'wuclean-database-cleaner-for-woocommerce' ),
                        min( $progress_data['customer_lookup_offset'], $progress_data['customer_lookup_total'] ),
                        $progress_data['customer_lookup_total']
                     );
                }
            } else {
                // No more customer lookup, move to next step.
                $progress_data['current_step'] = 'comments_fix';
                $progress_data['message'] = __( 'Fixing orphaned comments...', 'wuclean-database-cleaner-for-woocommerce' );
            }
        } else {
            // Table doesn't exist, skip to comments fix.
            $progress_data['current_step'] = 'comments_fix';
            $progress_data['message'] = __( 'Fixing orphaned comments...', 'wuclean-database-cleaner-for-woocommerce' );
        }
    }
    if ( 'comments_fix' === $current_step || 'comments_fix' === $progress_data['current_step'] ) {
        // Mark as completed.
        $progress_data['status'] = 'completed';
        $progress_data['current_step'] = 'complete';
        // Store final results.
        $scan_time = current_time( 'mysql' );
        $final_results = array(
            'items'                => $progress_data['items'] ?? array(),
            'stats'                => array(
                'usermeta_count'        => 0,
                'customer_lookup_count' => 0,
                'comments_reassigned'   => $progress_data['comments_fixed'] ?? 0,
            ),
            'auto_deleted_summary' => array(),
        );
        // Count stats.
        foreach ( $final_results['items'] as $item ) {
            if ( 'Orphaned Usermeta' === $item['type'] ) {
                ++$final_results['stats']['usermeta_count'];
            } elseif ( 'Orphaned Customer Analytics' === $item['type'] ) {
                ++$final_results['stats']['customer_lookup_count'];
            }
        }
        // Store results.
        if ( !empty( $final_results['items'] ) ) {
            pt_wuclean_store_orphaned_customer_data( $final_results );
        } else {
            pt_wuclean_delete_orphaned_customer_data();
        }
        update_option( 'pt_wuclean_orphaned_customer_scan_time', $scan_time, false );
        // Include stored data count for redirect logic (may include items from previous scans).
        $stored_data = pt_wuclean_get_orphaned_customer_data();
        $progress_data['stored_data_count'] = ( is_array( $stored_data ) && isset( $stored_data['items'] ) ? count( $stored_data['items'] ) : 0 );
        $progress_data['message'] = sprintf( 
            /* translators: %d: number of orphaned items found. */
            __( 'Search completed! Found %d orphaned item(s).', 'wuclean-database-cleaner-for-woocommerce' ),
            $progress_data['total_found']
         );
    }
    // Update progress.
    $progress_data['last_activity'] = time();
    update_option( 'pt_wuclean_orphaned_customer_scan_progress', $progress_data, false );
    // Check if completed.
    $is_completed = 'completed' === ($progress_data['status'] ?? '');
    wp_send_json_success( array(
        'message'   => ( $is_completed ? $progress_data['message'] : $progress_data['message'] ?? __( 'Processing...', 'wuclean-database-cleaner-for-woocommerce' ) ),
        'completed' => $is_completed,
        'progress'  => $progress_data,
    ) );
}

/**
 * AJAX handler to stop orphaned customer data scan
 */
function pt_wuclean_ajax_stop_orphaned_customer_scan() {
    // Check permissions.
    pt_wuclean_users_check_ajax_permissions();
    // Check nonce.
    if ( !isset( $_POST['nonce'] ) || !wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'pt_wuclean_orphaned_customer_scan' ) ) {
        wp_send_json_error( array(
            'message' => __( 'Invalid nonce', 'wuclean-database-cleaner-for-woocommerce' ),
        ) );
        return;
    }
    // Update progress to stopped.
    $progress_data = get_option( 'pt_wuclean_orphaned_customer_scan_progress', array() );
    if ( !empty( $progress_data ) ) {
        $progress_data['status'] = 'stopped';
        $progress_data['message'] = __( 'Scan stopped by user', 'wuclean-database-cleaner-for-woocommerce' );
        update_option( 'pt_wuclean_orphaned_customer_scan_progress', $progress_data, false );
    }
    wp_send_json_success( array(
        'message' => __( 'Scan stopped', 'wuclean-database-cleaner-for-woocommerce' ),
    ) );
}

/**
 * AJAX: clear user scan progress (after redirect / UI cleanup).
 */
function pt_wuclean_ajax_clear_user_scan_progress() {
    if ( ob_get_level() ) {
        ob_clean();
    }
    pt_wuclean_users_check_ajax_permissions();
    check_ajax_referer( 'pt_wuclean_user_scan', 'nonce' );
    delete_option( 'pt_wuclean_user_scan_progress' );
    wp_send_json_success( array(
        'message' => __( 'Progress cleared.', 'wuclean-database-cleaner-for-woocommerce' ),
    ) );
}

/**
 * AJAX: clear orphaned customer scan progress option.
 */
function pt_wuclean_ajax_clear_orphaned_customer_scan_progress() {
    pt_wuclean_users_check_ajax_permissions();
    check_ajax_referer( 'pt_wuclean_orphaned_customer_scan', 'nonce' );
    delete_option( 'pt_wuclean_orphaned_customer_scan_progress' );
    wp_send_json_success( array(
        'message' => __( 'Progress cleared.', 'wuclean-database-cleaner-for-woocommerce' ),
    ) );
}

// Register AJAX actions.
add_action( 'wp_ajax_pt_wuclean_start_user_search', 'pt_wuclean_ajax_start_user_scan' );
add_action( 'wp_ajax_pt_wuclean_process_user_batch', 'pt_wuclean_ajax_process_user_batch' );
add_action( 'wp_ajax_pt_wuclean_stop_user_search', 'pt_wuclean_ajax_stop_user_scan' );
add_action( 'wp_ajax_pt_wuclean_clear_user_scan_progress', 'pt_wuclean_ajax_clear_user_scan_progress' );
add_action( 'wp_ajax_pt_wuclean_start_orphaned_customer_search', 'pt_wuclean_ajax_start_orphaned_customer_scan' );
add_action( 'wp_ajax_pt_wuclean_process_orphaned_customer_batch', 'pt_wuclean_ajax_process_orphaned_customer_batch' );
add_action( 'wp_ajax_pt_wuclean_stop_orphaned_customer_search', 'pt_wuclean_ajax_stop_orphaned_customer_scan' );
add_action( 'wp_ajax_pt_wuclean_clear_orphaned_customer_scan_progress', 'pt_wuclean_ajax_clear_orphaned_customer_scan_progress' );