<?php

/**
 * WUClean - Image Health Scanner
 *
 * Unified image scanner that checks both Media Library and uploads folder
 * to provide a complete picture of unused, orphaned, and related image files.
 *
 * @package WUClean
 * @version 2.0.0
 */
if ( !defined( 'ABSPATH' ) ) {
    exit;
}
// Shared scan engine (manual + scheduled scan parity).
require_once plugin_dir_path( __FILE__ ) . 'includes/image-attachment-index.php';
require_once plugin_dir_path( __FILE__ ) . 'includes/image-scan-engine.php';
// ============================================================================
// CONSTANTS & GLOBALS.
// ============================================================================
// Define setting values if not already defined by main plugin.
if ( !defined( 'PT_WUCLEAN_ENABLED_SETTING_VALUE' ) ) {
    define( 'PT_WUCLEAN_ENABLED_SETTING_VALUE', 'enabled' );
}
if ( !defined( 'PT_WUCLEAN_DISABLED_SETTING_VALUE' ) ) {
    define( 'PT_WUCLEAN_DISABLED_SETTING_VALUE', 'disabled' );
}
// Initialize global variables if not set by main plugin.
global $pt_wuclean_nonces, $pt_wuclean_css_classes;
if ( !isset( $pt_wuclean_nonces ) || !is_array( $pt_wuclean_nonces ) ) {
    $pt_wuclean_nonces = array(
        'save_image_settings'  => 'pt_wuclean_save_image_settings',
        'image_settings_nonce' => 'pt_wuclean_image_settings_nonce',
    );
}
if ( !isset( $pt_wuclean_css_classes ) || !is_array( $pt_wuclean_css_classes ) ) {
    $pt_wuclean_css_classes = array(
        'form_table'   => 'form-table',
        'tooltip'      => 'pt-wuclean-tooltip',
        'tooltip_text' => 'pt-wuclean-tooltip-text',
    );
}
// ============================================================================
// TAB REGISTRATION.
// ============================================================================
/**
 * Register image scanner tabs
 */
function pt_wuclean_register_images_tabs() {
    return array(
        'settings'  => __( 'Settings', 'wuclean-database-cleaner-for-woocommerce' ),
        'results'   => __( 'Scan Results', 'wuclean-database-cleaner-for-woocommerce' ),
        'protected' => __( 'Protected', 'wuclean-database-cleaner-for-woocommerce' ),
        'trash'     => __( 'Trash', 'wuclean-database-cleaner-for-woocommerce' ),
    );
}

/**
 * Whether the latest completed image scan included uploads-folder scanning.
 *
 * @return bool
 */
function pt_wuclean_image_results_last_scan_included_uploads() {
    $latest_completed = pt_wuclean_image_scan_db_get_latest_run( 'completed' );
    if ( !is_array( $latest_completed ) || empty( $latest_completed['id'] ) ) {
        return false;
    }
    $run_settings = pt_wuclean_image_scan_db_decode_json_field( $latest_completed['settings_json'] ?? '' );
    $scan_scope = ( isset( $run_settings['scan_scope'] ) && is_string( $run_settings['scan_scope'] ) ? (string) $run_settings['scan_scope'] : 'media_only' );
    $scan_scope = pt_wuclean_image_scan_engine_normalize_scan_scope( $scan_scope );
    return 'media_and_uploads' === $scan_scope;
}

/**
 * Return localized strings for the image scanner UI.
 *
 * @return array<string, string> Script strings for JS.
 */
function pt_wuclean_get_images_script_strings() {
    /* translators: %1$d: number of issues found */
    $issues_found_template = __( 'Issues found: <strong>%1$d</strong>', 'wuclean-database-cleaner-for-woocommerce' );
    /* translators: %1$d: total issues detected */
    $total_issues_template = __( 'Found <strong>%1$d</strong> issues.', 'wuclean-database-cleaner-for-woocommerce' );
    /* translators: %d: number of verified images */
    $verified_images_single = __( 'Verified %d image in use.', 'wuclean-database-cleaner-for-woocommerce' );
    /* translators: %d: number of verified images */
    $verified_images_plural = __( 'Verified %d images in use.', 'wuclean-database-cleaner-for-woocommerce' );
    /* translators: %d: number of uploads-folder rows removed after re-check */
    $orphan_refinement_single = __( 'Removed %d uploads-folder result that was registered in the Media Library after a final check.', 'wuclean-database-cleaner-for-woocommerce' );
    /* translators: %d: number of uploads-folder rows removed after re-check */
    $orphan_refinement_plural = __( 'Removed %d uploads-folder results that were registered in the Media Library after a final check.', 'wuclean-database-cleaner-for-woocommerce' );
    /* translators: %d: number of selected images */
    $selection_single = __( '%d image selected', 'wuclean-database-cleaner-for-woocommerce' );
    /* translators: %d: number of selected images */
    $selection_plural = __( '%d images selected', 'wuclean-database-cleaner-for-woocommerce' );
    /* translators: 1: items checked so far, 2: total items to check during refinement. */
    $scan_finalize_waiting_count = __( 'Cross-checking flagged images to improve accuracy (%1$d of %2$d).', 'wuclean-database-cleaner-for-woocommerce' );
    /* translators: %1$d: current progress count, %2$d: total count */
    $restore_selected_progress = __( 'Restoring... %1$d/%2$d', 'wuclean-database-cleaner-for-woocommerce' );
    $strings = array(
        'scanning_label'                  => __( 'Scanning...', 'wuclean-database-cleaner-for-woocommerce' ),
        'error_label'                     => __( 'Error', 'wuclean-database-cleaner-for-woocommerce' ),
        'failed_start'                    => __( 'Failed to start scan', 'wuclean-database-cleaner-for-woocommerce' ),
        'connection_error_detailed'       => __( 'Connection error. Please try again.', 'wuclean-database-cleaner-for-woocommerce' ),
        'stopped_label'                   => __( 'Stopped', 'wuclean-database-cleaner-for-woocommerce' ),
        'scan_stopped_message'            => __( 'Scan cancelled. You can start a new scan.', 'wuclean-database-cleaner-for-woocommerce' ),
        'scan_failed_retry'               => __( 'Scan failed. Please try again.', 'wuclean-database-cleaner-for-woocommerce' ),
        'request_timed_out'               => __( 'Request timed out. Try Resume Scan, or use Stop & Reset to cancel.', 'wuclean-database-cleaner-for-woocommerce' ),
        'scan_resume_prompt'              => __( 'Scan interrupted. Click Resume Scan to continue, or Stop & Reset to cancel.', 'wuclean-database-cleaner-for-woocommerce' ),
        'connection_timeout'              => __( 'Connection error. Please try again or use Stop & Reset.', 'wuclean-database-cleaner-for-woocommerce' ),
        'checking_flagged_keyword'        => __( 'Checking flagged', 'wuclean-database-cleaner-for-woocommerce' ),
        'finalizing_keyword'              => __( 'Finalizing', 'wuclean-database-cleaner-for-woocommerce' ),
        'finalizing_html'                 => __( 'Finalizing scan', 'wuclean-database-cleaner-for-woocommerce' ),
        'scan_finalize_waiting'           => __( 'Cross-checking flagged images to improve accuracy. This may take a minute on large sites.', 'wuclean-database-cleaner-for-woocommerce' ),
        'scan_finalize_waiting_count'     => $scan_finalize_waiting_count,
        'issues_found_template'           => $issues_found_template,
        'scan_complete_heading'           => __( 'Scan complete!', 'wuclean-database-cleaner-for-woocommerce' ),
        'total_issues_template'           => $total_issues_template,
        'verified_images_single'          => $verified_images_single,
        'verified_images_plural'          => $verified_images_plural,
        'orphan_refinement_single'        => $orphan_refinement_single,
        'orphan_refinement_plural'        => $orphan_refinement_plural,
        'selection_single'                => $selection_single,
        'selection_plural'                => $selection_plural,
        'redirecting_results'             => __( 'Redirecting to results...', 'wuclean-database-cleaner-for-woocommerce' ),
        'results_url'                     => esc_url( add_query_arg( array(
            'page'     => 'wuclean-images',
            'tab'      => 'results',
            '_wpnonce' => wp_create_nonce( 'wuclean-images-tab' ),
        ), admin_url( 'admin.php' ) ) ),
        'quick_start_button_label'        => __( 'Media Library Scan', 'wuclean-database-cleaner-for-woocommerce' ),
        'combined_scan_available'         => false,
        'results_uploads_views_available' => false,
        'unknown_error'                   => __( 'Unknown error', 'wuclean-database-cleaner-for-woocommerce' ),
        'connection_error'                => __( 'Connection error', 'wuclean-database-cleaner-for-woocommerce' ),
        'protect_button_loading'          => __( '⏳ Protecting...', 'wuclean-database-cleaner-for-woocommerce' ),
        'protect_button_label'            => __( '🛡️ Protect', 'wuclean-database-cleaner-for-woocommerce' ),
        'protect_single_success'          => __( 'Image protected successfully', 'wuclean-database-cleaner-for-woocommerce' ),
        'protect_single_failed'           => __( 'Failed to protect image', 'wuclean-database-cleaner-for-woocommerce' ),
        'trash_button_loading'            => __( '⏳ Trashing...', 'wuclean-database-cleaner-for-woocommerce' ),
        'trash_button_label'              => __( '🗑️ Trash', 'wuclean-database-cleaner-for-woocommerce' ),
        'trash_single_success'            => __( 'Image moved to trash successfully', 'wuclean-database-cleaner-for-woocommerce' ),
        'trash_single_failed'             => __( 'Failed to trash image', 'wuclean-database-cleaner-for-woocommerce' ),
        'confirm_delete_image'            => __( '⚠️ PERMANENTLY DELETE this image? Have you made a backup of your uploads folder? This action CANNOT be undone!', 'wuclean-database-cleaner-for-woocommerce' ),
        'confirm_delete_trash_orphan'     => __( '⚠️ PERMANENTLY DELETE these trashed files? Have you made a backup of your uploads folder? This action CANNOT be undone!', 'wuclean-database-cleaner-for-woocommerce' ),
        'delete_image_button'             => __( '❌ Delete Permanently', 'wuclean-database-cleaner-for-woocommerce' ),
        'download_button_label'           => __( '⬇ Download', 'wuclean-database-cleaner-for-woocommerce' ),
        'delete_file_loading'             => __( 'Deleting...', 'wuclean-database-cleaner-for-woocommerce' ),
        'image_deleted_success'           => __( 'Image deleted permanently', 'wuclean-database-cleaner-for-woocommerce' ),
        'image_deleted_failed'            => __( 'Failed to delete image', 'wuclean-database-cleaner-for-woocommerce' ),
        'confirm_unprotect_image'         => __( 'Unprotect this image? It will not immediately be visible on the results tab but will be detected with the next image scan.', 'wuclean-database-cleaner-for-woocommerce' ),
        'unprotecting_label'              => __( 'Unprotecting...', 'wuclean-database-cleaner-for-woocommerce' ),
        'unprotect_button_label'          => __( '🔓 Unprotect', 'wuclean-database-cleaner-for-woocommerce' ),
        'unprotect_success'               => __( 'Image unprotected successfully', 'wuclean-database-cleaner-for-woocommerce' ),
        'unprotect_failed'                => __( 'Failed to unprotect image', 'wuclean-database-cleaner-for-woocommerce' ),
        'restoring_label'                 => __( 'Restoring...', 'wuclean-database-cleaner-for-woocommerce' ),
        'restore_selected_progress'       => $restore_selected_progress,
        'restore_button_label'            => __( '♻️ Restore', 'wuclean-database-cleaner-for-woocommerce' ),
        'confirm_restore_image'           => __( 'Restore image? It will immediately be visible on the website but will only show on the results tab again after the next image scan.', 'wuclean-database-cleaner-for-woocommerce' ),
        'restore_success'                 => __( 'Image restored successfully', 'wuclean-database-cleaner-for-woocommerce' ),
        'restore_failed'                  => __( 'Failed to restore image', 'wuclean-database-cleaner-for-woocommerce' ),
        'delete_image_nonce'              => wp_create_nonce( 'pt_wuclean_delete_image' ),
        'protect_image_nonce'             => wp_create_nonce( 'pt_wuclean_protect_image' ),
        'trash_image_nonce'               => wp_create_nonce( 'pt_wuclean_trash_image' ),
        'unprotect_image_nonce'           => wp_create_nonce( 'pt_wuclean_unprotect_image' ),
        'restore_image_nonce'             => wp_create_nonce( 'pt_wuclean_restore_image' ),
        'image_scan_nonce'                => wp_create_nonce( 'pt_wuclean_image_scan' ),
        'delete_trash_orphan_nonce'       => wp_create_nonce( 'pt_wuclean_delete_trash_orphan' ),
        'download_trash_orphan_nonce'     => wp_create_nonce( 'pt_wuclean_download_trash_orphan' ),
        'save_results_per_page_nonce'     => wp_create_nonce( 'pt_wuclean_save_results_per_page' ),
    );
    $manual_scan_resumable = false;
    $latest_scan_run = pt_wuclean_image_scan_db_get_latest_run();
    if ( is_array( $latest_scan_run ) && isset( $latest_scan_run['mode'], $latest_scan_run['status'] ) ) {
        $manual_scan_resumable = 'manual' === (string) $latest_scan_run['mode'] && 'running' === (string) $latest_scan_run['status'];
    }
    $strings['manual_scan_resumable'] = $manual_scan_resumable;
    return $strings;
}

/**
 * Main images page handler
 */
function pt_wuclean_render_images_page() {
    $tabs = pt_wuclean_register_images_tabs();
    // Verify nonce for tab parameter - required for security (CSRF protection).
    $active_tab = 'settings';
    if ( isset( $_GET['tab'] ) ) {
        $tab = sanitize_text_field( wp_unslash( $_GET['tab'] ) );
        $tab_is_valid = array_key_exists( $tab, $tabs );
        if ( $tab_is_valid && isset( $_GET['_wpnonce'] ) ) {
            $nonce = sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) );
            // Some tabs use dedicated nonces for search/pagination actions.
            // Accept the tab nonce OR the relevant tab-specific nonces to avoid redirecting back to Settings.
            $allowed_actions = array('wuclean-images-tab');
            if ( 'results' === $tab ) {
                $allowed_actions[] = 'wuclean-images-search';
                $allowed_actions[] = 'wuclean-images-pagination';
            }
            if ( 'trash' === $tab ) {
                $allowed_actions[] = 'wuclean-images-trash-search';
                $allowed_actions[] = 'wuclean-images-trash-pagination';
            }
            if ( 'protected' === $tab ) {
                $allowed_actions[] = 'wuclean-images-protected-search';
                $allowed_actions[] = 'wuclean-images-protected-pagination';
            }
            foreach ( $allowed_actions as $action ) {
                if ( wp_verify_nonce( $nonce, $action ) ) {
                    $active_tab = $tab;
                    break;
                }
            }
        }
        // If the nonce is missing/expired, still allow admins to view the requested tab.
        // This prevents scheduled scan emails (and older bookmarks) from redirecting to Settings.
        if ( 'settings' === $active_tab && $tab_is_valid && current_user_can( pt_wuclean_get_required_capability() ) ) {
            $active_tab = $tab;
        }
        // Nonce verification failed - default to settings tab for security.
    }
    echo '<div class="wrap">';
    echo '<h1>🖼️ ' . esc_html__( 'Images', 'wuclean-database-cleaner-for-woocommerce' ) . '</h1>';
    // Tab navigation.
    echo '<h2 class="nav-tab-wrapper">';
    foreach ( $tabs as $tab_key => $tab_label ) {
        $active_class = ( $active_tab === $tab_key ? 'nav-tab-active' : '' );
        $tab_url = add_query_arg( array(
            'page'     => 'wuclean-images',
            'tab'      => $tab_key,
            '_wpnonce' => wp_create_nonce( 'wuclean-images-tab' ),
        ), admin_url( 'admin.php' ) );
        echo '<a href="' . esc_url( $tab_url ) . '" class="nav-tab ' . esc_attr( $active_class ) . '">' . esc_html( $tab_label ) . '</a>';
    }
    echo '</h2>';
    // 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;">';
    // Render active tab content.
    switch ( $active_tab ) {
        case 'settings':
            pt_wuclean_render_images_settings_tab();
            break;
        case 'results':
            pt_wuclean_render_images_results_tab();
            break;
        case 'protected':
            pt_wuclean_render_images_protected_tab();
            break;
        case 'trash':
            pt_wuclean_render_images_trash_tab();
            break;
    }
    echo '</div>';
    // Close inner wrapper.
    echo '</div>';
}

// ============================================================================
// SETTINGS TAB.
// ============================================================================
/**
 * Render settings tab
 */
function pt_wuclean_render_images_settings_tab() {
    global $pt_wuclean_nonces, $pt_wuclean_css_classes;
    $did_save = false;
    // Handle form submission.
    if ( isset( $_POST[$pt_wuclean_nonces['image_settings_nonce']] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST[$pt_wuclean_nonces['image_settings_nonce']] ) ), $pt_wuclean_nonces['save_image_settings'] ) ) {
        $existing_settings = get_option( 'pt_wuclean_image_settings', array() );
        $settings = ( is_array( $existing_settings ) ? $existing_settings : array() );
        update_option( 'pt_wuclean_image_settings', $settings, false );
        $did_save = true;
    }
    // Get current settings.
    $settings = get_option( 'pt_wuclean_image_settings', array() );
    $enable_image_scanning = ( isset( $settings['enable_image_scanning'] ) ? $settings['enable_image_scanning'] : 0 );
    $enable_delete_images = ( isset( $settings['enable_delete_images'] ) ? $settings['enable_delete_images'] : 0 );
    $resolved_scan_batch = pt_wuclean_image_scan_resolve_batch_size( ( is_array( $settings ) ? $settings : array() ), 'display' );
    $results_per_page = ( isset( $settings['results_per_page'] ) ? $settings['results_per_page'] : 25 );
    $scan_frequency = ( isset( $settings['scan_frequency'] ) ? $settings['scan_frequency'] : 'unscheduled' );
    $email_notification = ( isset( $settings['email_notification'] ) ? $settings['email_notification'] : 0 );
    echo '<div style="margin-top:2em; max-width: 1600px;">';
    echo '<h2>⚙️ ' . esc_html__( 'Image Cleanup Settings', 'wuclean-database-cleaner-for-woocommerce' ) . '</h2>';
    if ( $did_save ) {
        echo '<div class="notice notice-success"><p>' . esc_html__( 'Settings saved successfully!', 'wuclean-database-cleaner-for-woocommerce' ) . '</p></div>';
    }
    $printed_premium_image_scan_intro = false;
    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() ) {
        if ( !$printed_premium_image_scan_intro ) {
            echo '<p style="margin: 0 0 10px 0;"><strong>ℹ️ ' . esc_html__( 'About Media Library scans:', 'wuclean-database-cleaner-for-woocommerce' ) . '</strong> ' . esc_html__( 'Each Media Library image is checked against common WordPress and WooCommerce usage signals (featured images, product galleries, post/page/product content and excerpts, term thumbnails, and related checks).', 'wuclean-database-cleaner-for-woocommerce' ) . '</p>';
        }
        echo '<p style="margin: 0;"><strong>👑 ' . esc_html__( 'Wuclean Pro', 'wuclean-database-cleaner-for-woocommerce' ) . '</strong> — ' . esc_html__( 'Offers Deep Scan, Uploads folder scan, Product auto-cleanup and automated Media Library scans with email notifications.', 'wuclean-database-cleaner-for-woocommerce' ) . '</p>';
    }
    echo '</div>';
    // ========================================================================
    // MANUAL 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 Image Scanner', 'wuclean-database-cleaner-for-woocommerce' ) . '</h3>';
    $printed_premium_manual_intro = false;
    if ( pt_wuclean_fs()->is_not_paying() ) {
        if ( !$printed_premium_manual_intro ) {
            echo '<p>' . esc_html__( 'Media Library Scan processes attachments in batches. Large libraries may take several minutes.', 'wuclean-database-cleaner-for-woocommerce' ) . '</p>';
        }
    }
    echo '<input type="hidden" id="pt-wuclean-image-scan-nonce" value="' . esc_attr( wp_create_nonce( 'pt_wuclean_image_scan' ) ) . '">';
    echo '<input type="hidden" id="pt-wuclean-image-batch-size" value="' . esc_attr( (string) $resolved_scan_batch ) . '">';
    echo '<button type="button" id="pt-wuclean-start-image-scan-media-only" class="button button-primary" style="background: #2196f3; border-color: #2196f3;">🔍 ' . esc_html__( 'Media Library Scan', 'wuclean-database-cleaner-for-woocommerce' ) . '</button>';
    echo '<button type="button" id="pt-wuclean-stop-image-scan" class="button" style="display: none; margin-left: 10px; background: #dc3232; border-color: #dc3232; color: #fff;">' . esc_html__( 'Stop & Reset', 'wuclean-database-cleaner-for-woocommerce' ) . '</button>';
    echo '<button type="button" id="pt-wuclean-resume-image-scan" class="button button-primary" style="display: none; margin-left: 10px;">' . esc_html__( 'Resume Scan', 'wuclean-database-cleaner-for-woocommerce' ) . '</button>';
    // Progress display.
    echo '<div id="pt-wuclean-image-scan-progress" style="display: none; margin-top: 20px;">';
    echo '<h4>' . esc_html__( 'Scan Progress', 'wuclean-database-cleaner-for-woocommerce' ) . '</h4>';
    echo '<ol id="pt-wuclean-image-scan-steps" class="pt-wuclean-image-scan-steps" aria-label="' . esc_attr__( 'Scan steps', 'wuclean-database-cleaner-for-woocommerce' ) . '">';
    echo '<li class="pt-wuclean-scan-step" data-step="media_library"><span class="pt-wuclean-scan-step-marker" aria-hidden="true"></span><span class="pt-wuclean-scan-step-label">' . esc_html__( 'Media Library', 'wuclean-database-cleaner-for-woocommerce' ) . '</span></li>';
    echo '<li class="pt-wuclean-scan-step" data-step="refinement"><span class="pt-wuclean-scan-step-marker" aria-hidden="true"></span><span class="pt-wuclean-scan-step-label">' . esc_html__( 'Cross-check', 'wuclean-database-cleaner-for-woocommerce' ) . '</span></li>';
    echo '</ol>';
    echo '<div class="pt-wuclean-image-scan-bar-wrap" style="background:#f0f0f0; border:1px solid #ddd; border-radius:2px; height:30px; position:relative; margin: 15px 0;">';
    echo '<div id="pt-wuclean-image-progress-bar" style="background:#0073aa; height:100%; width:0%; transition:width 0.3s ease; border-radius:2px;"></div>';
    echo '<div id="pt-wuclean-image-progress-text" style="position:absolute; top:50%; left:50%; transform:translate(-50%, -50%); font-weight:bold; color:#333; font-size:14px;"></div>';
    echo '</div>';
    echo '<p id="pt-wuclean-image-progress-waiting" class="description" style="display:none; margin:10px 0 0 0; color:#2271b1; font-size:13px;" aria-live="polite"></p>';
    echo '<div id="pt-wuclean-image-progress-details" style="font-size:13px; color:#666; line-height:1.6;"></div>';
    echo '</div>';
    echo '</div>';
    // Upgrade callout on Settings tab.
    $pro_benefits = array(
        __( 'Scan Uploads folder for potentially unused images', 'wuclean-database-cleaner-for-woocommerce' ),
        __( 'Deep Scan selected images for any reference in any active plugin', 'wuclean-database-cleaner-for-woocommerce' ),
        __( 'Thumbnail visibility for detailed image variation analysis ', 'wuclean-database-cleaner-for-woocommerce' ),
        __( 'Automatically delete product images when a WooCommerce product is permanently deleted (only if unused elsewhere)', 'wuclean-database-cleaner-for-woocommerce' ),
        __( 'Schedule periodic Media Library scans and receive email notifications when potentially unused images are detected', 'wuclean-database-cleaner-for-woocommerce' ),
        __( 'Multiple select & bulk action functionlity on every tab.', 'wuclean-database-cleaner-for-woocommerce' )
    );
    pt_wuclean_render_pro_automation_callout( $pro_benefits, 'images-settings' );
    echo '</div>';
}

// ============================================================================
// RESULTS TAB (Phase 1: Simple List).
// ============================================================================
/**
 * Sanitize a user-provided image search term from request input.
 *
 * @param mixed $raw Raw value (e.g. from $_GET['search']).
 * @return string
 */
function pt_wuclean_image_search_sanitize_term(  $raw  ) {
    $term = ( is_string( $raw ) ? wp_unslash( $raw ) : '' );
    if ( '' === $term ) {
        return '';
    }
    return trim( sanitize_text_field( $term ) );
}

/**
 * Whether a string contains any apostrophe-like character.
 *
 * @param string $text Text to inspect.
 * @return bool
 */
function pt_wuclean_image_search_contains_apostrophe(  $text  ) {
    $text = ( is_string( $text ) ? $text : (string) $text );
    if ( '' === $text ) {
        return false;
    }
    return (bool) preg_match( '/[\\x{0027}\\x{0060}\\x{00B4}\\x{2018}\\x{2019}\\x{201A}\\x{201B}\\x{2032}\\x{02BC}]/u', $text );
}

/**
 * Unify apostrophe-like characters to ASCII apostrophe in decoded text.
 *
 * @param string $text Raw or partially decoded text.
 * @return string
 */
function pt_wuclean_image_search_unify_apostrophes(  $text  ) {
    $text = ( is_string( $text ) ? $text : (string) $text );
    if ( '' === $text ) {
        return '';
    }
    $text = html_entity_decode( $text, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
    $text = str_replace( array(
        '&#8217;',
        '&#8216;',
        '&#039;',
        '&apos;',
        "\\'",
        '&rsquo;',
        '&lsquo;'
    ), "'", $text );
    $text = preg_replace( '/[\\x{0027}\\x{0060}\\x{00B4}\\x{2018}\\x{2019}\\x{201A}\\x{201B}\\x{2032}\\x{02BC}]/u', "'", $text );
    return $text;
}

/**
 * Normalize text for case-insensitive image search comparisons.
 *
 * @param string $text Raw text.
 * @return string
 */
function pt_wuclean_image_search_normalize_text(  $text  ) {
    $text = ( is_string( $text ) ? $text : (string) $text );
    if ( '' === $text ) {
        return '';
    }
    $text = pt_wuclean_image_search_unify_apostrophes( $text );
    $text = str_replace( " ", ' ', $text );
    $text = preg_replace( '/\\s+/u', ' ', $text );
    $text = trim( $text );
    if ( function_exists( 'mb_strtolower' ) ) {
        return mb_strtolower( $text, 'UTF-8' );
    }
    return strtolower( $text );
}

/**
 * Remove apostrophes after normalization (for filename/title folding).
 *
 * @param string $text Raw text.
 * @return string
 */
function pt_wuclean_image_search_strip_apostrophes(  $text  ) {
    $text = pt_wuclean_image_search_normalize_text( $text );
    return str_replace( "'", '', $text );
}

/**
 * SQL expression that removes common apostrophe forms from a text column.
 *
 * @param string $column SQL column reference (e.g. p.post_title).
 * @return string
 */
function pt_wuclean_image_search_sql_folded_column(  $column  ) {
    return "REPLACE(REPLACE(REPLACE(REPLACE(REPLACE({$column}, CHAR(39), ''), UNHEX('E28099'), ''), UNHEX('E28098'), ''), '&#8217;', ''), '&#8216;', '')";
}

/**
 * Extra search variants for titles/filenames that differ only by apostrophe encoding.
 *
 * @param string $term Base search term.
 * @return string[]
 */
function pt_wuclean_image_search_get_apostrophe_variants(  $term  ) {
    $term = trim( (string) $term );
    $variants = array();
    if ( '' === $term ) {
        return $variants;
    }
    $apostrophe_chars = array(
        "'",
        "'",
        'ʼ',
        '`'
    );
    if ( !pt_wuclean_image_search_contains_apostrophe( $term ) ) {
        return $variants;
    }
    foreach ( $apostrophe_chars as $from_char ) {
        if ( false === strpos( $term, $from_char ) ) {
            continue;
        }
        foreach ( $apostrophe_chars as $to_char ) {
            if ( $from_char === $to_char ) {
                continue;
            }
            $variants[] = str_replace( $from_char, $to_char, $term );
        }
    }
    $without = str_replace( $apostrophe_chars, '', $term );
    $without = trim( preg_replace( '/\\s+/u', ' ', $without ) );
    if ( '' !== $without && $without !== $term ) {
        $variants[] = $without;
    }
    foreach ( $apostrophe_chars as $apos_char ) {
        if ( false === strpos( $term, $apos_char ) ) {
            continue;
        }
        $variants[] = str_replace( $apos_char, '&#039;', $term );
        $variants[] = str_replace( $apos_char, '&#8217;', $term );
        $variants[] = str_replace( $apos_char, '&apos;', $term );
        break;
    }
    return array_values( array_unique( array_filter( $variants ) ) );
}

/**
 * Whether normalized $haystack contains normalized $needle.
 *
 * @param string $haystack Haystack.
 * @param string $needle   Needle.
 * @return bool
 */
function pt_wuclean_image_search_text_contains(  $haystack, $needle  ) {
    $haystack = pt_wuclean_image_search_normalize_text( $haystack );
    $needle = pt_wuclean_image_search_normalize_text( $needle );
    if ( '' === $needle ) {
        return true;
    }
    if ( '' === $haystack ) {
        return false;
    }
    if ( function_exists( 'mb_strpos' ) ) {
        if ( false !== mb_strpos(
            $haystack,
            $needle,
            0,
            'UTF-8'
        ) ) {
            return true;
        }
    } elseif ( false !== strpos( $haystack, $needle ) ) {
        return true;
    }
    // Apostrophe-insensitive fallback (e.g. title "Dugan's" vs filename "Dugans").
    $haystack_folded = str_replace( "'", '', $haystack );
    $needle_folded = str_replace( "'", '', $needle );
    if ( '' === $needle_folded ) {
        return false;
    }
    if ( function_exists( 'mb_strpos' ) ) {
        return false !== mb_strpos(
            $haystack_folded,
            $needle_folded,
            0,
            'UTF-8'
        );
    }
    return false !== strpos( $haystack_folded, $needle_folded );
}

/**
 * Search term variants (spaced title vs hyphenated filename, etc.).
 *
 * @param string $search_term User search input.
 * @return string[] Unique non-empty variants.
 */
function pt_wuclean_image_search_get_term_variants(  $search_term  ) {
    $term = trim( (string) $search_term );
    $variants = array();
    if ( '' === $term ) {
        return $variants;
    }
    $variants[] = $term;
    if ( preg_match( '/\\s/u', $term ) ) {
        $hyphen = preg_replace( '/\\s+/u', '-', $term );
        if ( is_string( $hyphen ) && '' !== $hyphen ) {
            $variants[] = $hyphen;
        }
        $underscore = preg_replace( '/\\s+/u', '_', $term );
        if ( is_string( $underscore ) && '' !== $underscore ) {
            $variants[] = $underscore;
        }
    }
    if ( preg_match( '/[-_]/u', $term ) ) {
        $spaced = preg_replace( '/[-_]+/u', ' ', $term );
        if ( is_string( $spaced ) ) {
            $spaced = trim( preg_replace( '/\\s+/u', ' ', $spaced ) );
            if ( '' !== $spaced ) {
                $variants[] = $spaced;
            }
        }
    }
    $variants = array_merge( $variants, pt_wuclean_image_search_get_apostrophe_variants( $term ) );
    $folded = pt_wuclean_image_search_strip_apostrophes( $term );
    if ( '' !== $folded ) {
        $variants[] = $folded;
    }
    return array_values( array_unique( array_filter( $variants ) ) );
}

/**
 * Whether an attachment matches a user search (title, slug, alt, filename, ID).
 *
 * @param int    $attachment_id Attachment ID.
 * @param string $search_term   Search term.
 * @return bool
 */
function pt_wuclean_attachment_matches_image_search(  $attachment_id, $search_term  ) {
    $attachment_id = absint( $attachment_id );
    $search_term = trim( (string) $search_term );
    if ( '' === $search_term ) {
        return true;
    }
    if ( $attachment_id <= 0 ) {
        return false;
    }
    $variants = pt_wuclean_image_search_get_term_variants( $search_term );
    $post = get_post( $attachment_id );
    $fields = array((string) $attachment_id);
    if ( $post instanceof WP_Post ) {
        $fields[] = $post->post_title;
        $fields[] = $post->post_name;
        $fields[] = $post->post_excerpt;
        $fields[] = $post->post_content;
    }
    $alt = get_post_meta( $attachment_id, '_wp_attachment_image_alt', true );
    if ( is_string( $alt ) && '' !== $alt ) {
        $fields[] = $alt;
    }
    $file_path = get_attached_file( $attachment_id );
    if ( is_string( $file_path ) && '' !== $file_path ) {
        $fields[] = $file_path;
        $fields[] = basename( $file_path );
        $fields[] = pathinfo( $file_path, PATHINFO_FILENAME );
    }
    foreach ( $fields as $field ) {
        if ( !is_string( $field ) || '' === $field ) {
            continue;
        }
        foreach ( $variants as $variant ) {
            if ( pt_wuclean_image_search_text_contains( $field, $variant ) ) {
                return true;
            }
        }
    }
    return false;
}

/**
 * Whether a Wuclean trash orphan group matches a search term.
 *
 * @param array  $group       Orphan group from pt_wuclean_get_wuclean_trash_orphan_groups().
 * @param string $search_term Search term.
 * @return bool
 */
function pt_wuclean_trash_orphan_group_matches_image_search(  array $group, $search_term  ) {
    $search_term = trim( (string) $search_term );
    if ( '' === $search_term ) {
        return true;
    }
    $variants = pt_wuclean_image_search_get_term_variants( $search_term );
    $fields = array();
    if ( isset( $group['attachment_id'] ) ) {
        $fields[] = (string) absint( $group['attachment_id'] );
    }
    if ( isset( $group['filename'] ) ) {
        $fields[] = (string) $group['filename'];
    }
    if ( isset( $group['files'] ) && is_array( $group['files'] ) ) {
        foreach ( $group['files'] as $file ) {
            if ( is_string( $file ) && '' !== $file ) {
                $fields[] = $file;
                $fields[] = basename( $file );
            }
        }
    }
    $attachment_id = ( isset( $group['attachment_id'] ) ? absint( $group['attachment_id'] ) : 0 );
    if ( $attachment_id > 0 && pt_wuclean_attachment_matches_image_search( $attachment_id, $search_term ) ) {
        return true;
    }
    foreach ( $fields as $field ) {
        foreach ( $variants as $variant ) {
            if ( pt_wuclean_image_search_text_contains( $field, $variant ) ) {
                return true;
            }
        }
    }
    return false;
}

/**
 * Append SQL to hide scan rows whose filename (or path) uses one of the given extensions.
 *
 * Format filters use the on-disk extension only. The file_type column is kept for
 * relationship/UI labels (e.g. original, thumbnail, sibling) and is not used here.
 *
 * @param string $where       WHERE clause fragment (modified by reference).
 * @param array  $args        Prepared statement args (modified by reference).
 * @param array  $extensions  Lowercase extensions without dot (e.g. jpg, jpeg, webp).
 * @return void
 */
function pt_wuclean_image_scan_db_append_hide_extensions_where(  &$where, array &$args, array $extensions  ) {
    $extensions = array_values( array_unique( array_filter( array_map( static function ( $ext ) {
        return strtolower( (string) $ext );
    }, $extensions ) ) ) );
    if ( empty( $extensions ) ) {
        return;
    }
    $match_parts = array();
    foreach ( $extensions as $ext ) {
        $match_parts[] = 'LOWER(SUBSTRING_INDEX(i.filename, \'.\', -1)) = %s';
        $args[] = $ext;
        $match_parts[] = '((i.filename IS NULL OR i.filename = \'\') AND i.path IS NOT NULL AND LOWER(SUBSTRING_INDEX(i.path, \'.\', -1)) = %s)';
        $args[] = $ext;
    }
    $where .= ' AND NOT ( ' . implode( ' OR ', $match_parts ) . ' ) ';
}

/**
 * DB: build query parts for results filtering/search.
 *
 * @param int    $run_id       Run ID.
 * @param string $search_term  Search term.
 * @param array  $filters      Filter keys (e.g. hide_orphaned, hide_originals_of_scaled for used scaled only, hide_thumbnails).
 * @return array{join:string,where:string,args:array}
 */
function pt_wuclean_image_scan_db_build_results_query_parts(  $run_id, $search_term, array $filters  ) {
    global $wpdb;
    $run_id = absint( $run_id );
    $search_term = (string) $search_term;
    $items_table = $wpdb->prefix . 'wuclean_image_scan_items';
    $posts_table = $wpdb->posts;
    // Left joins to allow searching Media Library titles, slugs, and alt text.
    $join = " LEFT JOIN {$posts_table} p ON p.ID = i.object_id ";
    $join .= " LEFT JOIN {$wpdb->postmeta} pm_alt ON pm_alt.post_id = i.object_id AND pm_alt.meta_key = '_wp_attachment_image_alt' ";
    $where = ' WHERE i.run_id = %d ';
    $args = array($run_id);
    // Always exclude woocommerce-placeholder (any extension).
    $where .= ' AND (i.filename IS NULL OR i.filename NOT LIKE %s) ';
    $args[] = '%' . $wpdb->esc_like( 'woocommerce-placeholder' ) . '%';
    if ( !empty( $filters['hide_orphaned'] ) ) {
        $where .= ' AND i.type = %s ';
        $args[] = 'media_library';
    }
    if ( !empty( $filters['show_orphans_only'] ) ) {
        $where .= ' AND i.type = %s ';
        $args[] = 'orphaned_file';
    }
    if ( !empty( $filters['hide_originals_of_scaled'] ) ) {
        // Only hide on-disk originals beside a scaled attachment when that scaled attachment appears in use (sibling_used = 1).
        $where .= ' AND NOT ( i.type = %s AND i.subtype = %s AND i.sibling_used = 1 ) ';
        $args[] = 'orphaned_file';
        $args[] = 'scaled_original';
    }
    if ( !empty( $filters['hide_safe_siblings'] ) ) {
        $where .= ' AND NOT (i.type = %s AND i.subtype = %s AND i.sibling_used = 1) ';
        $args[] = 'orphaned_file';
        $args[] = 'sibling';
    }
    if ( !empty( $filters['hide_jpg'] ) ) {
        pt_wuclean_image_scan_db_append_hide_extensions_where( $where, $args, array('jpg', 'jpeg') );
    }
    if ( !empty( $filters['hide_png'] ) ) {
        pt_wuclean_image_scan_db_append_hide_extensions_where( $where, $args, array('png') );
    }
    if ( !empty( $filters['hide_webp'] ) ) {
        pt_wuclean_image_scan_db_append_hide_extensions_where( $where, $args, array('webp') );
    }
    if ( !empty( $filters['hide_avif'] ) ) {
        pt_wuclean_image_scan_db_append_hide_extensions_where( $where, $args, array('avif') );
    }
    if ( !empty( $filters['hide_thumbnails'] ) ) {
        $where .= ' AND NOT ( i.type = %s AND i.subtype IN (%s,%s) ) ';
        $args[] = 'orphaned_file';
        $args[] = 'thumbnail';
        $args[] = 'thumbnail_no_parent';
        $thumb_sibling_like = '%' . $wpdb->esc_like( '"is_thumbnail_sibling":true' ) . '%';
        $where .= ' AND NOT ( i.type = %s AND i.subtype IN (%s,%s) AND i.data_json LIKE %s ) ';
        $args[] = 'orphaned_file';
        $args[] = 'sibling';
        $args[] = 'sibling_no_parent';
        $args[] = $thumb_sibling_like;
    }
    if ( '' !== $search_term ) {
        $search_clauses = array();
        $variants = pt_wuclean_image_search_get_term_variants( $search_term );
        if ( is_numeric( $search_term ) ) {
            $search_clauses[] = 'i.object_id = %d';
            $args[] = absint( $search_term );
        }
        $file_columns = array('i.filename', 'i.path', 'i.parent_filename');
        $text_columns = array('p.post_title', 'p.post_name', 'pm_alt.meta_value');
        foreach ( $variants as $variant ) {
            $like = '%' . $wpdb->esc_like( $variant ) . '%';
            foreach ( array_merge( $file_columns, $text_columns ) as $column ) {
                $search_clauses[] = "{$column} LIKE %s";
                $args[] = $like;
            }
        }
        $folded = pt_wuclean_image_search_strip_apostrophes( $search_term );
        if ( '' !== $folded ) {
            $like_folded = '%' . $wpdb->esc_like( $folded ) . '%';
            foreach ( $file_columns as $column ) {
                $search_clauses[] = "{$column} LIKE %s";
                $args[] = $like_folded;
            }
            foreach ( $text_columns as $column ) {
                $search_clauses[] = pt_wuclean_image_search_sql_folded_column( $column ) . ' LIKE %s';
                $args[] = $like_folded;
            }
        }
        if ( !empty( $search_clauses ) ) {
            $where .= ' AND (' . implode( ' OR ', $search_clauses ) . ') ';
        }
    }
    return array(
        'join'  => $join,
        'where' => $where,
        'args'  => $args,
    );
}

/**
 * DB: count filtered results for a run.
 *
 * @param int    $run_id       Run ID.
 * @param string $search_term  Search term.
 * @param array  $filters      Filter keys.
 * @return int
 */
function pt_wuclean_image_scan_db_count_filtered_results(  $run_id, $search_term, array $filters  ) {
    global $wpdb;
    $run_id = absint( $run_id );
    if ( $run_id <= 0 ) {
        return 0;
    }
    $items_table = $wpdb->prefix . 'wuclean_image_scan_items';
    $items_table_escaped = esc_sql( $items_table );
    $parts = pt_wuclean_image_scan_db_build_results_query_parts( $run_id, $search_term, $filters );
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $items_table_escaped from esc_sql(); $parts['join']/$parts['where'] from build_results_query_parts() with placeholders; spread args match WHERE placeholders
    // phpcs:disable PluginCheck.Security.DirectDB.UnescapedDBParameter -- $parts['join']/$parts['where'] contain only static SQL fragments with placeholders; all dynamic values are passed via $parts['args'] to prepare()
    return (int) $wpdb->get_var( $wpdb->prepare( 
        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $items_table_escaped, $parts from esc_sql/build_results_query_parts; variadic $parts['args']
        "SELECT COUNT(*) FROM {$items_table_escaped} i {$parts['join']} {$parts['where']}",
        ...$parts['args']
     ) );
    // phpcs:enable PluginCheck.Security.DirectDB.UnescapedDBParameter
}

/**
 * DB: fetch one page of filtered results (decoded to legacy item shape).
 *
 * @param int    $run_id       Run ID.
 * @param string $search_term  Search term.
 * @param array  $filters      Filter keys.
 * @param int    $per_page     Results per page.
 * @param int    $offset       Offset.
 * @return array
 */
function pt_wuclean_image_scan_db_get_filtered_results_page(
    $run_id,
    $search_term,
    array $filters,
    $per_page,
    $offset
) {
    global $wpdb;
    $run_id = absint( $run_id );
    $per_page = max( 25, min( 200, absint( $per_page ) ) );
    $offset = max( 0, absint( $offset ) );
    if ( $run_id <= 0 ) {
        return array();
    }
    $items_table = $wpdb->prefix . 'wuclean_image_scan_items';
    $items_table_escaped = esc_sql( $items_table );
    $parts = pt_wuclean_image_scan_db_build_results_query_parts( $run_id, $search_term, $filters );
    $sql = "SELECT i.data_json\n        FROM {$items_table_escaped} i\n        {$parts['join']}\n        {$parts['where']}\n        ORDER BY COALESCE(i.filename, '') ASC, i.id ASC\n        LIMIT %d OFFSET %d";
    $args = array_merge( $parts['args'], array($per_page, $offset) );
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $sql built from esc_sql()/build_results_query_parts(); $parts['join']/$parts['where'] contain only static SQL and placeholders; values passed via variadic $args to prepare()
    $rows = $wpdb->get_col( 
        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $sql is passed to prepare() on next line; result is prepared
        $wpdb->prepare( $sql, ...$args )
     );
    if ( empty( $rows ) || !is_array( $rows ) ) {
        return array();
    }
    $items = array();
    foreach ( $rows as $json ) {
        $decoded = json_decode( (string) $json, true );
        if ( is_array( $decoded ) ) {
            $items[] = $decoded;
        }
    }
    if ( function_exists( 'pt_wuclean_image_scan_sort_results_clustered' ) ) {
        $items = pt_wuclean_image_scan_sort_results_clustered( $items );
    }
    return $items;
}

/**
 * DB: get latest completed run id (0 if none).
 *
 * @return int
 */
function pt_wuclean_image_scan_db_get_latest_completed_run_id() {
    $run = pt_wuclean_image_scan_db_get_latest_run( 'completed' );
    return ( is_array( $run ) && !empty( $run['id'] ) ? absint( $run['id'] ) : 0 );
}

/**
 * DB: remove a Media Library result item and its orphaned siblings (by parent filename).
 *
 * @param int    $image_id        Image ID.
 * @param string $parent_filename Parent filename.
 * @return void
 */
function pt_wuclean_image_scan_db_remove_media_item_and_siblings(  $image_id, $parent_filename  ) {
    global $wpdb;
    $run_id = pt_wuclean_image_scan_db_get_latest_completed_run_id();
    if ( $run_id <= 0 ) {
        return;
    }
    $image_id = absint( $image_id );
    $parent_filename = (string) $parent_filename;
    $items_table = $wpdb->prefix . 'wuclean_image_scan_items';
    $items_table_escaped = esc_sql( $items_table );
    if ( 0 < $image_id ) {
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from esc_sql.
        $wpdb->query( $wpdb->prepare(
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $items_table_escaped from esc_sql.
            "DELETE FROM {$items_table_escaped} WHERE run_id = %d AND type = %s AND object_id = %d",
            $run_id,
            'media_library',
            $image_id
        ) );
    }
    if ( '' !== $parent_filename ) {
        // Also remove any orphaned-file rows that are tied to this parent filename (e.g. scaled originals, siblings).
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from esc_sql.
        $wpdb->query( $wpdb->prepare(
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $items_table_escaped from esc_sql.
            "DELETE FROM {$items_table_escaped} WHERE run_id = %d AND type = %s AND parent_filename = %s",
            $run_id,
            'orphaned_file',
            $parent_filename
        ) );
        // If this is a WordPress scaled filename (e.g. "image-scaled.jpg"), also remove any
        // orphaned rows that track the original filename as "scaled_original" or "scaled_original_no_scaled".
        $scaled_path_info = pathinfo( $parent_filename );
        $scaled_base = ( isset( $scaled_path_info['filename'] ) && is_string( $scaled_path_info['filename'] ) ? (string) $scaled_path_info['filename'] : '' );
        $scaled_ext = ( isset( $scaled_path_info['extension'] ) && is_string( $scaled_path_info['extension'] ) ? strtolower( (string) $scaled_path_info['extension'] ) : '' );
        if ( '' !== $scaled_base && '' !== $scaled_ext && false !== strpos( $scaled_base, '-scaled' ) ) {
            $original_base = str_replace( '-scaled', '', $scaled_base );
            $original_filename = $original_base . '.' . $scaled_ext;
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from esc_sql.
            $wpdb->query( $wpdb->prepare(
                // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $items_table_escaped from esc_sql.
                "DELETE FROM {$items_table_escaped} WHERE run_id = %d AND type = %s AND filename = %s AND subtype IN ( %s, %s )",
                $run_id,
                'orphaned_file',
                $original_filename,
                'scaled_original',
                'scaled_original_no_scaled'
            ) );
            // WebP/AVIF siblings of the pre-scaled file use parent_filename "aaagiant.jpg" while the ML row
            // is trashed with parent_filename "aaagiant-scaled.jpg" — remove those orphaned_file rows too.
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from esc_sql.
            $wpdb->query( $wpdb->prepare(
                // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $items_table_escaped from esc_sql.
                "DELETE FROM {$items_table_escaped} WHERE run_id = %d AND type = %s AND parent_filename = %s",
                $run_id,
                'orphaned_file',
                $original_filename
            ) );
        }
    }
    if ( 0 < $image_id ) {
        // Rows that link WebP/AVIF to this attachment in data_json (subtype "sibling") — always remove by ID
        // so we do not depend on parent_filename column matching (encoding, scan edge cases).
        $sibling_json_needle = '"sibling_id":' . (string) $image_id . ',';
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from esc_sql.
        $wpdb->query( $wpdb->prepare(
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $items_table_escaped from esc_sql.
            "DELETE FROM {$items_table_escaped} WHERE run_id = %d AND type = %s AND LOCATE( %s, data_json ) > 0",
            $run_id,
            'orphaned_file',
            $sibling_json_needle
        ) );
        // subtype "sibling_no_parent" leaves parent_filename NULL; match stored path for known sidecars next to this attachment.
        $uploads = wp_upload_dir();
        $basedir = ( isset( $uploads['basedir'] ) ? (string) $uploads['basedir'] : '' );
        $rel = get_post_meta( $image_id, '_wp_attached_file', true );
        if ( '' !== $basedir && is_string( $rel ) && '' !== $rel ) {
            $rel_norm = str_replace( '\\', '/', $rel );
            $dir_abs = wp_normalize_path( path_join( $basedir, dirname( $rel_norm ) ) );
            if ( '' !== $dir_abs ) {
                $meta_att = wp_get_attachment_metadata( $image_id );
                $basenames = array(basename( $rel_norm ));
                if ( is_array( $meta_att ) && !empty( $meta_att['original_image'] ) && is_string( $meta_att['original_image'] ) ) {
                    $basenames[] = basename( $meta_att['original_image'] );
                }
                if ( is_array( $meta_att ) && isset( $meta_att['sizes'] ) && is_array( $meta_att['sizes'] ) ) {
                    foreach ( $meta_att['sizes'] as $size_data ) {
                        if ( !empty( $size_data['file'] ) && is_string( $size_data['file'] ) ) {
                            $basenames[] = basename( $size_data['file'] );
                        }
                    }
                }
                $basenames = array_unique( array_filter( $basenames ) );
                $side_paths = array();
                foreach ( $basenames as $bn ) {
                    foreach ( array('webp', 'avif') as $fmt ) {
                        $pi = pathinfo( $bn );
                        $stem = ( isset( $pi['filename'] ) && is_string( $pi['filename'] ) ? $pi['filename'] : '' );
                        if ( '' !== $stem ) {
                            $side_paths[] = wp_normalize_path( path_join( $dir_abs, $stem . '.' . $fmt ) );
                        }
                        $side_paths[] = wp_normalize_path( path_join( $dir_abs, $bn . '.' . $fmt ) );
                    }
                }
                $side_paths = array_unique( array_filter( $side_paths ) );
                foreach ( $side_paths as $side_path ) {
                    pt_wuclean_image_scan_db_remove_orphaned_item_by_path( $side_path );
                }
            }
        }
    }
}

/**
 * DB: remove an orphaned file result item by full path.
 *
 * @param string $path Full path to the orphaned file.
 * @return void
 */
function pt_wuclean_image_scan_db_remove_orphaned_item_by_path(  $path  ) {
    global $wpdb;
    $run_id = pt_wuclean_image_scan_db_get_latest_completed_run_id();
    if ( $run_id <= 0 ) {
        return;
    }
    $path = wp_normalize_path( (string) $path );
    if ( '' === $path ) {
        return;
    }
    $items_table = $wpdb->prefix . 'wuclean_image_scan_items';
    $items_table_escaped = esc_sql( $items_table );
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from esc_sql
    $wpdb->query( $wpdb->prepare(
        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $items_table_escaped from esc_sql
        "DELETE FROM {$items_table_escaped} WHERE run_id = %d AND type = %s AND path = %s",
        $run_id,
        'orphaned_file',
        $path
    ) );
}

/**
 * Build query args that preserve Scan Results filters for in-tab links and search.
 *
 * @param string $orphan_filter_value         orphan_filter radio value.
 * @param int    $per_page                    Results per page.
 * @param bool   $show_uploads_folder_filters Whether uploads-folder checkboxes apply.
 * @param int    $hide_safe_siblings          0|1.
 * @param int    $hide_thumbnails             0|1.
 * @param int    $hide_originals_of_scaled    0|1.
 * @param int    $hide_jpg                    0|1.
 * @param int    $hide_png                    0|1.
 * @param int    $hide_webp                   0|1.
 * @param int    $hide_avif                   0|1.
 * @return array<string, int|string>
 */
function pt_wuclean_image_results_build_preserve_query_args(
    $orphan_filter_value,
    $per_page,
    $show_uploads_folder_filters,
    $hide_safe_siblings,
    $hide_thumbnails,
    $hide_originals_of_scaled,
    $hide_jpg,
    $hide_png,
    $hide_webp,
    $hide_avif
) {
    $args = array(
        'page'          => 'wuclean-images',
        'tab'           => 'results',
        'orphan_filter' => $orphan_filter_value,
        'per_page'      => max( 25, min( 200, absint( $per_page ) ) ),
    );
    if ( $show_uploads_folder_filters ) {
        $args['hide_safe_siblings'] = ( $hide_safe_siblings ? '1' : '0' );
        $args['hide_thumbnails'] = ( $hide_thumbnails ? '1' : '0' );
        $args['hide_originals_of_scaled'] = ( $hide_originals_of_scaled ? '1' : '0' );
    }
    if ( $hide_jpg ) {
        $args['hide_jpg'] = '1';
    }
    if ( $hide_png ) {
        $args['hide_png'] = '1';
    }
    if ( $hide_webp ) {
        $args['hide_webp'] = '1';
    }
    if ( $hide_avif ) {
        $args['hide_avif'] = '1';
    }
    return $args;
}

/**
 * Render results tab.
 */
function pt_wuclean_render_images_results_tab() {
    $latest_completed = pt_wuclean_image_scan_db_get_latest_run( 'completed' );
    $run_id = ( is_array( $latest_completed ) && !empty( $latest_completed['id'] ) ? absint( $latest_completed['id'] ) : 0 );
    $run_settings = ( is_array( $latest_completed ) ? pt_wuclean_image_scan_db_decode_json_field( $latest_completed['settings_json'] ?? '' ) : array() );
    $scan_scope = ( isset( $run_settings['scan_scope'] ) && is_string( $run_settings['scan_scope'] ) ? (string) $run_settings['scan_scope'] : 'media_only' );
    $scan_scope = pt_wuclean_image_scan_engine_normalize_scan_scope( $scan_scope );
    $is_full_scan = 'media_and_uploads' === $scan_scope;
    $show_uploads_folder_view_modes = $run_id > 0 && $is_full_scan;
    $protected_images = pt_wuclean_get_protected_images();
    echo '<div style="max-width: 1600px;">';
    echo '<h2 style="margin: 12px 0 15px 0; font-size: 1.3em; font-weight: 600; display: flex; align-items: center; gap: 8px;">🔍 ' . esc_html__( 'Image Scan Results', '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 0 10px 0;"><strong>ℹ️ ' . esc_html__( 'About Results:', 'wuclean-database-cleaner-for-woocommerce' ) . '</strong> ' . esc_html__( 'While very accurate for most WooCommerce websites, it is impossible to cater for every theme, plugin, widget and custom code snippet in a quick scan. Therefore, we cannot guarantee that the results are 100% accurate.', 'wuclean-database-cleaner-for-woocommerce' ) . '</p>';
    echo '<p style="margin: 0 0 10px 0;"><strong>⚠️ ' . esc_html__( 'Backup & Verify:', 'wuclean-database-cleaner-for-woocommerce' ) . '</strong> ' . esc_html__( 'On any system, it is wise to make backups before deleting anything. Always verify before deleting.', 'wuclean-database-cleaner-for-woocommerce' ) . '</p>';
    echo '<ul style="margin: 0; padding-left: 20px;">';
    echo '<li><strong>🛡️ ' . esc_html__( 'Protect', 'wuclean-database-cleaner-for-woocommerce' ) . '</strong> - ' . esc_html__( 'If you see images that you know are in use (false positives), you can Protect these images so that they are removed from the list and future scans.', 'wuclean-database-cleaner-for-woocommerce' ) . '</li>';
    echo '<li><strong>🗑️ ' . esc_html__( 'Trash', 'wuclean-database-cleaner-for-woocommerce' ) . '</strong> - ' . esc_html__( 'Hide images from WordPress to test the website (non-cached) before restoring needed images or deleting unused images.', 'wuclean-database-cleaner-for-woocommerce' ) . '</li>';
    echo '<li><strong>❌ ' . esc_html__( 'Delete', 'wuclean-database-cleaner-for-woocommerce' ) . '</strong> - ' . esc_html__( 'Review items carefully before deleting permanently. This action CANNOT be undone!', 'wuclean-database-cleaner-for-woocommerce' ) . '</li>';
    if ( pt_wuclean_fs()->is_not_paying() ) {
        echo '<li><strong>👑 ' . esc_html__( 'Wuclean Pro', 'wuclean-database-cleaner-for-woocommerce' ) . '</strong> - ' . esc_html__( 'Offers Deep Scan, Uploads Folder scan, Thumbnail visibility, Bulk operations, auto-product image cleanup, and scheduled periodic scans.', 'wuclean-database-cleaner-for-woocommerce' ) . '</li>';
    }
    echo '</ul>';
    echo '</div>';
    // Search functionality.
    $search_term = '';
    if ( isset( $_GET['search'] ) ) {
        // Verify nonce - required for security (CSRF protection).
        if ( isset( $_GET['_wpnonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'wuclean-images-search' ) ) {
            $search_term = pt_wuclean_image_search_sanitize_term( sanitize_text_field( wp_unslash( $_GET['search'] ) ) );
        }
        // Nonce verification failed - ignore search parameter for security.
    }
    // Get filter parameters from URL (needed early for search form).
    // Handle radio button for orphan filter (mutually exclusive).
    // Support both new radio button parameter and old parameters for backward compatibility.
    $orphan_filter_set = isset( $_GET['orphan_filter'] );
    $orphan_filter = '';
    if ( $orphan_filter_set ) {
        $orphan_filter = sanitize_text_field( wp_unslash( $_GET['orphan_filter'] ) );
    }
    if ( '' === $orphan_filter ) {
        if ( isset( $_GET['hide_orphaned'] ) && '1' === sanitize_text_field( wp_unslash( $_GET['hide_orphaned'] ) ) ) {
            $orphan_filter = 'hide_orphaned';
        } elseif ( isset( $_GET['show_orphans_only'] ) && '1' === sanitize_text_field( wp_unslash( $_GET['show_orphans_only'] ) ) ) {
            $orphan_filter = 'show_orphans_only';
        } elseif ( !$orphan_filter_set ) {
            $orphan_filter = 'hide_orphaned';
        }
    }
    $allowed_orphan_filters = array(
        '',
        'hide_orphaned',
        'show_orphans_only',
        'show_all'
    );
    if ( !in_array( $orphan_filter, $allowed_orphan_filters, true ) ) {
        $orphan_filter = 'hide_orphaned';
    }
    $orphan_filter_for_results = $orphan_filter;
    $orphan_filter = 'hide_orphaned';
    $hide_orphaned = ( 'hide_orphaned' === $orphan_filter ? 1 : 0 );
    $show_orphans_only = ( 'show_orphans_only' === $orphan_filter ? 1 : 0 );
    // Uploads-folder-derived filters only apply when this run included uploads scanning and the view can list orphan rows.
    $show_uploads_folder_filters = $run_id > 0 && $is_full_scan && !$hide_orphaned;
    // Default on: hide siblings of used images unless explicitly disabled (?hide_safe_siblings=0).
    $hide_safe_siblings_param = ( isset( $_GET['hide_safe_siblings'] ) ? sanitize_text_field( wp_unslash( $_GET['hide_safe_siblings'] ) ) : '' );
    if ( $show_uploads_folder_filters && '0' === $hide_safe_siblings_param ) {
        $hide_safe_siblings = 0;
    } elseif ( $show_uploads_folder_filters ) {
        $hide_safe_siblings = 1;
    } else {
        $hide_safe_siblings = 0;
    }
    $hide_jpg = ( isset( $_GET['hide_jpg'] ) ? 1 : 0 );
    $hide_png = ( isset( $_GET['hide_png'] ) ? 1 : 0 );
    $hide_webp = ( isset( $_GET['hide_webp'] ) ? 1 : 0 );
    $hide_avif = ( isset( $_GET['hide_avif'] ) ? 1 : 0 );
    // Default on: hide thumbnail / resized rows unless explicitly disabled (?hide_thumbnails=0).
    $hide_thumbnails_param = ( isset( $_GET['hide_thumbnails'] ) ? sanitize_text_field( wp_unslash( $_GET['hide_thumbnails'] ) ) : '' );
    if ( $show_uploads_folder_filters && '0' === $hide_thumbnails_param ) {
        $hide_thumbnails = 0;
    } elseif ( $show_uploads_folder_filters ) {
        $hide_thumbnails = 1;
    } else {
        $hide_thumbnails = 0;
    }
    // Uploads-folder + combined views: by default hide on-disk originals whose scaled attachment appears in use; ?hide_originals_of_scaled=0 shows them.
    // Legacy bookmark: show_originals_only=1 meant "only those rows" → treat as hide_originals_of_scaled=0 (combined view).
    // Media Library-only or media-only scan: filter does not apply.
    if ( $show_uploads_folder_filters ) {
        $show_originals_only_param = ( isset( $_GET['show_originals_only'] ) ? sanitize_text_field( wp_unslash( $_GET['show_originals_only'] ) ) : '' );
        $hide_originals_of_scaled_param = ( isset( $_GET['hide_originals_of_scaled'] ) ? sanitize_text_field( wp_unslash( $_GET['hide_originals_of_scaled'] ) ) : '' );
        if ( '1' === $show_originals_only_param ) {
            $hide_originals_of_scaled = 0;
        } elseif ( '0' === $hide_originals_of_scaled_param ) {
            $hide_originals_of_scaled = 0;
        } else {
            $hide_originals_of_scaled = 1;
        }
    } else {
        $hide_originals_of_scaled = 0;
    }
    // Radio values: show_all is explicit so combined-results choice survives search/pagination (legacy '' still means show all).
    $orphan_filter_value = 'show_all';
    if ( $hide_orphaned ) {
        $orphan_filter_value = 'hide_orphaned';
    } elseif ( $show_orphans_only ) {
        $orphan_filter_value = 'show_orphans_only';
    }
    $filters = array(
        'hide_orphaned'            => $hide_orphaned,
        'show_orphans_only'        => $show_orphans_only,
        'hide_safe_siblings'       => $hide_safe_siblings,
        'hide_jpg'                 => $hide_jpg,
        'hide_png'                 => $hide_png,
        'hide_webp'                => $hide_webp,
        'hide_avif'                => $hide_avif,
        'hide_thumbnails'          => $hide_thumbnails,
        'hide_originals_of_scaled' => $hide_originals_of_scaled,
    );
    /**
     * Filters image scan results query flags before counting and fetching rows.
     *
     * @param array $filters Associative filters for {@see pt_wuclean_image_scan_db_build_results_query_parts()}.
     * @param int   $run_id  Current completed scan run ID.
     */
    $filters_filtered = apply_filters( 'pt_wuclean_image_scan_results_filters', $filters, $run_id );
    if ( is_array( $filters_filtered ) ) {
        $filters = $filters_filtered;
    }
    $total_results = ( $run_id > 0 ? pt_wuclean_image_scan_db_count_filtered_results( $run_id, $search_term, $filters ) : 0 );
    // Results per page (search form, view-related links, pagination).
    $settings = get_option( 'pt_wuclean_image_settings', array() );
    $per_page = ( isset( $_GET['per_page'] ) ? max( 25, min( 200, intval( $_GET['per_page'] ) ) ) : (( isset( $settings['results_per_page'] ) ? max( 25, min( 200, intval( $settings['results_per_page'] ) ) ) : 25 )) );
    $results_query_preserve = pt_wuclean_image_results_build_preserve_query_args(
        $orphan_filter_value,
        $per_page,
        $show_uploads_folder_filters,
        $hide_safe_siblings,
        $hide_thumbnails,
        $hide_originals_of_scaled,
        $hide_jpg,
        $hide_png,
        $hide_webp,
        $hide_avif
    );
    // Display search+filter controls when we have results, when a search was performed,
    // or when any filters are active (so users can remove filters even if results are 0).
    $has_filters = $hide_orphaned || $hide_safe_siblings || $hide_jpg || $hide_png || $hide_webp || $hide_avif || $hide_thumbnails || $hide_originals_of_scaled || $show_orphans_only;
    if ( 0 < $total_results || !empty( $search_term ) || $has_filters ) {
        echo '<div style="margin-bottom: 20px;">';
        echo '<form method="get" action="' . esc_url( admin_url( 'admin.php' ) ) . '" style="display: inline-block;">';
        echo '<input type="hidden" name="page" value="wuclean-images">';
        echo '<input type="hidden" name="tab" value="results">';
        echo '<input type="hidden" name="_wpnonce" value="' . esc_attr( wp_create_nonce( 'wuclean-images-search' ) ) . '">';
        foreach ( $results_query_preserve as $preserve_key => $preserve_value ) {
            if ( in_array( $preserve_key, array('page', 'tab'), true ) ) {
                continue;
            }
            echo '<input type="hidden" name="' . esc_attr( $preserve_key ) . '" value="' . esc_attr( (string) $preserve_value ) . '">';
        }
        echo '<input type="text" name="search" value="' . esc_attr( $search_term ) . '" placeholder="' . esc_attr__( 'Search images...', 'wuclean-database-cleaner-for-woocommerce' ) . '" style="width: 250px;">';
        echo '<button type="submit" class="button">' . esc_html__( 'Search', 'wuclean-database-cleaner-for-woocommerce' ) . '</button>';
        if ( !empty( $search_term ) ) {
            $clear_url = add_query_arg( array_merge( $results_query_preserve, array(
                '_wpnonce' => wp_create_nonce( 'wuclean-images-tab' ),
            ) ), admin_url( 'admin.php' ) );
            echo ' <a href="' . esc_url( $clear_url ) . '" class="button">' . esc_html__( 'Clear', 'wuclean-database-cleaner-for-woocommerce' ) . '</a> ';
        }
        echo '</form>';
        echo '</div>';
        // Filter controls - set checkbox states from URL parameters (placed below search for better UX).
        echo '<div style="background: #f9f9f9; border: 1px solid #ddd; border-radius: 4px; padding: 12px; margin-bottom: 15px;">';
        // Radio / view mode for orphan results.
        echo '<div style="margin-bottom: 10px;">';
        $printed_premium_orphan_radios = false;
        if ( !$printed_premium_orphan_radios ) {
            echo '<p style="margin: 0; color: #555;">' . esc_html__( 'Showing Media Library results.', 'wuclean-database-cleaner-for-woocommerce' ) . '</p>';
        }
        echo '</div>';
        if ( $show_uploads_folder_filters ) {
            // Checkbox: hide on-disk originals when scaled attachment appears in use (default on; disabled for Media Library-only radio — filter N/A there).
            echo '<label style="margin-right: 15px; cursor: pointer;"><input type="checkbox" id="pt-wuclean-filter-hide-scaled-originals" style="margin: 0 5px 0 0;"' . checked( $hide_originals_of_scaled, 1, false ) . (( $hide_orphaned ? ' disabled' : '' )) . '> ' . esc_html__( 'Hide originals of used scaled images', 'wuclean-database-cleaner-for-woocommerce' ) . '</label>';
            echo '<label style="margin-right: 15px; cursor: pointer;"><input type="checkbox" id="pt-wuclean-filter-safe-siblings" style="margin: 0 5px 0 0;"' . checked( $hide_safe_siblings, 1, false ) . '> ' . esc_html__( 'Hide siblings of used images', 'wuclean-database-cleaner-for-woocommerce' ) . '</label>';
            echo '<label style="margin-right: 15px; cursor: pointer;"><input type="checkbox" id="pt-wuclean-filter-thumbnails" style="margin: 0 5px 0 0;"' . checked( $hide_thumbnails, 1, false ) . '> ' . esc_html__( 'Hide thumbnails', 'wuclean-database-cleaner-for-woocommerce' ) . '</label>';
        }
        echo '<label style="margin-right: 15px; cursor: pointer;"><input type="checkbox" id="pt-wuclean-filter-jpg" style="margin: 0 5px 0 0;"' . checked( $hide_jpg, 1, false ) . '> ' . esc_html__( 'Hide jpg', 'wuclean-database-cleaner-for-woocommerce' ) . '</label>';
        echo '<label style="margin-right: 15px; cursor: pointer;"><input type="checkbox" id="pt-wuclean-filter-png" style="margin: 0 5px 0 0;"' . checked( $hide_png, 1, false ) . '> ' . esc_html__( 'Hide png', 'wuclean-database-cleaner-for-woocommerce' ) . '</label>';
        echo '<label style="margin-right: 15px; cursor: pointer;"><input type="checkbox" id="pt-wuclean-filter-webp" style="margin: 0 5px 0 0;"' . checked( $hide_webp, 1, false ) . '> ' . esc_html__( 'Hide webp', 'wuclean-database-cleaner-for-woocommerce' ) . '</label>';
        echo '<label style="margin-right: 15px; cursor: pointer;"><input type="checkbox" id="pt-wuclean-filter-avif" style="margin: 0 5px 0 0;"' . checked( $hide_avif, 1, false ) . '> ' . esc_html__( 'Hide avif', 'wuclean-database-cleaner-for-woocommerce' ) . '</label>';
        echo '</div>';
    }
    // Check if search term might be looking for a parent image that's in use.
    $parent_info = null;
    if ( !empty( $search_term ) ) {
        global $wpdb;
        // Build potential parent filenames from search term.
        $potential_parents = array(
            $search_term . '.jpg',
            $search_term . '.jpeg',
            $search_term . '.png',
            $search_term . '.gif',
            $search_term . '.webp',
            $search_term . '.avif'
        );
        // Check if any of these exist in Media Library.
        foreach ( $potential_parents as $parent_filename ) {
            $attachment_id = $wpdb->get_var( $wpdb->prepare( "SELECT pm.post_id FROM {$wpdb->postmeta} pm\n                 INNER JOIN {$wpdb->posts} p ON pm.post_id = p.ID\n                 WHERE pm.meta_key = '_wp_attached_file' \n                 AND pm.meta_value LIKE %s\n                 AND p.post_type = 'attachment'\n                 AND p.post_status = 'inherit'\n                 LIMIT 1", '%' . $wpdb->esc_like( $parent_filename ) ) );
            if ( $attachment_id ) {
                // Check if this parent is actually used (that's why it's not in scan results).
                $usage_check = pt_wuclean_is_attachment_used( $attachment_id );
                if ( isset( $usage_check['used'] ) && $usage_check['used'] ) {
                    $parent_info = array(
                        'id'       => $attachment_id,
                        'filename' => $parent_filename,
                        'reason'   => ( isset( $usage_check['reason'] ) ? $usage_check['reason'] : __( 'In use', 'wuclean-database-cleaner-for-woocommerce' ) ),
                    );
                    break;
                }
            }
        }
    }
    // Scan results are now stored in the DB; filtering/sorting is handled in SQL.
    $scan_results = array();
    // Pagination.
    $current_page = 1;
    if ( isset( $_GET['paged'] ) ) {
        // Verify nonce if provided (accept either pagination or search nonce), otherwise allow if user has capability.
        $nonce_valid = false;
        if ( isset( $_GET['_wpnonce'] ) ) {
            $nonce = sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) );
            $nonce_valid = wp_verify_nonce( $nonce, 'wuclean-images-pagination' ) || wp_verify_nonce( $nonce, 'wuclean-images-search' );
        }
        if ( $nonce_valid || current_user_can( pt_wuclean_get_required_capability() ) ) {
            // Allow if nonce is valid or user has capability (for backward compatibility).
            $current_page = max( 1, intval( $_GET['paged'] ) );
        }
    }
    $total_pages = ( $total_results > 0 ? ceil( $total_results / $per_page ) : 1 );
    $offset = ($current_page - 1) * $per_page;
    $paginated_results = ( $run_id > 0 ? pt_wuclean_image_scan_db_get_filtered_results_page(
        $run_id,
        $search_term,
        $filters,
        $per_page,
        $offset
    ) : array() );
    if ( $total_results <= 0 ) {
        echo '<div style="background: #fff; border: 1px solid #ccd0d4; border-radius: 4px; padding: 40px; text-align: center; margin: 20px 0;">';
        if ( !empty( $search_term ) ) {
            // Check if we found a parent image that's in use.
            if ( $parent_info ) {
                echo '<div style="background: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin-bottom: 20px; text-align: left; border-radius: 4px;">';
                echo '<strong style="color: #856404;">⚠️ ' . esc_html__( 'Parent Image Information', 'wuclean-database-cleaner-for-woocommerce' ) . '</strong><br><br>';
                echo esc_html__( 'The parent image was found but is currently in use:', 'wuclean-database-cleaner-for-woocommerce' ) . '<br><br>';
                echo '📷 <strong>' . esc_html( $parent_info['filename'] ) . '</strong> (ID: ' . esc_html( $parent_info['id'] ) . ')<br>';
                echo '<em style="color: #666;">' . esc_html( $parent_info['reason'] ) . '</em><br><br>';
                echo '<span style="color: #856404;">💡 ' . esc_html__( 'Since this image is in use, it is not shown in the scan results list.', 'wuclean-database-cleaner-for-woocommerce' ) . '</span>';
                echo '</div>';
            }
            echo '<p style="font-size: 16px; color: #666;">' . esc_html__( 'No unused images found matching your search criteria.', 'wuclean-database-cleaner-for-woocommerce' ) . '</p>';
            $clear_search_url = add_query_arg( array_merge( $results_query_preserve, array(
                '_wpnonce' => wp_create_nonce( 'wuclean-images-tab' ),
            ) ), admin_url( 'admin.php' ) );
            echo '<a href="' . esc_url( $clear_search_url ) . '" class="button">' . esc_html__( 'Clear Search', 'wuclean-database-cleaner-for-woocommerce' ) . '</a>';
        } else {
            echo '<p style="font-size: 16px; color: #666;">' . esc_html__( 'No results. Clear search, filters or run a scan from the settings tab.', 'wuclean-database-cleaner-for-woocommerce' ) . '</p>';
            echo '<a href="?page=wuclean-images&tab=settings" class="button button-primary">' . esc_html__( 'Go to Settings', 'wuclean-database-cleaner-for-woocommerce' ) . '</a>';
        }
        echo '</div>';
        echo '</div>';
        return;
    }
    // Show parent info box if searching and parent is in use.
    if ( !empty( $search_term ) && $parent_info ) {
        echo '<div style="background: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 20px 0; border-radius: 4px;">';
        echo '<strong style="color: #856404;">⚠️ ' . esc_html__( 'Parent Image Information', 'wuclean-database-cleaner-for-woocommerce' ) . '</strong><br><br>';
        echo sprintf( 
            /* translators: %1$s: parent image filename, %2$d: parent image ID. */
            esc_html__( 'The parent image %1$s (ID: %2$d) is currently in use and not shown in results:', 'wuclean-database-cleaner-for-woocommerce' ),
            '<strong>' . esc_html( $parent_info['filename'] ) . '</strong>',
            absint( $parent_info['id'] )
         ) . '<br>';
        echo '<em style="color: #666;">' . esc_html( $parent_info['reason'] ) . '</em><br><br>';
        echo '<span style="color: #856404;">💡 ' . esc_html__( 'The related files shown below may be siblings or cropped versions of this parent image.', 'wuclean-database-cleaner-for-woocommerce' ) . '</span>';
        echo '</div>';
    }
    // Results per page selector and pagination info.
    $start_item = $offset + 1;
    $end_item = min( $offset + $per_page, $total_results );
    echo '<div style="margin-bottom: 10px; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 15px;">';
    // Pagination info - "Showing X-Y of Z images" (left-aligned).
    if ( !empty( $search_term ) && $has_filters ) {
        /* translators: 1: first item index, 2: last item index, 3: total results */
        $template = __( 'Showing %1$d-%2$d of %3$d images (filtered by search and filters)', 'wuclean-database-cleaner-for-woocommerce' );
    } elseif ( !empty( $search_term ) ) {
        /* translators: 1: first item index, 2: last item index, 3: total results */
        $template = __( 'Showing %1$d-%2$d of %3$d images (filtered by search)', 'wuclean-database-cleaner-for-woocommerce' );
    } elseif ( $has_filters ) {
        /* translators: 1: first item index, 2: last item index, 3: total results */
        $template = __( 'Showing %1$d-%2$d of %3$d images (filtered)', 'wuclean-database-cleaner-for-woocommerce' );
    } else {
        /* translators: 1: first item index, 2: last item index, 3: total results */
        $template = __( 'Showing %1$d-%2$d of %3$d images', 'wuclean-database-cleaner-for-woocommerce' );
    }
    /* translators: 1: first item index, 2: last item index, 3: total results */
    echo '<span class="pt-wuclean-pagination" data-template="' . esc_attr( $template ) . '" data-start="' . esc_attr( $start_item ) . '" data-end="' . esc_attr( $end_item ) . '" data-total="' . esc_attr( $total_results ) . '">' . esc_html( sprintf(
        $template,
        $start_item,
        $end_item,
        $total_results
    ) ) . '</span>';
    // Results per page selector (right-aligned).
    echo '<div style="display: flex; align-items: center; gap: 8px;">';
    echo '<label for="pt-wuclean-results-per-page">' . esc_html__( 'Results per page:', 'wuclean-database-cleaner-for-woocommerce' ) . '</label>';
    echo '<select id="pt-wuclean-results-per-page" style="padding: 4px 24px 4px 8px; height: 28px; min-width: 70px;">';
    echo '<option value="25"' . selected( $per_page, 25, false ) . '>25</option>';
    echo '<option value="50"' . selected( $per_page, 50, false ) . '>50</option>';
    echo '<option value="100"' . selected( $per_page, 100, false ) . '>100</option>';
    echo '<option value="200"' . selected( $per_page, 200, false ) . '>200</option>';
    echo '</select>';
    echo '</div>';
    echo '</div>';
    // Results table.
    echo '<div style="max-width: 1600px;background: #fff; border: 1px solid #ccd0d4; border-radius: 4px; padding: 20px; margin: 20px 0;">';
    echo '<table class="wp-list-table widefat striped pt-wuclean-image-results-table">';
    echo '<colgroup>';
    echo '<col class="pt-wuclean-col-preview">';
    echo '<col class="pt-wuclean-col-image-info">';
    echo '<col class="pt-wuclean-col-issue">';
    echo '<col class="pt-wuclean-col-actions">';
    echo '</colgroup>';
    echo '<thead><tr>';
    echo '<th class="pt-wuclean-col-preview">' . esc_html__( 'Preview', 'wuclean-database-cleaner-for-woocommerce' ) . '</th>';
    echo '<th class="pt-wuclean-col-image-info">' . esc_html__( 'Image Info', 'wuclean-database-cleaner-for-woocommerce' ) . '</th>';
    echo '<th class="pt-wuclean-col-issue">' . esc_html__( 'Issue', 'wuclean-database-cleaner-for-woocommerce' ) . '</th>';
    echo '<th class="pt-wuclean-col-actions">' . esc_html__( 'Actions', 'wuclean-database-cleaner-for-woocommerce' ) . '</th>';
    echo '</tr></thead>';
    echo '<tbody>';
    foreach ( $paginated_results as $item ) {
        $item_id = ( isset( $item['id'] ) ? $item['id'] : 0 );
        $item_type = ( isset( $item['type'] ) ? $item['type'] : 'unknown' );
        $item_subtype = ( isset( $item['subtype'] ) ? $item['subtype'] : '' );
        $item_data = ( isset( $item['data'] ) ? $item['data'] : array() );
        // Build data attributes for filtering.
        $data_attrs = '';
        // Add filename attribute for Media Library items (so siblings can be removed).
        if ( 'media_library' === $item_type && 0 < $item_id ) {
            $file_path = get_attached_file( $item_id );
            if ( $file_path ) {
                $data_attrs .= ' data-filename="' . esc_attr( basename( $file_path ) ) . '"';
            }
        }
        if ( 'orphaned_file' === $item_type ) {
            $file_type = ( isset( $item_data['file_type'] ) ? strtolower( $item_data['file_type'] ) : '' );
            $is_safe_sibling = 'sibling' === $item_subtype && isset( $item_data['sibling_used'] ) && $item_data['sibling_used'];
            // Store the orphan's filename on the row so front-end JS can remove related rows immediately after actions.
            if ( isset( $item_data['filename'] ) && '' !== $item_data['filename'] ) {
                $data_attrs .= ' data-filename="' . esc_attr( $item_data['filename'] ) . '"';
            }
            if ( $file_type ) {
                $data_attrs .= ' data-file-type="' . esc_attr( $file_type ) . '"';
            }
            if ( $is_safe_sibling ) {
                $data_attrs .= ' data-safe-sibling="1"';
            }
            // Add parent filename for sibling and scaled_original rows (so we can remove them when parent is acted upon).
            if ( in_array( $item_subtype, array('sibling', 'scaled_original'), true ) && isset( $item_data['sibling_filename'] ) ) {
                $data_attrs .= ' data-parent-filename="' . esc_attr( $item_data['sibling_filename'] ) . '"';
            }
            // Media Library attachment ID for sibling rows (JS removes these when that attachment is trashed/deleted/protected).
            if ( isset( $item_data['sibling_id'] ) && (int) $item_data['sibling_id'] > 0 ) {
                $data_attrs .= ' data-sibling-id="' . esc_attr( (string) (int) $item_data['sibling_id'] ) . '"';
            }
        }
        // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Already escaped via esc_attr()
        echo '<tr' . $data_attrs . '>';
        // Preview.
        echo '<td class="pt-wuclean-col-preview">';
        if ( 'media_library' === $item_type && $item_id ) {
            $preview_url = pt_wuclean_get_safe_attachment_preview_url( $item_id, 'thumbnail' );
            if ( $preview_url ) {
                echo '<img src="' . esc_url( $preview_url ) . '" style="max-width: 60px; max-height: 60px; object-fit: contain; border-radius: 4px; background: #f0f0f0;" onerror="this.style.display=\'none\'; this.nextElementSibling.style.display=\'flex\';">';
                echo '<div style="display: none; width: 60px; height: 60px; background: #f0f0f0; border-radius: 4px; align-items: center; justify-content: center;">🖼️</div>';
            } else {
                echo '<div style="width: 60px; height: 60px; background: #f0f0f0; border-radius: 4px; display: flex; align-items: center; justify-content: center;">📄</div>';
            }
        } else {
            // Orphaned file - try to show thumbnail if it's an image.
            // Check for file_path in both new and old structures.
            $file_path = ( isset( $item['file_path'] ) ? $item['file_path'] : (( isset( $item_data['path'] ) ? $item_data['path'] : '' )) );
            $upload_dir = wp_upload_dir();
            $upload_base_url = ( isset( $upload_dir['baseurl'] ) ? (string) $upload_dir['baseurl'] : '' );
            $upload_base_path = ( isset( $upload_dir['basedir'] ) ? (string) $upload_dir['basedir'] : '' );
            // Normalize paths for comparison (convert backslashes to forward slashes).
            $file_path_normalized = str_replace( '\\', '/', (string) $file_path );
            $upload_base_path_normalized = str_replace( '\\', '/', $upload_base_path );
            if ( $file_path && false !== strpos( $file_path_normalized, $upload_base_path_normalized ) ) {
                // File is in uploads directory - create URL.
                $relative_path = str_replace( $upload_base_path_normalized, '', $file_path_normalized );
                // Ensure relative path starts with /.
                if ( '/' !== substr( $relative_path, 0, 1 ) ) {
                    $relative_path = '/' . $relative_path;
                }
                $file_url = $upload_base_url . $relative_path;
                // Check if file is an image by extension.
                $ext = strtolower( pathinfo( $file_path, PATHINFO_EXTENSION ) );
                $image_exts = array(
                    'jpg',
                    'jpeg',
                    'png',
                    'gif',
                    'webp',
                    'svg',
                    'bmp',
                    'ico',
                    'avif'
                );
                if ( in_array( $ext, $image_exts, true ) && file_exists( $file_path ) ) {
                    // Display image thumbnail.
                    echo '<img src="' . esc_url( $file_url ) . '" style="max-width: 60px; max-height: 60px; object-fit: contain; border-radius: 4px; background: #f0f0f0;" onerror="this.style.display=\'none\'; this.nextElementSibling.style.display=\'flex\';">';
                    echo '<div style="display: none; width: 60px; height: 60px; background: #f0f0f0; border-radius: 4px; align-items: center; justify-content: center;">🖼️</div>';
                } else {
                    // Not an image, show document icon.
                    echo '<div style="width: 60px; height: 60px; background: #f0f0f0; border-radius: 4px; display: flex; align-items: center; justify-content: center;">📄</div>';
                }
            } else {
                // Unknown file, show document icon.
                echo '<div style="width: 60px; height: 60px; background: #f0f0f0; border-radius: 4px; display: flex; align-items: center; justify-content: center;">📄</div>';
            }
        }
        echo '</td>';
        // Info.
        echo '<td class="pt-wuclean-col-image-info"><div class="pt-wuclean-image-info-cell">';
        if ( 'media_library' === $item_type ) {
            $title = get_the_title( $item_id );
            $file_path = get_attached_file( $item_id );
            $filename = basename( $file_path );
            $upload_date = get_the_date( 'Y-m-d', $item_id );
            $file_size = 'N/A';
            if ( $file_path && file_exists( $file_path ) ) {
                $file_size = size_format( filesize( $file_path ) );
            }
            echo '<strong>' . esc_html( $title ) . '</strong><br>';
            echo '<small style="color: #666;">';
            echo esc_html__( 'ID:', 'wuclean-database-cleaner-for-woocommerce' ) . ' ' . esc_html( $item_id ) . '<br>';
            echo esc_html__( 'File:', 'wuclean-database-cleaner-for-woocommerce' ) . ' ' . esc_html( $filename ) . '<br>';
            echo esc_html__( 'Uploaded:', 'wuclean-database-cleaner-for-woocommerce' ) . ' ' . esc_html( $upload_date ) . ' | ';
            echo esc_html__( 'Size:', 'wuclean-database-cleaner-for-woocommerce' ) . ' ' . esc_html( $file_size );
            echo '</small>';
            // Show siblings notice; "View related" is output below (scaled-original path preferred, else sibling stem).
            if ( isset( $item_data['siblings'] ) && !empty( $item_data['siblings'] ) ) {
                echo '<br><small style="color: #d63638;">⚠️ ' . esc_html__( 'Has WebP/AVIF siblings', 'wuclean-database-cleaner-for-woocommerce' ) . '</small>';
            }
            $printed_view_related_ml = false;
            // Prefer WordPress big-image original stem when attached file differs from stored original.
            if ( $is_full_scan && function_exists( 'wp_get_original_image_path' ) ) {
                $original_path = wp_get_original_image_path( $item_id );
                $attached_file = get_attached_file( $item_id );
                if ( $original_path && $attached_file && $attached_file !== $original_path ) {
                    $original_filename = basename( $original_path );
                    $base_filename = pathinfo( $original_filename, PATHINFO_FILENAME );
                    $base_filename = str_replace( '-scaled', '', $base_filename );
                    $search_url = add_query_arg( array_merge( $results_query_preserve, array(
                        'search'   => $base_filename,
                        '_wpnonce' => wp_create_nonce( 'wuclean-images-search' ),
                    ) ), admin_url( 'admin.php' ) );
                    echo '<br><a href="' . esc_url( $search_url ) . '" class="pt-wuclean-results-search-link" style="color: #2271b1; text-decoration: none;">🔗 ' . esc_html__( 'View related', 'wuclean-database-cleaner-for-woocommerce' ) . '</a>';
                    $printed_view_related_ml = true;
                }
            }
            // Siblings but not a scaled-vs-original row: still offer one related search (same stem rule as orphans).
            if ( $is_full_scan && !$printed_view_related_ml && isset( $item_data['siblings'] ) && !empty( $item_data['siblings'] ) && is_string( $filename ) && '' !== $filename ) {
                $base_filename = pathinfo( $filename, PATHINFO_FILENAME );
                $base_filename = str_replace( '-scaled', '', $base_filename );
                $search_url = add_query_arg( array_merge( $results_query_preserve, array(
                    'search'   => $base_filename,
                    '_wpnonce' => wp_create_nonce( 'wuclean-images-search' ),
                ) ), admin_url( 'admin.php' ) );
                echo '<br><a href="' . esc_url( $search_url ) . '" class="pt-wuclean-results-search-link" style="color: #2271b1; text-decoration: none;">🔗 ' . esc_html__( 'View related', 'wuclean-database-cleaner-for-woocommerce' ) . '</a>';
            }
        } else {
            // Orphaned file (not in Media Library).
            $orphan_filename = ( isset( $item['filename'] ) ? $item['filename'] : (( isset( $item_data['filename'] ) ? $item_data['filename'] : 'Unknown' )) );
            $orphan_size = ( isset( $item_data['size'] ) ? size_format( $item_data['size'] ) : 'N/A' );
            $file_type = ( isset( $item_data['file_type'] ) ? $item_data['file_type'] : '' );
            // Add badge for sibling files, scaled originals, and thumbnails.
            $badge = '';
            if ( 'sibling' === $item_subtype && isset( $item_data['sibling_used'] ) ) {
                if ( $item_data['sibling_used'] ) {
                    $badge = '<span style="background: #ffc107; color: #856404; padding: 2px 6px; border-radius: 3px; font-size: 11px; font-weight: bold; margin-left: 5px;">🌐 ' . esc_html( $file_type ) . ' ' . esc_html__( 'SIBLING', 'wuclean-database-cleaner-for-woocommerce' ) . '</span>';
                } else {
                    $badge = '<span style="background: #e7f3ff; color: #0c5460; padding: 2px 6px; border-radius: 3px; font-size: 11px; font-weight: bold; margin-left: 5px;">🌐 ' . esc_html( $file_type ) . '</span>';
                }
            } elseif ( 'thumbnail' === $item_subtype || 'thumbnail_no_parent' === $item_subtype ) {
                $badge = '<span style="background: #e7f3ff; color: #0c5460; padding: 2px 6px; border-radius: 3px; font-size: 11px; font-weight: bold; margin-left: 5px;">🖼️ ' . esc_html__( 'THUMBNAIL', 'wuclean-database-cleaner-for-woocommerce' ) . '</span>';
            } elseif ( 'scaled_original' === $item_subtype && isset( $item_data['sibling_used'] ) ) {
                if ( $item_data['sibling_used'] ) {
                    $badge = '<span style="background: #ffc107; color: #856404; padding: 2px 6px; border-radius: 3px; font-size: 11px; font-weight: bold; margin-left: 5px;">📐 ' . esc_html__( 'ORIGINAL', 'wuclean-database-cleaner-for-woocommerce' ) . '</span>';
                } else {
                    $badge = '<span style="background: #e7f3ff; color: #0c5460; padding: 2px 6px; border-radius: 3px; font-size: 11px; font-weight: bold; margin-left: 5px;">📐 ' . esc_html__( 'ORIGINAL', 'wuclean-database-cleaner-for-woocommerce' ) . '</span>';
                }
            } elseif ( $file_type ) {
                $badge = '<span style="background: #e0e0e0; color: #333; padding: 2px 6px; border-radius: 3px; font-size: 11px; margin-left: 5px;">' . esc_html( $file_type ) . '</span>';
            }
            echo '<strong>' . esc_html( $orphan_filename ) . '<br>' . wp_kses_post( $badge ) . '</strong><br>';
            echo '<small style="color: #666;">';
            echo esc_html__( 'Not in Media Library', 'wuclean-database-cleaner-for-woocommerce' ) . '<br>';
            echo esc_html__( 'Size:', 'wuclean-database-cleaner-for-woocommerce' ) . ' ' . esc_html( $orphan_size );
            // Add "View related files" link for siblings, scaled originals, and thumbnails (search by image stem).
            $related_search_stem = '';
            if ( ('sibling' === $item_subtype || 'scaled_original' === $item_subtype) && isset( $item_data['sibling_filename'] ) && is_string( $item_data['sibling_filename'] ) ) {
                $related_search_stem = pathinfo( $item_data['sibling_filename'], PATHINFO_FILENAME );
                $related_search_stem = str_replace( '-scaled', '', (string) $related_search_stem );
            } elseif ( isset( $item_data['path'] ) && is_string( $item_data['path'] ) && function_exists( 'pt_wuclean_is_thumbnail_file' ) ) {
                $thumb_parent = pt_wuclean_is_thumbnail_file( $item_data['path'] );
                if ( is_string( $thumb_parent ) && '' !== $thumb_parent ) {
                    $related_search_stem = pathinfo( $thumb_parent, PATHINFO_FILENAME );
                    $related_search_stem = str_replace( '-scaled', '', (string) $related_search_stem );
                }
            }
            if ( '' === $related_search_stem && is_string( $orphan_filename ) && '' !== $orphan_filename ) {
                $fnstem = pathinfo( $orphan_filename, PATHINFO_FILENAME );
                if ( preg_match( '/^(.+)-\\d+x\\d+$/i', (string) $fnstem, $m ) && !empty( $m[1] ) ) {
                    $related_search_stem = str_replace( '-scaled', '', strtolower( $m[1] ) );
                }
            }
            if ( '' !== $related_search_stem ) {
                $search_url = add_query_arg( array_merge( $results_query_preserve, array(
                    'search'   => $related_search_stem,
                    '_wpnonce' => wp_create_nonce( 'wuclean-images-search' ),
                ) ), admin_url( 'admin.php' ) );
                echo '<br><a href="' . esc_url( $search_url ) . '" class="pt-wuclean-results-search-link" style="color: #2271b1; text-decoration: none;">🔗 ' . esc_html__( 'View related', 'wuclean-database-cleaner-for-woocommerce' ) . '</a>';
            }
            echo '</small>';
        }
        echo '</div>';
        echo '</td>';
        // Issue.
        echo '<td class="pt-wuclean-col-issue">';
        if ( 'media_library' === $item_type ) {
            // Media Library image.
            // Check for issue in both locations (backward compatibility).
            $issue_text = ( isset( $item['issue'] ) ? $item['issue'] : (( isset( $item_data['issue'] ) ? $item_data['issue'] : __( 'Media Library image not found in typical WooCommerce locations.', 'wuclean-database-cleaner-for-woocommerce' ) )) );
            echo '<div>' . esc_html( $issue_text ) . '</div>';
            // Check if this image has cropped children (cropped versions in use).
            if ( isset( $item_data['has_cropped_children'] ) && true === $item_data['has_cropped_children'] ) {
                echo '<div style="background: #fff3cd; border-left: 3px solid #ffc107; padding: 10px; margin-top: 8px; font-size: 12px; line-height: 1.6;">';
                echo '<strong style="color: #856404;">⚠️ ' . esc_html__( 'Cropped Version Warning', 'wuclean-database-cleaner-for-woocommerce' ) . '</strong><br>';
                echo esc_html__( 'This image has cropped versions that are currently in use (e.g., as site logo or custom header).', 'wuclean-database-cleaner-for-woocommerce' ) . '<br><br>';
                echo '<strong style="color: #d63638;">' . esc_html__( 'Important:', 'wuclean-database-cleaner-for-woocommerce' ) . '</strong> ' . esc_html__( 'Deleting this original image will prevent WordPress from regenerating thumbnails if you change themes, adjust image settings, or use image optimization plugins.', 'wuclean-database-cleaner-for-woocommerce' ) . '<br><br>';
                echo '<span style="color: #856404;"><strong>💡 ' . esc_html__( 'Recommendation: Keep this original image for future flexibility.', 'wuclean-database-cleaner-for-woocommerce' ) . '</strong></span>';
                echo '</div>';
            }
        } else {
            // Orphaned file - check for sibling relationship.
            $subtype = ( isset( $item['subtype'] ) ? $item['subtype'] : '' );
            // Check for issue in both locations (backward compatibility).
            $issue_text = ( isset( $item['issue'] ) ? $item['issue'] : (( isset( $item_data['issue'] ) ? $item_data['issue'] : __( 'Found in uploads folder but not in Media Library.', 'wuclean-database-cleaner-for-woocommerce' ) )) );
            echo '<div>' . esc_html( $issue_text ) . '</div>';
            // Display parent folder path to help identify which plugin might be using the file.
            $file_path = ( isset( $item_data['path'] ) ? (string) $item_data['path'] : '' );
            if ( !empty( $file_path ) ) {
                $upload_dir = wp_upload_dir();
                $uploads_basedir = ( isset( $upload_dir['basedir'] ) && is_string( $upload_dir['basedir'] ) ? wp_normalize_path( (string) $upload_dir['basedir'] ) : '' );
                $normalized_path = wp_normalize_path( (string) $file_path );
                if ( !empty( $uploads_basedir ) && 0 === strpos( $normalized_path, $uploads_basedir ) ) {
                    // Get relative path from uploads base directory.
                    $relative_path = str_replace( $uploads_basedir . '/', '', $normalized_path );
                    $path_parts = explode( '/', $relative_path );
                    // Show parent folder structure (e.g., "gravity_forms/19-098ed56c6b030c8fbed5169ab95498f8/").
                    if ( count( $path_parts ) > 1 ) {
                        $parent_folder = $path_parts[0] . '/' . (( isset( $path_parts[1] ) ? $path_parts[1] . '/' : '' ));
                        echo '<div style="margin-top: 4px; font-size: 12px; color: #666;">';
                        /* translators: %s: folder path. */
                        echo '<strong>' . esc_html__( 'Location:', 'wuclean-database-cleaner-for-woocommerce' ) . '</strong> <code>uploads/' . esc_html( $parent_folder ) . '</code>';
                        echo '</div>';
                    }
                }
            }
            if ( ('sibling' === $subtype || 'scaled_original' === $subtype || 'thumbnail' === $subtype) && isset( $item_data['sibling_id'] ) ) {
                // Show parent relationship (for WebP/AVIF siblings, scaled originals, or thumbnails).
                $parent_id = $item_data['sibling_id'];
                $parent_filename = $item_data['sibling_filename'];
                $sibling_used = ( isset( $item_data['sibling_used'] ) ? $item_data['sibling_used'] : false );
                $recommendation = ( isset( $item_data['recommendation'] ) ? $item_data['recommendation'] : 'review' );
                $file_type = ( isset( $item_data['file_type'] ) ? $item_data['file_type'] : (( 'scaled_original' === $subtype ? 'Original' : (( 'thumbnail' === $subtype ? 'THUMBNAIL' : 'WebP/AVIF' )) )) );
                if ( 'thumbnail' === $subtype ) {
                    $relationship_label = esc_html__( 'parent image detected', 'wuclean-database-cleaner-for-woocommerce' );
                } elseif ( 'scaled_original' === $subtype ) {
                    $relationship_label = esc_html__( 'scaled version detected', 'wuclean-database-cleaner-for-woocommerce' );
                } else {
                    $relationship_label = esc_html__( 'sibling detected', 'wuclean-database-cleaner-for-woocommerce' );
                }
                if ( $sibling_used ) {
                    // Parent/scaled version is in use - CRITICAL to keep.
                    $usage_reason = ( isset( $item_data['usage_reason'] ) ? $item_data['usage_reason'] : '' );
                    $usage_text = '';
                    switch ( $usage_reason ) {
                        case 'in_post_content':
                            $usage_text = esc_html__( 'Referenced in post/page content', 'wuclean-database-cleaner-for-woocommerce' );
                            break;
                        case 'in_post_content_resized':
                            $usage_text = esc_html__( 'Resized version referenced in post/page content', 'wuclean-database-cleaner-for-woocommerce' );
                            break;
                        case 'featured_image':
                            $usage_text = esc_html__( 'Used as featured image', 'wuclean-database-cleaner-for-woocommerce' );
                            break;
                        case 'product_gallery':
                            $usage_text = esc_html__( 'In product gallery', 'wuclean-database-cleaner-for-woocommerce' );
                            break;
                        case 'term_thumbnail':
                            $usage_text = esc_html__( 'Used as category/term thumbnail', 'wuclean-database-cleaner-for-woocommerce' );
                            break;
                        default:
                            $usage_text = esc_html__( 'In use', 'wuclean-database-cleaner-for-woocommerce' );
                    }
                    echo '<div style="background: #fff3cd; border-left: 3px solid #ffc107; padding: 8px; margin-top: 8px; font-size: 12px; line-height: 1.5;">';
                    if ( 'scaled_original' === $subtype ) {
                        echo '<strong style="color: #856404;">⚠️ ' . esc_html__( 'WordPress scaled image original', 'wuclean-database-cleaner-for-woocommerce' ) . '</strong><br>';
                        echo esc_html__( 'The scaled version of this image is in use:', 'wuclean-database-cleaner-for-woocommerce' ) . '<br>';
                    } elseif ( 'thumbnail' === $subtype ) {
                        echo '<strong style="color: #856404;">⚠️ ' . esc_html__( 'WordPress thumbnail/resized version', 'wuclean-database-cleaner-for-woocommerce' ) . '</strong><br>';
                        echo esc_html__( 'The parent image is in use:', 'wuclean-database-cleaner-for-woocommerce' ) . '<br>';
                    } else {
                        // Check if this is a thumbnail sibling.
                        $is_thumbnail_sibling = ( isset( $item_data['is_thumbnail_sibling'] ) ? $item_data['is_thumbnail_sibling'] : false );
                        if ( $is_thumbnail_sibling ) {
                            echo '<strong style="color: #856404;">⚠️ ' . esc_html__( 'WebP/AVIF of thumbnail/resized version', 'wuclean-database-cleaner-for-woocommerce' ) . '</strong><br>';
                            echo esc_html__( 'The thumbnail version is in use:', 'wuclean-database-cleaner-for-woocommerce' ) . '<br>';
                        } else {
                            echo '<strong style="color: #856404;">⚠️ ' . esc_html( $file_type ) . ' ' . esc_html( $relationship_label ) . '</strong><br>';
                            echo esc_html__( 'A potentially related image was detected that is in use:', 'wuclean-database-cleaner-for-woocommerce' ) . '<br>';
                        }
                    }
                    echo '<strong>📷 ' . esc_html( $parent_filename ) . '</strong> <small>(ID: ' . esc_html( $parent_id ) . ')</small><br>';
                    // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Already escaped via esc_html__()
                    echo '<small style="color: #666;">(' . $usage_text . ')</small><br>';
                    if ( 'scaled_original' === $subtype && isset( $item_data['note'] ) ) {
                        echo '<small style="color: #666;">' . esc_html( $item_data['note'] ) . '</small><br>';
                    }
                    echo '<span style="color: #856404;"><strong>⚠️ ' . esc_html__( 'Recommendation: Keep this file.', 'wuclean-database-cleaner-for-woocommerce' ) . '</strong></span>';
                    echo '</div>';
                } else {
                    // Parent/scaled version also unused - check if protected.
                    $protected_images = pt_wuclean_get_protected_images();
                    $is_protected = in_array( $parent_id, $protected_images, true );
                    // Recalculate current trash status for parent on render (results may be viewed after parent was deleted/restored).
                    $parent_in_trash = false;
                    if ( $parent_id > 0 ) {
                        $parent_trash = get_post_meta( $parent_id, '_pt_wuclean_trash_path', true );
                        if ( empty( $parent_trash ) ) {
                            $parent_trash = get_post_meta( $parent_id, '_wuclean_trash_path', true );
                        }
                        if ( !empty( $parent_trash ) ) {
                            $parent_in_trash = true;
                        } else {
                            $parent_status = get_post_status( $parent_id );
                            if ( 'trash' === $parent_status ) {
                                $parent_in_trash = true;
                            }
                        }
                    }
                    // Special case: scaled original where the scaled image is currently in Wuclean trash.
                    if ( 'scaled_original' === $subtype && !empty( $item_data['scaled_in_trash'] ) ) {
                        echo '<div style="background: #fff3cd; border-left: 3px solid #ffc107; padding: 8px; margin-top: 8px; font-size: 12px; line-height: 1.5;">';
                        echo '<strong style="color: #856404;">⚠️ ' . esc_html__( 'WordPress scaled image original', 'wuclean-database-cleaner-for-woocommerce' ) . '</strong><br>';
                        echo esc_html__( 'Scaled version in Wuclean trash.', 'wuclean-database-cleaner-for-woocommerce' ) . '<br>';
                        echo '<strong>📷 ' . esc_html( $parent_filename ) . '</strong> <small>(ID: ' . esc_html( $parent_id ) . ')</small><br>';
                        echo '<small style="color: #666;">' . esc_html__( 'If the trashed scaled image is restored, this original may be needed to regenerate thumbnails.', 'wuclean-database-cleaner-for-woocommerce' ) . '</small><br>';
                        echo '<span style="color: #856404;"><strong>⚠️ ' . esc_html__( 'Recommendation: Keep this file.', 'wuclean-database-cleaner-for-woocommerce' ) . '</strong></span>';
                        echo '</div>';
                    } elseif ( 'sibling' === $subtype && $parent_in_trash ) {
                        // WebP/AVIF sibling where the parent image is currently in Wuclean trash.
                        echo '<div style="background: #fff3cd; border-left: 3px solid #ffc107; padding: 8px; margin-top: 8px; font-size: 12px; line-height: 1.5;">';
                        echo '<strong style="color: #856404;">💡 ' . esc_html__( 'Parent image in Wuclean trash', 'wuclean-database-cleaner-for-woocommerce' ) . '</strong><br>';
                        echo esc_html__( 'This WebP/AVIF file is related to an image currently in the Wuclean trash directory.', 'wuclean-database-cleaner-for-woocommerce' ) . '<br>';
                        echo '<strong>📷 ' . esc_html( $parent_filename ) . '</strong> <small>(ID: ' . esc_html( $parent_id ) . ')</small><br>';
                        echo '<small style="color: #666;">' . esc_html__( 'If you plan to restore the parent image, you may want to keep this optimized copy; otherwise it is usually safe to delete.', 'wuclean-database-cleaner-for-woocommerce' ) . '</small><br>';
                        echo '</div>';
                    } elseif ( $is_protected ) {
                        // Parent/scaled version is protected.
                        echo '<div style="background: #d4edda; border-left: 3px solid #28a745; padding: 8px; margin-top: 8px; font-size: 12px; line-height: 1.5;">';
                        if ( 'scaled_original' === $subtype ) {
                            echo '<strong style="color: #155724;">🛡️ ' . esc_html__( 'WordPress scaled image original', 'wuclean-database-cleaner-for-woocommerce' ) . '</strong><br>';
                            echo esc_html__( 'The scaled version of this image was detected:', 'wuclean-database-cleaner-for-woocommerce' ) . '<br>';
                        } elseif ( 'thumbnail' === $subtype ) {
                            echo '<strong style="color: #155724;">🛡️ ' . esc_html__( 'WordPress thumbnail/resized version', 'wuclean-database-cleaner-for-woocommerce' ) . '</strong><br>';
                            echo esc_html__( 'The parent image was detected:', 'wuclean-database-cleaner-for-woocommerce' ) . '<br>';
                        } else {
                            // Check if this is a thumbnail sibling.
                            $is_thumbnail_sibling = ( isset( $item_data['is_thumbnail_sibling'] ) ? $item_data['is_thumbnail_sibling'] : false );
                            if ( $is_thumbnail_sibling ) {
                                echo '<strong style="color: #155724;">🛡️ ' . esc_html__( 'WebP/AVIF of thumbnail/resized version', 'wuclean-database-cleaner-for-woocommerce' ) . '</strong><br>';
                                echo esc_html__( 'The thumbnail version was detected:', 'wuclean-database-cleaner-for-woocommerce' ) . '<br>';
                            } else {
                                echo '<strong style="color: #155724;">🛡️ ' . esc_html( $file_type ) . ' ' . esc_html( $relationship_label ) . '</strong><br>';
                                echo esc_html__( 'A potentially related image was detected:', 'wuclean-database-cleaner-for-woocommerce' ) . '<br>';
                            }
                        }
                        echo '<strong>📷 ' . esc_html( $parent_filename ) . '</strong> <small>(ID: ' . esc_html( $parent_id ) . ') <span style="background: #28a745; color: white; padding: 2px 6px; border-radius: 3px; font-size: 10px;">🛡️ PROTECTED</span></small><br>';
                        if ( 'scaled_original' === $subtype && isset( $item_data['note'] ) ) {
                            echo '<small style="color: #666;">' . esc_html( $item_data['note'] ) . '</small><br>';
                        }
                        echo '<span style="color: #155724;"><strong>⚠️ ' . esc_html__( 'Recommendation: Keep this file (related image is protected).', 'wuclean-database-cleaner-for-woocommerce' ) . '</strong></span>';
                        echo '</div>';
                    } else {
                        // Parent/scaled version also unused.
                        echo '<div style="background: #e7f3ff; border-left: 3px solid #2196f3; padding: 8px; margin-top: 8px; font-size: 12px; line-height: 1.5;">';
                        if ( 'scaled_original' === $subtype ) {
                            echo '<strong style="color: #0c5460;">💡 ' . esc_html__( 'WordPress scaled image original', 'wuclean-database-cleaner-for-woocommerce' ) . '</strong><br>';
                            echo esc_html__( 'The scaled version of this image was detected (also appears unused):', 'wuclean-database-cleaner-for-woocommerce' ) . '<br>';
                        } elseif ( 'thumbnail' === $subtype ) {
                            echo '<strong style="color: #0c5460;">💡 ' . esc_html__( 'WordPress thumbnail/resized version', 'wuclean-database-cleaner-for-woocommerce' ) . '</strong><br>';
                            echo esc_html__( 'The parent image was detected (also appears unused):', 'wuclean-database-cleaner-for-woocommerce' ) . '<br>';
                        } else {
                            // Check if this is a thumbnail sibling.
                            $is_thumbnail_sibling = ( isset( $item_data['is_thumbnail_sibling'] ) ? $item_data['is_thumbnail_sibling'] : false );
                            if ( $is_thumbnail_sibling ) {
                                echo '<strong style="color: #0c5460;">💡 ' . esc_html__( 'WebP/AVIF of thumbnail/resized version', 'wuclean-database-cleaner-for-woocommerce' ) . '</strong><br>';
                                echo esc_html__( 'The thumbnail version was detected (also appears unused):', 'wuclean-database-cleaner-for-woocommerce' ) . '<br>';
                            } else {
                                echo '<strong style="color: #0c5460;">💡 ' . esc_html( $file_type ) . ' ' . esc_html( $relationship_label ) . '</strong><br>';
                                echo esc_html__( 'A potentially related image was detected (also appears unused):', 'wuclean-database-cleaner-for-woocommerce' ) . '<br>';
                            }
                        }
                        echo '<strong>📷 ' . esc_html( $parent_filename ) . '</strong> <small>(ID: ' . esc_html( $parent_id ) . ')</small><br>';
                        if ( ('scaled_original' === $subtype || 'thumbnail' === $subtype) && isset( $item_data['note'] ) ) {
                            echo '<small style="color: #666;">' . esc_html( $item_data['note'] ) . '</small><br>';
                        }
                        echo '<span style="color: #0c5460;">' . esc_html__( 'Note: Both files appear unused.', 'wuclean-database-cleaner-for-woocommerce' ) . '</span>';
                        echo '</div>';
                    }
                }
            } elseif ( 'sibling_no_parent' === $subtype && isset( $item_data['note'] ) ) {
                // WebP/AVIF but no parent found.
                echo '<div style="background: #f0f0f0; border-left: 3px solid #999; padding: 8px; margin-top: 8px; font-size: 12px;">';
                echo '<span style="color: #666;">' . esc_html( $item_data['note'] ) . '</span>';
                echo '</div>';
            } elseif ( 'scaled_original_no_scaled' === $subtype && isset( $item_data['note'] ) ) {
                // Original but scaled version not found.
                echo '<div style="background: #f0f0f0; border-left: 3px solid #999; padding: 8px; margin-top: 8px; font-size: 12px;">';
                echo '<span style="color: #666;">' . esc_html( $item_data['note'] ) . '</span>';
                echo '</div>';
            } elseif ( 'thumbnail_no_parent' === $subtype && isset( $item_data['note'] ) ) {
                // Thumbnail but parent not found.
                echo '<div style="background: #f0f0f0; border-left: 3px solid #999; padding: 8px; margin-top: 8px; font-size: 12px;">';
                echo '<span style="color: #666;">' . esc_html( $item_data['note'] ) . '</span>';
                echo '</div>';
            }
        }
        echo '</td>';
        // Actions.
        echo '<td class="pt-wuclean-col-actions" style="white-space: normal;">';
        if ( 'media_library' === $item_type ) {
            echo '<div class="pt-wuclean-image-row-actions">';
            $deep_scan_key = 'id_' . $item_id;
            // Protect and Trash buttons are available to all users.
            echo '<button type="button" class="button pt-wuclean-protect-btn" data-image-id="' . esc_attr( $item_id ) . '" style="margin: 0;">🛡️ ' . esc_html__( 'Protect', 'wuclean-database-cleaner-for-woocommerce' ) . '</button>';
            echo '<button type="button" class="button pt-wuclean-trash-btn" data-image-id="' . esc_attr( $item_id ) . '" style="background: #f0ad4e; border-color: #f0ad4e; color: #fff; margin: 0;">🗑️ ' . esc_html__( 'Trash', 'wuclean-database-cleaner-for-woocommerce' ) . '</button>';
            // Delete button - Enabled for all users.
            echo '<button type="button" class="button pt-wuclean-delete-btn" data-image-id="' . esc_attr( $item_id ) . '" style="background: #dc3232; border-color: #dc3232; color: #fff; margin: 0;"><span style="filter: brightness(0) invert(1);">❌</span> ' . esc_html__( 'Delete', 'wuclean-database-cleaner-for-woocommerce' ) . '</button>';
            echo '</div>';
        } else {
            // Orphaned files can only be deleted.
            $file_path = ( isset( $item_data['path'] ) ? $item_data['path'] : '' );
        }
        echo '</td>';
        echo '</tr>';
    }
    echo '</tbody></table>';
    echo '</div>';
    // Pagination - using centralized function.
    $preserve_params = array_merge( $results_query_preserve, array(
        '_wpnonce' => wp_create_nonce( 'wuclean-images-pagination' ),
    ) );
    if ( !empty( $search_term ) ) {
        $preserve_params['search'] = $search_term;
        $preserve_params['_wpnonce'] = wp_create_nonce( 'wuclean-images-search' );
        // Use search nonce if searching.
    }
    pt_wuclean_render_pagination( array(
        'current_page'    => $current_page,
        'total_pages'     => $total_pages,
        'base_url'        => admin_url( 'admin.php' ),
        'page_param'      => 'paged',
        'preserve_params' => $preserve_params,
    ) );
    echo '</div>';
}

// ============================================================================
// TRASH TAB
// ============================================================================
/**
 * Render trash tab
 */
function pt_wuclean_render_images_trash_tab() {
    echo '<div style="max-width: 1600px;">';
    echo '<h2 style="margin: 12px 0 15px 0; font-size: 1.3em; font-weight: 600; display: flex; align-items: center; gap: 8px;">🗑️ ' . esc_html__( 'Trashed Images', '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 Trash:', 'wuclean-database-cleaner-for-woocommerce' ) . '</strong> ' . esc_html__( 'Images shown here have been hidden from WordPress and should not display on the frontend. After clearing cache and viewing your website, you can restore these images if required or delete images no longer needed. It is best to reach a decision and not leave images in trash since some WordPress setups may delink trashed attachments after 30 days.', 'wuclean-database-cleaner-for-woocommerce' );
    if ( pt_wuclean_fs()->is_not_paying() ) {
        echo ' ' . esc_html__( 'Select multiple trashed images to restore or delete them at once with Wuclean Pro👑', 'wuclean-database-cleaner-for-woocommerce' );
    }
    echo '</p>';
    echo '</div>';
    // Search functionality.
    $search_term = '';
    if ( isset( $_GET['search'] ) ) {
        // Verify nonce - required for security (CSRF protection).
        if ( isset( $_GET['_wpnonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'wuclean-images-trash-search' ) ) {
            $search_term = pt_wuclean_image_search_sanitize_term( sanitize_text_field( wp_unslash( $_GET['search'] ) ) );
        }
        // Nonce verification failed - ignore search parameter for security.
    }
    // Get trashed attachments (images only).
    $args = array(
        'post_type'      => 'attachment',
        'post_mime_type' => 'image',
        'post_status'    => 'trash',
        'posts_per_page' => -1,
        'orderby'        => 'modified',
        'order'          => 'DESC',
    );
    $trashed_images = get_posts( $args );
    if ( !empty( $search_term ) ) {
        $trashed_images = array_values( array_filter( $trashed_images, static function ( $image ) use($search_term) {
            return $image instanceof WP_Post && pt_wuclean_attachment_matches_image_search( (int) $image->ID, $search_term );
        } ) );
    }
    // Also scan the plugin trash directory for items that no longer have a WP attachment record.
    $trash_orphan_groups = pt_wuclean_get_wuclean_trash_orphan_groups( $trashed_images );
    if ( !empty( $search_term ) && !empty( $trash_orphan_groups ) ) {
        $trash_orphan_groups = array_values( array_filter( $trash_orphan_groups, static function ( $group ) use($search_term) {
            return is_array( $group ) && pt_wuclean_trash_orphan_group_matches_image_search( $group, $search_term );
        } ) );
    }
    // Display search form (always show on trash tab).
    echo '<div style="margin-bottom: 20px;">';
    echo '<form method="get" action="' . esc_url( admin_url( 'admin.php' ) ) . '" style="display: inline-block;">';
    echo '<input type="hidden" name="page" value="wuclean-images">';
    echo '<input type="hidden" name="tab" value="trash">';
    echo '<input type="hidden" name="_wpnonce" value="' . esc_attr( wp_create_nonce( 'wuclean-images-trash-search' ) ) . '">';
    echo '<input type="text" name="search" value="' . esc_attr( $search_term ) . '" placeholder="' . esc_attr__( 'Search trashed images...', 'wuclean-database-cleaner-for-woocommerce' ) . '" style="width: 250px;">';
    echo '<button type="submit" class="button">' . esc_html__( 'Search', 'wuclean-database-cleaner-for-woocommerce' ) . '</button>';
    if ( !empty( $search_term ) ) {
        $clear_url = add_query_arg( array(
            'page'     => 'wuclean-images',
            'tab'      => 'trash',
            '_wpnonce' => wp_create_nonce( 'wuclean-images-tab' ),
        ), admin_url( 'admin.php' ) );
        echo ' <a href="' . esc_url( $clear_url ) . '" class="button">' . esc_html__( 'Clear', 'wuclean-database-cleaner-for-woocommerce' ) . '</a>';
    }
    echo '</form>';
    echo '</div>';
    if ( empty( $trashed_images ) ) {
        if ( empty( $trash_orphan_groups ) ) {
            echo '<div style="background: #fff; border: 1px solid #ccd0d4; border-radius: 4px; padding: 40px; text-align: center; margin: 20px 0;">';
            echo '<p style="font-size: 16px; color: #666;">' . esc_html__( 'Trash is empty. No images to display.', 'wuclean-database-cleaner-for-woocommerce' ) . '</p>';
            echo '</div>';
            echo '</div>';
            return;
        }
    }
    // Pagination.
    $per_page = 25;
    $current_page = 1;
    if ( isset( $_GET['paged'] ) ) {
        // Honor paged only when a valid trash-tab nonce is present (pagination or search); default to page 1 otherwise.
        $images_trash_pagination_nonce_valid = false;
        if ( isset( $_GET['_wpnonce'] ) ) {
            $nonce = sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) );
            $images_trash_pagination_nonce_valid = wp_verify_nonce( $nonce, 'wuclean-images-trash-pagination' ) || wp_verify_nonce( $nonce, 'wuclean-images-trash-search' );
        }
        if ( $images_trash_pagination_nonce_valid ) {
            $current_page = max( 1, intval( $_GET['paged'] ) );
        }
    }
    $total_trashed = count( $trashed_images );
    $total_pages = ceil( $total_trashed / $per_page );
    $offset = ($current_page - 1) * $per_page;
    $paginated_images = array_slice( $trashed_images, $offset, $per_page );
    // Count display.
    $start_item = $offset + 1;
    $end_item = min( $offset + $per_page, $total_trashed );
    /* translators: 1: first item index, 2: last item index, 3: total results */
    $count_template = __( 'Showing %1$d-%2$d of %3$d images', 'wuclean-database-cleaner-for-woocommerce' );
    echo '<div style="margin-bottom: 10px;">';
    echo '<span>' . esc_html( sprintf(
        $count_template,
        $start_item,
        $end_item,
        $total_trashed
    ) ) . '</span>';
    echo '</div>';
    // Results table.
    if ( !empty( $trashed_images ) ) {
        echo '<div style="background: #fff; border: 1px solid #ccd0d4; border-radius: 4px; padding: 20px; margin: 20px 0;">';
        echo '<table class="wp-list-table widefat fixed striped">';
        echo '<thead><tr>';
        echo '<th style="width: 80px;">' . esc_html__( 'Preview', 'wuclean-database-cleaner-for-woocommerce' ) . '</th>';
        echo '<th>' . esc_html__( 'Image Info', 'wuclean-database-cleaner-for-woocommerce' ) . '</th>';
        echo '<th style="width: 150px;">' . esc_html__( 'Trashed Date', 'wuclean-database-cleaner-for-woocommerce' ) . '</th>';
        echo '<th style="width: 33%;">' . esc_html__( 'Actions', 'wuclean-database-cleaner-for-woocommerce' ) . '</th>';
        echo '</tr></thead>';
        echo '<tbody>';
        foreach ( $paginated_images as $image ) {
            $image_id = $image->ID;
            $title = get_the_title( $image_id );
            $filename = basename( get_attached_file( $image_id ) );
            $trashed_date = get_the_modified_date( 'Y-m-d H:i', $image );
            echo '<tr>';
            // Preview - use trash path if available.
            echo '<td>';
            // Check new meta key first, fallback to old for backward compatibility.
            $trash_path = get_post_meta( $image_id, '_pt_wuclean_trash_path', true );
            if ( empty( $trash_path ) ) {
                $trash_path = get_post_meta( $image_id, '_wuclean_trash_path', true );
                // Migrate to new key if old key exists.
                if ( !empty( $trash_path ) ) {
                    update_post_meta( $image_id, '_pt_wuclean_trash_path', $trash_path );
                    delete_post_meta( $image_id, '_wuclean_trash_path' );
                }
            }
            if ( $trash_path && file_exists( $trash_path ) ) {
                // Image is in wuclean trash directory, construct URL.
                $trash_location = pt_wuclean_get_trash_location();
                $trash_url = $trash_location['url'];
                // Extract just the filename (format: ID_filename.ext).
                $trash_filename = basename( str_replace( '\\', '/', $trash_path ) );
                $thumb_url = $trash_url . $trash_filename;
                echo '<img src="' . esc_url( $thumb_url ) . '" style="max-width: 60px; max-height: 60px; object-fit: contain; border-radius: 4px; background: #f0f0f0;" onerror="this.style.display=\'none\'; this.nextElementSibling.style.display=\'flex\';">';
                echo '<div style="display: none; width: 60px; height: 60px; background: #f0f0f0; border-radius: 4px; align-items: center; justify-content: center;">🖼️</div>';
            } else {
                // Try WordPress default (might not work for trashed items).
                $preview_url = pt_wuclean_get_safe_attachment_preview_url( $image_id, 'thumbnail' );
                if ( $preview_url ) {
                    echo '<img src="' . esc_url( $preview_url ) . '" style="max-width: 60px; max-height: 60px; object-fit: contain; border-radius: 4px; background: #f0f0f0;" onerror="this.style.display=\'none\'; this.nextElementSibling.style.display=\'flex\';">';
                    echo '<div style="display: none; width: 60px; height: 60px; background: #f0f0f0; border-radius: 4px; align-items: center; justify-content: center;">🖼️</div>';
                } else {
                    echo '<div style="width: 60px; height: 60px; background: #f0f0f0; border-radius: 4px; display: flex; align-items: center; justify-content: center;">📄</div>';
                }
            }
            echo '</td>';
            // Info.
            echo '<td>';
            echo '<strong>' . esc_html( $title ) . '</strong><br>';
            echo '<small style="color: #666;">';
            echo esc_html__( 'ID:', 'wuclean-database-cleaner-for-woocommerce' ) . ' ' . esc_html( $image_id ) . ' | ';
            echo esc_html__( 'File:', 'wuclean-database-cleaner-for-woocommerce' ) . ' ' . esc_html( $filename );
            echo '</small>';
            echo '</td>';
            // Trashed date.
            echo '<td>';
            echo esc_html( $trashed_date );
            echo '</td>';
            // Actions.
            echo '<td>';
            echo '<button type="button" class="button pt-wuclean-restore-btn" data-image-id="' . esc_attr( $image_id ) . '" style="background: #46b450; border-color: #46b450; color: #fff; margin: 0 4px 4px 0;">♻️ ' . esc_html__( 'Restore', 'wuclean-database-cleaner-for-woocommerce' ) . '</button>';
            echo '<button type="button" class="button pt-wuclean-delete-permanent-btn" data-image-id="' . esc_attr( $image_id ) . '" style="background: #dc3232; border-color: #dc3232; color: #fff;"><span style="filter: brightness(0) invert(1);">❌</span> ' . esc_html__( 'Delete Permanently', 'wuclean-database-cleaner-for-woocommerce' ) . '</button>';
            echo '</td>';
            echo '</tr>';
        }
        echo '</tbody></table>';
        echo '</div>';
    }
    // Orphaned trash files (no WP attachment record).
    if ( !empty( $trash_orphan_groups ) ) {
        echo '<h3 style="margin: 28px 0 10px 0; font-size: 1.1em; font-weight: 600;">' . esc_html__( 'Orphaned files in Wuclean trash', 'wuclean-database-cleaner-for-woocommerce' ) . '</h3>';
        echo '<div style="background: #fff; border: 1px solid #ccd0d4; border-radius: 4px; padding: 20px; margin: 12px 0 20px 0;">';
        echo '<p style="margin: 0 0 12px 0; color: #666;">' . esc_html__( 'These files are still present in the Wuclean trash folder, but WordPress no longer has an attachment record for them (so they cannot be restored). You can download or permanently delete the files here.', 'wuclean-database-cleaner-for-woocommerce' ) . '</p>';
        echo '<table class="wp-list-table widefat fixed striped">';
        echo '<thead><tr>';
        echo '<th style="width: 80px;">' . esc_html__( 'Preview', 'wuclean-database-cleaner-for-woocommerce' ) . '</th>';
        echo '<th>' . esc_html__( 'Trash Info', 'wuclean-database-cleaner-for-woocommerce' ) . '</th>';
        echo '<th style="width: 33%;">' . esc_html__( 'Actions', 'wuclean-database-cleaner-for-woocommerce' ) . '</th>';
        echo '</tr></thead>';
        echo '<tbody>';
        foreach ( $trash_orphan_groups as $group ) {
            $group_id = ( isset( $group['attachment_id'] ) ? absint( $group['attachment_id'] ) : 0 );
            $group_filename = ( isset( $group['filename'] ) ? (string) $group['filename'] : '' );
            $group_count = ( isset( $group['file_count'] ) ? absint( $group['file_count'] ) : 0 );
            $group_preview = ( isset( $group['preview_url'] ) ? (string) $group['preview_url'] : '' );
            $group_file_list = ( isset( $group['files'] ) && is_array( $group['files'] ) ? $group['files'] : array() );
            echo '<tr data-trash-orphan-id="' . esc_attr( (string) $group_id ) . '">';
            echo '<td>';
            if ( '' !== $group_preview ) {
                echo '<img src="' . esc_url( $group_preview ) . '" style="max-width: 60px; max-height: 60px; object-fit: contain; border-radius: 4px; background: #f0f0f0;" onerror="this.style.display=\'none\'; this.nextElementSibling.style.display=\'flex\';">';
                echo '<div style="display: none; width: 60px; height: 60px; background: #f0f0f0; border-radius: 4px; align-items: center; justify-content: center;">🖼️</div>';
            } else {
                echo '<div style="width: 60px; height: 60px; background: #f0f0f0; border-radius: 4px; display: flex; align-items: center; justify-content: center;">📄</div>';
            }
            echo '</td>';
            echo '<td>';
            echo '<strong>' . esc_html( $group_filename ) . '</strong><br>';
            echo '<small style="color: #666;">';
            /* translators: %1$d: attachment id prefix, %2$d: number of files in group */
            echo esc_html( sprintf( __( 'ID prefix: %1$d | Files: %2$d', 'wuclean-database-cleaner-for-woocommerce' ), $group_id, $group_count ) );
            echo '</small>';
            if ( !empty( $group_file_list ) ) {
                $sample = array_slice( $group_file_list, 0, 4 );
                echo '<br><small style="color:#666;">' . esc_html( implode( ', ', $sample ) );
                if ( count( $group_file_list ) > count( $sample ) ) {
                    echo esc_html__( '…', 'wuclean-database-cleaner-for-woocommerce' );
                }
                echo '</small>';
            }
            echo '</td>';
            echo '<td>';
            echo '<button type="button" class="button pt-wuclean-download-trash-orphan-btn" data-attachment-id="' . esc_attr( (string) $group_id ) . '" style="background: #0073aa; border-color: #0073aa; color: #fff; margin: 0 6px 6px 0;">⬇ ' . esc_html__( 'Download', 'wuclean-database-cleaner-for-woocommerce' ) . '</button>';
            echo '<button type="button" class="button pt-wuclean-delete-trash-orphan-btn" data-attachment-id="' . esc_attr( (string) $group_id ) . '" style="background: #dc3232; border-color: #dc3232; color: #fff;"><span style="filter: brightness(0) invert(1);">❌</span> ' . esc_html__( 'Delete Files Permanently', 'wuclean-database-cleaner-for-woocommerce' ) . '</button>';
            echo '</td>';
            echo '</tr>';
        }
        echo '</tbody></table>';
        echo '</div>';
    }
    // Pagination - using centralized function.
    $preserve_params = array(
        'page'     => 'wuclean-images',
        'tab'      => 'trash',
        '_wpnonce' => wp_create_nonce( 'wuclean-images-trash-pagination' ),
    );
    if ( !empty( $search_term ) ) {
        $preserve_params['search'] = $search_term;
        $preserve_params['_wpnonce'] = wp_create_nonce( 'wuclean-images-trash-search' );
        // Use search nonce if searching.
    }
    pt_wuclean_render_pagination( array(
        'current_page'    => $current_page,
        'total_pages'     => $total_pages,
        'base_url'        => admin_url( 'admin.php' ),
        'page_param'      => 'paged',
        'preserve_params' => $preserve_params,
    ) );
    echo '</div>';
}

/**
 * Scan the plugin trash directory and return groups that no longer have a WP attachment post.
 *
 * @param WP_Post[] $trashed_images Current WP trashed attachments list (used to avoid duplicate display).
 * @return array<int, array<string, mixed>> List of groups: attachment_id, filename, file_count, files, preview_url.
 */
function pt_wuclean_get_wuclean_trash_orphan_groups(  array $trashed_images  ) {
    $already_listed_ids = array();
    foreach ( $trashed_images as $img ) {
        if ( isset( $img->ID ) ) {
            $already_listed_ids[absint( $img->ID )] = true;
        }
    }
    $trash_location = pt_wuclean_get_trash_location();
    $trash_dir = ( isset( $trash_location['dir'] ) ? (string) $trash_location['dir'] : '' );
    $trash_url = ( isset( $trash_location['url'] ) ? (string) $trash_location['url'] : '' );
    $trash_dir = rtrim( $trash_dir, "/\\ \t\n\r\x00\v" );
    if ( '' === $trash_dir || !is_dir( $trash_dir ) || !is_readable( $trash_dir ) ) {
        return array();
    }
    // Security: trash directory should live under WP_CONTENT_DIR.
    $wp_content_dir = str_replace( '\\', '/', WP_CONTENT_DIR );
    $trash_dir_normalized = str_replace( '\\', '/', $trash_dir );
    if ( 0 !== strpos( $trash_dir_normalized, $wp_content_dir ) ) {
        return array();
    }
    $files = scandir( $trash_dir );
    if ( false === $files ) {
        return array();
    }
    $groups = array();
    foreach ( $files as $file ) {
        if ( '.' === $file || '..' === $file ) {
            continue;
        }
        if ( !is_string( $file ) || '' === $file ) {
            continue;
        }
        // Only consider files in the format "{ID}_{name}".
        if ( !preg_match( '/^(\\d+)_+(.+)$/', $file, $m ) ) {
            continue;
        }
        $attachment_id = absint( $m[1] );
        if ( $attachment_id <= 0 ) {
            continue;
        }
        $full_path = $trash_dir . DIRECTORY_SEPARATOR . $file;
        if ( is_dir( $full_path ) ) {
            continue;
        }
        if ( !isset( $groups[$attachment_id] ) ) {
            $groups[$attachment_id] = array(
                'attachment_id' => $attachment_id,
                'files'         => array(),
            );
        }
        $groups[$attachment_id]['files'][] = $file;
    }
    if ( empty( $groups ) ) {
        return array();
    }
    // Build final list: only groups without a live WP trashed attachment post.
    $out = array();
    foreach ( $groups as $attachment_id => $group ) {
        $attachment_id = absint( $attachment_id );
        if ( $attachment_id <= 0 ) {
            continue;
        }
        // If WP currently lists this attachment in trash, it will already appear above.
        if ( isset( $already_listed_ids[$attachment_id] ) ) {
            continue;
        }
        $post = get_post( $attachment_id );
        if ( $post && isset( $post->ID ) && 'attachment' === $post->post_type && 'trash' === $post->post_status ) {
            continue;
        }
        $file_list = ( isset( $group['files'] ) && is_array( $group['files'] ) ? $group['files'] : array() );
        sort( $file_list, SORT_STRING );
        $filename = '';
        if ( !empty( $file_list ) ) {
            // Pick the best "preview" file for this group.
            // Practical compromise: prefer non-thumbnail raster images (exclude WebP/AVIF),
            // then pick the largest file size for highest quality preview.
            $raster_ext_re = '/\\.(jpe?g|png|gif|bmp|tiff?)$/i';
            $thumb_suffix_re = '/-\\d+x\\d+\\.[^.]+$/i';
            $non_thumb_candidates = array();
            $all_raster_candidates = array();
            foreach ( $file_list as $f ) {
                $f = ( is_string( $f ) ? $f : '' );
                if ( '' === $f ) {
                    continue;
                }
                if ( !preg_match( $raster_ext_re, $f ) ) {
                    continue;
                }
                $all_raster_candidates[] = $f;
                if ( !preg_match( $thumb_suffix_re, $f ) ) {
                    $non_thumb_candidates[] = $f;
                }
            }
            $candidates = ( !empty( $non_thumb_candidates ) ? $non_thumb_candidates : $all_raster_candidates );
            if ( !empty( $candidates ) ) {
                $best_file = '';
                $best_size = -1;
                foreach ( $candidates as $f ) {
                    $path = $trash_dir . DIRECTORY_SEPARATOR . $f;
                    if ( !file_exists( $path ) || is_dir( $path ) ) {
                        continue;
                    }
                    $size = @filesize( $path );
                    // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- filesize can emit warnings for unreadable files; we handle false.
                    if ( false === $size ) {
                        continue;
                    }
                    if ( (int) $size > $best_size ) {
                        $best_size = (int) $size;
                        $best_file = (string) $f;
                    }
                }
                if ( '' !== $best_file ) {
                    $filename = $best_file;
                } else {
                    $filename = (string) $candidates[0];
                }
            } else {
                $filename = (string) $file_list[0];
            }
        }
        $preview_url = '';
        if ( '' !== $filename && '' !== $trash_url ) {
            $preview_url = $trash_url . $filename;
        }
        $out[] = array(
            'attachment_id' => $attachment_id,
            'filename'      => $filename,
            'file_count'    => count( $file_list ),
            'files'         => $file_list,
            'preview_url'   => $preview_url,
        );
    }
    return $out;
}

// ============================================================================
// PROTECTED TAB.
// ============================================================================
/**
 * Render protected tab.
 */
function pt_wuclean_render_images_protected_tab() {
    echo '<div style="max-width: 1600px;">';
    echo '<h2 style="margin: 12px 0 15px 0; font-size: 1.3em; font-weight: 600; display: flex; align-items: center; gap: 8px;">🔒 ' . esc_html__( 'Protected Images', '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 Protected Images:', 'wuclean-database-cleaner-for-woocommerce' ) . '</strong> ' . esc_html__( 'Images shown here have been marked as protected and will be excluded from scans.', 'wuclean-database-cleaner-for-woocommerce' ) . '</p>';
    echo '</div>';
    // Search functionality.
    $search_term = '';
    if ( isset( $_GET['search'] ) ) {
        // Verify nonce - required for security (CSRF protection).
        if ( isset( $_GET['_wpnonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'wuclean-images-protected-search' ) ) {
            $search_term = pt_wuclean_image_search_sanitize_term( sanitize_text_field( wp_unslash( $_GET['search'] ) ) );
        }
        // Nonce verification failed - ignore search parameter for security.
    }
    // Get protected images (auto-cleans corrupted data).
    $protected_image_ids = pt_wuclean_get_protected_images();
    // Filter by search if needed.
    $filtered_ids = $protected_image_ids;
    if ( !empty( $search_term ) && !empty( $filtered_ids ) ) {
        $filtered_ids = array_filter( $filtered_ids, static function ( $image_id ) use($search_term) {
            return pt_wuclean_attachment_matches_image_search( (int) $image_id, $search_term );
        } );
    }
    // Display search form (always show).
    echo '<div style="margin-bottom: 20px;">';
    echo '<form method="get" action="' . esc_url( admin_url( 'admin.php' ) ) . '" style="display: inline-block;">';
    echo '<input type="hidden" name="page" value="wuclean-images">';
    echo '<input type="hidden" name="tab" value="protected">';
    echo '<input type="hidden" name="_wpnonce" value="' . esc_attr( wp_create_nonce( 'wuclean-images-protected-search' ) ) . '">';
    echo '<input type="text" name="search" value="' . esc_attr( $search_term ) . '" placeholder="' . esc_attr__( 'Search protected images...', 'wuclean-database-cleaner-for-woocommerce' ) . '" style="width: 250px;">';
    echo '<button type="submit" class="button">' . esc_html__( 'Search', 'wuclean-database-cleaner-for-woocommerce' ) . '</button>';
    if ( !empty( $search_term ) ) {
        $clear_url = add_query_arg( array(
            'page'     => 'wuclean-images',
            'tab'      => 'protected',
            '_wpnonce' => wp_create_nonce( 'wuclean-images-tab' ),
        ), admin_url( 'admin.php' ) );
        echo ' <a href="' . esc_url( $clear_url ) . '" class="button">' . esc_html__( 'Clear', 'wuclean-database-cleaner-for-woocommerce' ) . '</a>';
    }
    echo '</form>';
    echo '</div>';
    if ( empty( $filtered_ids ) ) {
        echo '<div style="background: #fff; border: 1px solid #ccd0d4; border-radius: 4px; padding: 40px; text-align: center; margin: 20px 0;">';
        if ( !empty( $search_term ) ) {
            echo '<p style="font-size: 16px; color: #666;">' . esc_html__( 'No protected images found matching your search criteria.', 'wuclean-database-cleaner-for-woocommerce' ) . '</p>';
            echo '<a href="' . esc_url( add_query_arg( array(
                'page'     => 'wuclean-images',
                'tab'      => 'protected',
                '_wpnonce' => wp_create_nonce( 'wuclean-images-tab' ),
            ), admin_url( 'admin.php' ) ) ) . '" class="button">' . esc_html__( 'Clear Search', 'wuclean-database-cleaner-for-woocommerce' ) . '</a>';
        } else {
            echo '<p style="font-size: 16px; color: #666;">' . esc_html__( 'No protected images. Protect images from the Scan Results tab to exclude them from future scans.', 'wuclean-database-cleaner-for-woocommerce' ) . '</p>';
        }
        echo '</div>';
        echo '</div>';
        return;
    }
    // Pagination.
    $per_page = 25;
    $current_page = 1;
    if ( isset( $_GET['paged'] ) ) {
        // Verify nonce if provided (accept either pagination or search nonce), otherwise allow if user has capability.
        $nonce_valid = false;
        if ( isset( $_GET['_wpnonce'] ) ) {
            $nonce = sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) );
            $nonce_valid = wp_verify_nonce( $nonce, 'wuclean-images-protected-pagination' ) || wp_verify_nonce( $nonce, 'wuclean-images-protected-search' );
        }
        if ( $nonce_valid || current_user_can( pt_wuclean_get_required_capability() ) ) {
            // Allow if nonce is valid or user has capability (for backward compatibility).
            $current_page = max( 1, intval( $_GET['paged'] ) );
        }
    }
    $total_protected = count( $filtered_ids );
    $total_pages = ceil( $total_protected / $per_page );
    $offset = ($current_page - 1) * $per_page;
    $paginated_ids = array_slice( $filtered_ids, $offset, $per_page );
    // Count display.
    $start_item = $offset + 1;
    $end_item = min( $offset + $per_page, $total_protected );
    /* translators: 1: first item index, 2: last item index, 3: total results */
    $count_template = __( 'Showing %1$d-%2$d of %3$d images', 'wuclean-database-cleaner-for-woocommerce' );
    echo '<div style="margin-bottom: 10px;">';
    echo '<span>' . esc_html( sprintf(
        $count_template,
        $start_item,
        $end_item,
        $total_protected
    ) ) . '</span>';
    echo '</div>';
    // Protected images table (`data-wuclean-images-tab` used by row Trash/Delete handlers when checkboxes are absent).
    echo '<div style="background: #fff; border: 1px solid #ccd0d4; border-radius: 4px; padding: 20px; margin: 20px 0;">';
    echo '<table class="wp-list-table widefat fixed striped" data-wuclean-images-tab="protected">';
    echo '<thead><tr>';
    echo '<th style="width: 80px;">' . esc_html__( 'Preview', 'wuclean-database-cleaner-for-woocommerce' ) . '</th>';
    echo '<th>' . esc_html__( 'Image Info', 'wuclean-database-cleaner-for-woocommerce' ) . '</th>';
    echo '<th style="width: 50%;">' . esc_html__( 'Actions', 'wuclean-database-cleaner-for-woocommerce' ) . '</th>';
    echo '</tr></thead>';
    echo '<tbody>';
    foreach ( $paginated_ids as $image_id ) {
        $title = get_the_title( $image_id );
        $file_path = get_attached_file( $image_id );
        $filename = ( $file_path ? basename( $file_path ) : 'N/A' );
        $upload_date = get_the_date( 'Y-m-d', $image_id );
        $file_size = 'N/A';
        if ( $file_path && file_exists( $file_path ) ) {
            $file_size = size_format( filesize( $file_path ) );
        }
        echo '<tr>';
        // Preview.
        echo '<td>';
        $preview_url = pt_wuclean_get_safe_attachment_preview_url( $image_id, 'thumbnail' );
        if ( $preview_url ) {
            echo '<img src="' . esc_url( $preview_url ) . '" style="max-width: 60px; max-height: 60px; object-fit: contain; border-radius: 4px; background: #f0f0f0;" onerror="this.style.display=\'none\'; this.nextElementSibling.style.display=\'flex\';">';
            echo '<div style="display: none; width: 60px; height: 60px; background: #f0f0f0; border-radius: 4px; align-items: center; justify-content: center;">🖼️</div>';
        } else {
            echo '<div style="width: 60px; height: 60px; background: #f0f0f0; border-radius: 4px; display: flex; align-items: center; justify-content: center;">📄</div>';
        }
        echo '</td>';
        // Info.
        echo '<td>';
        echo '<strong>' . esc_html( $title ) . '</strong><br>';
        echo '<small style="color: #666;">';
        echo esc_html__( 'ID:', 'wuclean-database-cleaner-for-woocommerce' ) . ' ' . esc_html( $image_id ) . '<br>';
        echo esc_html__( 'File:', 'wuclean-database-cleaner-for-woocommerce' ) . ' ' . esc_html( $filename ) . '<br>';
        echo esc_html__( 'Uploaded:', 'wuclean-database-cleaner-for-woocommerce' ) . ' ' . esc_html( $upload_date ) . ' | ';
        echo esc_html__( 'Size:', 'wuclean-database-cleaner-for-woocommerce' ) . ' ' . esc_html( $file_size );
        echo '</small>';
        echo '</td>';
        // Actions.
        echo '<td>';
        echo '<button type="button" class="button pt-wuclean-unprotect-btn" data-image-id="' . esc_attr( $image_id ) . '" style="margin: 0 4px 4px 0;">🔓 ' . esc_html__( 'Unprotect', 'wuclean-database-cleaner-for-woocommerce' ) . '</button>';
        echo '<button type="button" class="button pt-wuclean-trash-btn" data-image-id="' . esc_attr( $image_id ) . '" style="background: #f0ad4e; border-color: #f0ad4e; color: #fff; margin: 0 4px 4px 0;">🗑️ ' . esc_html__( 'Trash', 'wuclean-database-cleaner-for-woocommerce' ) . '</button>';
        echo '<button type="button" class="button pt-wuclean-delete-btn" data-image-id="' . esc_attr( $image_id ) . '" style="background: #dc3232; border-color: #dc3232; color: #fff; margin: 0 4px 4px 0;"><span style="filter: brightness(0) invert(1);">❌</span> ' . esc_html__( 'Delete', 'wuclean-database-cleaner-for-woocommerce' ) . '</button>';
        echo '</td>';
        echo '</tr>';
    }
    echo '</tbody></table>';
    echo '</div>';
    // Pagination - using centralized function.
    $preserve_params = array(
        'page'     => 'wuclean-images',
        'tab'      => 'protected',
        '_wpnonce' => wp_create_nonce( 'wuclean-images-protected-pagination' ),
    );
    if ( !empty( $search_term ) ) {
        $preserve_params['search'] = $search_term;
        $preserve_params['_wpnonce'] = wp_create_nonce( 'wuclean-images-protected-search' );
        // Use search nonce if searching.
    }
    pt_wuclean_render_pagination( array(
        'current_page'    => $current_page,
        'total_pages'     => $total_pages,
        'base_url'        => admin_url( 'admin.php' ),
        'page_param'      => 'paged',
        'preserve_params' => $preserve_params,
    ) );
    echo '</div>';
}

// ============================================================================
// HELPER FUNCTIONS.
// ============================================================================
/**
 * Get a safe preview URL for an attachment thumbnail.
 *
 * WordPress can sometimes return a thumbnail URL that points to a non-existent file
 * (e.g. missing intermediate size for WebP/AVIF). In that case, fall back to the
 * full attachment URL so admin tables don't show broken images.
 *
 * @param int    $attachment_id Attachment ID.
 * @param string $size          Image size.
 * @return string
 */
function pt_wuclean_get_safe_attachment_preview_url(  $attachment_id, $size = 'thumbnail'  ) {
    $attachment_id = absint( $attachment_id );
    if ( $attachment_id <= 0 ) {
        return '';
    }
    $thumb = wp_get_attachment_image_src( $attachment_id, $size );
    if ( is_array( $thumb ) && !empty( $thumb[0] ) ) {
        $thumb_url = (string) $thumb[0];
        // If the URL is in uploads, validate the file exists on disk to avoid 404s.
        $uploads = wp_upload_dir();
        $uploads_baseurl = ( isset( $uploads['baseurl'] ) ? (string) $uploads['baseurl'] : '' );
        $uploads_basedir = ( isset( $uploads['basedir'] ) ? (string) $uploads['basedir'] : '' );
        if ( !empty( $uploads_baseurl ) && !empty( $uploads_basedir ) && strpos( (string) $thumb_url, $uploads_baseurl ) === 0 ) {
            $relative = substr( (string) $thumb_url, strlen( $uploads_baseurl ) );
            $relative = ltrim( (string) $relative, '/' );
            $path = wp_normalize_path( trailingslashit( $uploads_basedir ) . $relative );
            if ( file_exists( $path ) ) {
                return $thumb_url;
            }
            // Missing intermediate file: fall back to full.
            $full = wp_get_attachment_url( $attachment_id );
            return ( $full ? (string) $full : '' );
        }
        // Not a local uploads URL (CDN/etc). Trust WP and return it.
        return $thumb_url;
    }
    $full = wp_get_attachment_url( $attachment_id );
    return ( $full ? (string) $full : '' );
}

/**
 * Get and clean protected images list
 * Validates data and removes any corrupted entries
 */
function pt_wuclean_get_protected_images() {
    $protected_images = get_option( 'pt_wuclean_protected_images', array() );
    // Ensure it's an array.
    if ( !is_array( $protected_images ) ) {
        $protected_images = array();
    }
    // Filter out invalid entries (non-numeric or zero/negative values).
    $cleaned = array_filter( $protected_images, function ( $id ) {
        return is_numeric( $id ) && intval( $id ) > 0;
    } );
    // Convert all to integers.
    $cleaned = array_map( 'intval', $cleaned );
    // Remove duplicates and re-index.
    $cleaned = array_values( array_unique( $cleaned ) );
    // Remove entries that no longer exist or whose files are missing (WP deletion or SFTP deletion).
    $cleaned = array_values( array_filter( $cleaned, function ( $image_id ) {
        $image_id = absint( $image_id );
        if ( $image_id <= 0 ) {
            return false;
        }
        $post = get_post( $image_id );
        if ( !$post || !isset( $post->ID ) || 'attachment' !== get_post_type( $image_id ) ) {
            return false;
        }
        $file_path = get_attached_file( $image_id );
        if ( !$file_path || !file_exists( $file_path ) ) {
            return false;
        }
        return true;
    } ) );
    // If we cleaned anything, save the cleaned version.
    if ( $cleaned !== $protected_images ) {
        update_option( 'pt_wuclean_protected_images', $cleaned, false );
    }
    return $cleaned;
}

/**
 * Check if an image has format siblings (WebP, AVIF, etc.)
 *
 * @param int $image_id Attachment ID.
 * @return array|false Array of sibling info or false.
 */
function pt_wuclean_has_format_siblings(  $image_id  ) {
    $file_path = get_attached_file( $image_id );
    if ( !$file_path || !file_exists( $file_path ) ) {
        return false;
    }
    $path_info = pathinfo( $file_path );
    $dir = $path_info['dirname'];
    $filename = $path_info['filename'];
    $current_ext = ( isset( $path_info['extension'] ) ? $path_info['extension'] : '' );
    $siblings_found = array();
    // Check for WebP/AVIF variations.
    $checks = array(
        'webp' => $dir . '/' . $filename . '.webp',
        'avif' => $dir . '/' . $filename . '.avif',
    );
    // Also check appended extension format.
    if ( $current_ext ) {
        $checks['webp_appended'] = $file_path . '.webp';
        $checks['avif_appended'] = $file_path . '.avif';
    }
    foreach ( $checks as $format => $sibling_path ) {
        if ( file_exists( $sibling_path ) ) {
            $format_name = ( false !== strpos( $format, 'webp' ) ? 'WebP' : 'AVIF' );
            $siblings_found[] = array(
                'format'   => $format_name,
                'path'     => $sibling_path,
                'filename' => basename( $sibling_path ),
                'size'     => ( file_exists( $sibling_path ) ? filesize( $sibling_path ) : 0 ),
            );
        }
    }
    return ( !empty( $siblings_found ) ? $siblings_found : false );
}

/**
 * Check if a file is a WebP or AVIF sibling.
 * Returns the parent filename if it's a sibling, false otherwise.
 *
 * @param string $file_path Full path to the file.
 * @return string|false Parent filename or false.
 */
function pt_wuclean_is_sibling_file(  $file_path  ) {
    global $wpdb;
    // Validate file_path parameter.
    if ( empty( $file_path ) || !is_string( $file_path ) ) {
        return false;
    }
    $filename = basename( $file_path );
    // Ensure filename is not empty.
    if ( empty( $filename ) ) {
        return false;
    }
    $ext = strtolower( pathinfo( $filename, PATHINFO_EXTENSION ) );
    // Only check webp and avif files.
    if ( !in_array( $ext, array('webp', 'avif'), true ) ) {
        return false;
    }
    // Pattern 1: image.jpg.webp (appended extension).
    if ( preg_match( '/^(.+\\.(jpg|jpeg|png|gif|bmp))\\.(webp|avif)$/i', $filename, $matches ) ) {
        return $matches[1];
        // Returns "image.jpg".
    }
    // Pattern 2: image.webp (replaced extension).
    $path_info = pathinfo( $filename );
    $filename_no_ext = ( isset( $path_info['filename'] ) && is_string( $path_info['filename'] ) ? $path_info['filename'] : '' );
    // Ensure we have a valid filename without extension.
    if ( empty( $filename_no_ext ) ) {
        return false;
    }
    $possible_extensions = array(
        'jpg',
        'jpeg',
        'png',
        'gif',
        'bmp'
    );
    // Create filename variations to handle WordPress filename sanitization.
    // WordPress often converts spaces to hyphens, so we need to check both.
    $filename_variations = array(
        $filename_no_ext,
        // Original: "Product Main".
        str_replace( ' ', '-', $filename_no_ext ),
        // Hyphens: "Product-Main".
        str_replace( '-', ' ', $filename_no_ext ),
    );
    $filename_variations = array_unique( $filename_variations );
    // First, check if parent file exists in same directory (fast check).
    foreach ( $filename_variations as $base_name ) {
        foreach ( $possible_extensions as $possible_ext ) {
            $parent_filename = $base_name . '.' . $possible_ext;
            $parent_path = dirname( $file_path ) . '/' . $parent_filename;
            if ( file_exists( $parent_path ) ) {
                return $parent_filename;
            }
        }
    }
    // If not found in filesystem, search Media Library database.
    // (parent might be in different folder or WebP generated by plugin).
    foreach ( $filename_variations as $base_name ) {
        foreach ( $possible_extensions as $possible_ext ) {
            $parent_filename = $base_name . '.' . $possible_ext;
            // Search in _wp_attached_file meta for this filename.
            $parent_exists = $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 pm.meta_key = '_wp_attached_file' \n                 AND pm.meta_value LIKE %s\n                 AND p.post_type = 'attachment'\n                 AND p.post_status IN ('inherit','trash')", '%' . $wpdb->esc_like( $parent_filename ) ) );
            if ( $parent_exists > 0 ) {
                return $parent_filename;
            }
        }
    }
    return false;
}

/**
 * Check if a file is the original of a scaled WordPress image.
 * Returns the scaled filename if it's an original, false otherwise.
 * WordPress creates scaled versions with "-scaled" suffix (e.g., "image-scaled.jpg" from "image.jpg").
 *
 * @param string $file_path Full path to the file.
 * @return string|false Scaled filename or false.
 */
function pt_wuclean_is_scaled_original(  $file_path  ) {
    $filename = basename( $file_path );
    $path_info = pathinfo( $filename );
    $filename_no_ext = $path_info['filename'];
    $ext = ( isset( $path_info['extension'] ) ? $path_info['extension'] : '' );
    // Check if this filename could be an original (doesn't already have -scaled in it).
    if ( false !== strpos( $filename_no_ext, '-scaled' ) ) {
        return false;
        // This is already a scaled version, not an original.
    }
    // Construct the scaled filename.
    $scaled_filename = $filename_no_ext . '-scaled.' . $ext;
    // Check if scaled version exists in same directory.
    $scaled_path = dirname( $file_path ) . '/' . $scaled_filename;
    if ( file_exists( $scaled_path ) ) {
        return $scaled_filename;
    }
    // Also check Media Library for scaled version (in case it's in a different folder).
    // Try exact match first (faster, can use indexes), then LIKE as fallback.
    global $wpdb;
    $scaled_exists = $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 pm.meta_key = '_wp_attached_file' \n         AND (pm.meta_value = %s OR pm.meta_value LIKE %s)\n         AND p.post_type = 'attachment'\n         AND p.post_status IN ('inherit','trash')\n         LIMIT 1", $scaled_filename, '%/' . $wpdb->esc_like( $scaled_filename ) ) );
    if ( $scaled_exists > 0 ) {
        return $scaled_filename;
    }
    return false;
}

/**
 * Check if a file is a WordPress-generated thumbnail/resized version.
 * Returns the parent filename if it's a thumbnail, false otherwise.
 * WordPress creates thumbnails with pattern: filename-100x100.jpg.
 * Also handles WebP/AVIF thumbnails: filename-100x100.jpg.webp or filename-100x100.webp.
 *
 * @param string $file_path Full path to the file.
 * @return string|false Parent filename or false.
 */
function pt_wuclean_is_thumbnail_file(  $file_path  ) {
    // Validate file_path parameter.
    if ( empty( $file_path ) || !is_string( $file_path ) ) {
        return false;
    }
    $filename = basename( $file_path );
    // Ensure filename is not empty.
    if ( empty( $filename ) ) {
        return false;
    }
    $path_info = pathinfo( $filename );
    $filename_no_ext = ( isset( $path_info['filename'] ) && is_string( $path_info['filename'] ) ? $path_info['filename'] : '' );
    $ext = ( isset( $path_info['extension'] ) ? strtolower( $path_info['extension'] ) : '' );
    // Ensure we have a valid filename without extension.
    if ( empty( $filename_no_ext ) ) {
        return false;
    }
    // Pattern 1: filename-100x100.jpg.webp (thumbnail with appended WebP/AVIF).
    if ( preg_match( '/^(.+)-(\\d+)x(\\d+)\\.(jpg|jpeg|png|gif|bmp)\\.(webp|avif)$/i', $filename, $matches ) ) {
        // Extract parent: filename.jpg.webp (the WebP version of the original).
        $parent_filename = $matches[1] . '.' . $matches[4] . '.' . $matches[5];
        return $parent_filename;
    }
    // Pattern 2: filename-100x100.webp (thumbnail with replaced extension WebP/AVIF).
    // This is a WebP/AVIF version of a thumbnail - the parent is the original image.
    if ( in_array( $ext, array('webp', 'avif'), true ) ) {
        if ( preg_match( '/^(.+)-(\\d+)x(\\d+)$/i', $filename_no_ext, $matches ) ) {
            // Extract base name (e.g., "Planet-of-Lana-Qatar" from "Planet-of-Lana-Qatar-100x100").
            $base_name = $matches[1];
            $possible_extensions = array(
                'jpg',
                'jpeg',
                'png',
                'gif',
                'bmp'
            );
            // Check if original parent exists in same directory (e.g., "Planet-of-Lana-Qatar.jpg").
            foreach ( $possible_extensions as $possible_ext ) {
                $parent_filename = $base_name . '.' . $possible_ext;
                $parent_path = dirname( $file_path ) . '/' . $parent_filename;
                if ( file_exists( $parent_path ) ) {
                    // Return the original parent filename (not the WebP version).
                    // The Media Library stores the original, not the WebP sibling.
                    return $parent_filename;
                }
            }
            // If not found in filesystem, return the most likely parent (jpg is most common).
            // The caller will check Media Library for this original filename.
            return $base_name . '.jpg';
        }
    }
    // Pattern 3: filename-100x100.jpg (standard thumbnail).
    if ( preg_match( '/^(.+)-(\\d+)x(\\d+)$/i', $filename_no_ext, $matches ) ) {
        // Extract parent: filename.jpg.
        $parent_filename = $matches[1] . '.' . $ext;
        return $parent_filename;
    }
    return false;
}

/**
 * Join a relative directory (under uploads) and a filename for _wp_attached_file style paths.
 *
 * @param string $directory_relative Relative directory using forward slashes, or "." / empty for uploads root.
 * @param string $filename           File name (no path segments).
 * @return string Normalized relative path.
 */
function pt_wuclean_join_uploads_relative_path(  $directory_relative, $filename  ) {
    $filename = ( is_string( $filename ) ? str_replace( '\\', '/', trim( $filename ) ) : '' );
    $filename = ltrim( $filename, '/' );
    if ( '' === $filename ) {
        return '';
    }
    $dir = ( is_string( $directory_relative ) ? str_replace( '\\', '/', trim( (string) $directory_relative, "/ \t\n\r\x00\v" ) ) : '' );
    $dir = trim( $dir, '/' );
    if ( '' === $dir || '.' === $dir ) {
        return $filename;
    }
    return $dir . '/' . $filename;
}

/**
 * Normalize a relative uploads path for comparisons and meta lookups.
 *
 * @param string $relpath Relative path under uploads.
 * @return string
 */
function pt_wuclean_normalize_uploads_attached_relpath(  $relpath  ) {
    $relpath = ( is_string( $relpath ) ? wp_normalize_path( $relpath ) : '' );
    $relpath = str_replace( '\\', '/', $relpath );
    return ltrim( $relpath, '/\\' );
}

/**
 * Build normalized _wp_attached_file-style relative paths to try when resolving an on-disk basename
 * under a month folder (handles WordPress `-scaled` primary files and `*-768x768.jpg` thumbnails).
 *
 * @param string $directory_relative Relative directory under uploads (e.g. `2026/05`) or empty.
 * @param string $filename           Basename only (no path segments).
 * @return string[] Unique normalized relative paths.
 */
function pt_wuclean_attached_file_relpath_resolution_candidates(  $directory_relative, $filename  ) {
    $out = array();
    $filename = ( is_string( $filename ) ? trim( str_replace( '\\', '/', $filename ), '/' ) : '' );
    if ( '' === $filename ) {
        return $out;
    }
    $filename = basename( $filename );
    if ( '' === $filename ) {
        return $out;
    }
    $dir_part = ( is_string( $directory_relative ) ? trim( str_replace( '\\', '/', $directory_relative ), '/' ) : '' );
    if ( '.' === $dir_part ) {
        $dir_part = '';
    }
    $push = static function ( $rel ) use(&$out) {
        if ( !function_exists( 'pt_wuclean_normalize_uploads_attached_relpath' ) ) {
            return;
        }
        $n = pt_wuclean_normalize_uploads_attached_relpath( $rel );
        if ( '' !== $n ) {
            $out[$n] = true;
        }
    };
    if ( !function_exists( 'pt_wuclean_join_uploads_relative_path' ) ) {
        $push( $filename );
        return array_keys( $out );
    }
    $push( pt_wuclean_join_uploads_relative_path( $dir_part, $filename ) );
    $path_info = pathinfo( $filename );
    $fno = ( isset( $path_info['filename'] ) && is_string( $path_info['filename'] ) ? $path_info['filename'] : '' );
    $ext = ( isset( $path_info['extension'] ) && is_string( $path_info['extension'] ) ? strtolower( $path_info['extension'] ) : '' );
    if ( '' !== $fno && '' !== $ext ) {
        if ( preg_match( '/^(.+)-(\\d+)x(\\d+)$/i', $fno, $matches ) ) {
            $base = $matches[1];
            $push( pt_wuclean_join_uploads_relative_path( $dir_part, $base . '.' . $ext ) );
            if ( false === strpos( $base, '-scaled' ) ) {
                $push( pt_wuclean_join_uploads_relative_path( $dir_part, $base . '-scaled.' . $ext ) );
            }
        } elseif ( false === strpos( $fno, '-scaled' ) ) {
            $push( pt_wuclean_join_uploads_relative_path( $dir_part, $fno . '-scaled.' . $ext ) );
        }
    }
    return array_keys( $out );
}

/**
 * Find a Media Library attachment whose `_wp_attachment_metadata` size `file` matches the given basename.
 *
 * @param string $size_basename      Expected size filename (e.g. `image-768x768.jpg`).
 * @param string $prefer_dir_part    Optional relative directory under uploads to narrow matches (e.g. `2026/05`).
 * @return int Attachment ID or 0.
 */
function pt_wuclean_find_attachment_id_by_metadata_size_filename(  $size_basename, $prefer_dir_part = ''  ) {
    global $wpdb;
    $size_basename = ( is_string( $size_basename ) ? basename( trim( $size_basename ) ) : '' );
    if ( '' === $size_basename ) {
        return 0;
    }
    if ( !preg_match( '/-\\d+x\\d+\\.(jpe?g|png|gif|bmp|webp|avif)$/i', $size_basename ) ) {
        return 0;
    }
    $like_meta = '%' . $wpdb->esc_like( $size_basename ) . '%';
    $prefer_dir_part = ( is_string( $prefer_dir_part ) ? trim( str_replace( '\\', '/', $prefer_dir_part ), '/' ) : '' );
    if ( '.' === $prefer_dir_part ) {
        $prefer_dir_part = '';
    }
    if ( '' !== $prefer_dir_part ) {
        $dir_prefix = $prefer_dir_part . '/';
        $like_att = $wpdb->esc_like( $dir_prefix ) . '%';
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $pid = (int) $wpdb->get_var( $wpdb->prepare( "SELECT MIN(pm.post_id) FROM {$wpdb->postmeta} pm\n\t\t\t\tINNER JOIN {$wpdb->postmeta} pm2 ON pm2.post_id = pm.post_id AND pm2.meta_key = '_wp_attached_file'\n\t\t\t\tINNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id\n\t\t\t\tWHERE pm.meta_key = '_wp_attachment_metadata'\n\t\t\t\tAND p.post_type = 'attachment'\n\t\t\t\tAND p.post_status IN ('inherit','trash')\n\t\t\t\tAND pm.meta_value LIKE %s\n\t\t\t\tAND pm2.meta_value LIKE %s", $like_meta, $like_att ) );
        return ( $pid > 0 ? $pid : 0 );
    }
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    $pid = (int) $wpdb->get_var( $wpdb->prepare( "SELECT MIN(pm.post_id) FROM {$wpdb->postmeta} pm\n\t\t\tINNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id\n\t\t\tWHERE pm.meta_key = '_wp_attachment_metadata'\n\t\t\tAND p.post_type = 'attachment'\n\t\t\tAND p.post_status IN ('inherit','trash')\n\t\t\tAND pm.meta_value LIKE %s", $like_meta ) );
    return ( $pid > 0 ? $pid : 0 );
}

/**
 * Resolve a Media Library attachment ID for an uploads-only derivative (thumbnail, WebP sibling, etc.).
 *
 * @param string            $dir_part                Relative directory under uploads (e.g. `2026/05`).
 * @param string            $basename                Basename to resolve (e.g. `image-768x768.jpg`).
 * @param array<string,int> $attachment_by_relpath   Map from {@see pt_wuclean_batch_resolve_attachment_ids_from_attached_file_relpaths()}.
 * @param string            $upload_base_dir         Uploads basedir.
 * @return int Attachment ID or 0.
 */
function pt_wuclean_resolve_attachment_id_for_uploads_derivative(
    $dir_part,
    $basename,
    array $attachment_by_relpath,
    $upload_base_dir
) {
    $basename = ( is_string( $basename ) ? basename( trim( $basename ) ) : '' );
    if ( '' === $basename ) {
        return 0;
    }
    $dir_part = ( is_string( $dir_part ) ? trim( str_replace( '\\', '/', $dir_part ), '/' ) : '' );
    if ( '.' === $dir_part ) {
        $dir_part = '';
    }
    $candidates = ( function_exists( 'pt_wuclean_attached_file_relpath_resolution_candidates' ) ? pt_wuclean_attached_file_relpath_resolution_candidates( $dir_part, $basename ) : array() );
    if ( empty( $candidates ) && function_exists( 'pt_wuclean_join_uploads_relative_path' ) && function_exists( 'pt_wuclean_normalize_uploads_attached_relpath' ) ) {
        $candidates = array(pt_wuclean_normalize_uploads_attached_relpath( pt_wuclean_join_uploads_relative_path( $dir_part, $basename ) ));
    }
    foreach ( $candidates as $rel ) {
        $rel = ( is_string( $rel ) ? $rel : '' );
        if ( '' === $rel ) {
            continue;
        }
        if ( isset( $attachment_by_relpath[$rel] ) && (int) $attachment_by_relpath[$rel] > 0 ) {
            return (int) $attachment_by_relpath[$rel];
        }
    }
    if ( function_exists( 'pt_wuclean_find_attachment_by_filename' ) ) {
        $found = pt_wuclean_find_attachment_by_filename( $basename, $upload_base_dir, $dir_part );
        if ( $found ) {
            return absint( $found );
        }
    }
    if ( function_exists( 'pt_wuclean_find_attachment_id_by_metadata_size_filename' ) ) {
        $mid = pt_wuclean_find_attachment_id_by_metadata_size_filename( $basename, $dir_part );
        if ( $mid > 0 ) {
            return $mid;
        }
    }
    return 0;
}

/**
 * Whether an uploads basename matches the site logo or site icon attachment (main file or a registered metadata size).
 * Compares filename stems so WebP/AVIF siblings match their JPEG/PNG size counterparts.
 *
 * @param string $dir_part Relative directory under uploads (e.g. `2026/05`) or empty to skip directory filtering.
 * @param string $basename Loose file basename.
 * @return array{used:bool,reason:null|string,attachment_id:int}
 */
function pt_wuclean_uploads_basename_is_site_identity_derivative(  $dir_part, $basename  ) {
    $result = array(
        'used'          => false,
        'reason'        => null,
        'attachment_id' => 0,
    );
    $basename = ( is_string( $basename ) ? basename( trim( $basename ) ) : '' );
    if ( '' === $basename ) {
        return $result;
    }
    $want_stem = strtolower( (string) pathinfo( $basename, PATHINFO_FILENAME ) );
    if ( '' === $want_stem ) {
        return $result;
    }
    $dir_part = ( is_string( $dir_part ) ? trim( str_replace( '\\', '/', $dir_part ), '/' ) : '' );
    if ( '.' === $dir_part ) {
        $dir_part = '';
    }
    $candidates = array();
    $logo_id = absint( get_theme_mod( 'custom_logo' ) );
    if ( $logo_id > 0 ) {
        $candidates[] = array(
            'id'     => $logo_id,
            'reason' => 'custom_logo',
        );
    }
    $icon_id = absint( get_option( 'site_icon' ) );
    if ( $icon_id > 0 ) {
        $candidates[] = array(
            'id'     => $icon_id,
            'reason' => 'site_icon',
        );
    }
    foreach ( $candidates as $c ) {
        $id = ( isset( $c['id'] ) ? absint( $c['id'] ) : 0 );
        if ( $id <= 0 ) {
            continue;
        }
        $attached = get_attached_file( $id );
        if ( !is_string( $attached ) || '' === $attached ) {
            continue;
        }
        $rel = get_post_meta( $id, '_wp_attached_file', true );
        $rel = ( is_string( $rel ) ? trim( str_replace( '\\', '/', $rel ), '/' ) : '' );
        $main_dir = '';
        if ( '' !== $rel ) {
            $d = dirname( $rel );
            $main_dir = ( '.' === $d || '' === $d ? '' : $d );
        }
        if ( '' !== $dir_part && '' !== $main_dir && 0 !== strcasecmp( $dir_part, $main_dir ) ) {
            continue;
        }
        $main_stem = strtolower( (string) pathinfo( $attached, PATHINFO_FILENAME ) );
        if ( $main_stem === $want_stem ) {
            return array(
                'used'          => true,
                'reason'        => ( isset( $c['reason'] ) ? (string) $c['reason'] : null ),
                'attachment_id' => $id,
            );
        }
        $meta = wp_get_attachment_metadata( $id );
        if ( !is_array( $meta ) || empty( $meta['sizes'] ) || !is_array( $meta['sizes'] ) ) {
            continue;
        }
        foreach ( $meta['sizes'] as $size_data ) {
            if ( !is_array( $size_data ) || empty( $size_data['file'] ) || !is_string( $size_data['file'] ) ) {
                continue;
            }
            $size_stem = strtolower( (string) pathinfo( $size_data['file'], PATHINFO_FILENAME ) );
            if ( $size_stem === $want_stem ) {
                return array(
                    'used'          => true,
                    'reason'        => ( isset( $c['reason'] ) ? (string) $c['reason'] : null ),
                    'attachment_id' => $id,
                );
            }
        }
    }
    return $result;
}

/**
 * Stable cluster stem for grouping image scan result rows (scaled originals, siblings, thumbnails).
 *
 * @param array $item Decoded scan result item.
 * @return string Lowercase stem for sorting (without `-scaled` suffix).
 */
function pt_wuclean_image_scan_result_cluster_stem(  array $item  ) {
    $type = ( isset( $item['type'] ) ? (string) $item['type'] : '' );
    $data = ( isset( $item['data'] ) && is_array( $item['data'] ) ? $item['data'] : array() );
    $stem = '';
    if ( 'orphaned_file' === $type ) {
        if ( !empty( $data['sibling_filename'] ) ) {
            $sf = (string) $data['sibling_filename'];
            $stem = pathinfo( $sf, PATHINFO_FILENAME );
        } elseif ( !empty( $data['path'] ) && function_exists( 'pt_wuclean_is_thumbnail_file' ) ) {
            $tp = pt_wuclean_is_thumbnail_file( (string) $data['path'] );
            if ( is_string( $tp ) && '' !== $tp ) {
                $stem = pathinfo( $tp, PATHINFO_FILENAME );
            }
        }
        if ( '' === $stem && !empty( $data['filename'] ) ) {
            $stem = pathinfo( (string) $data['filename'], PATHINFO_FILENAME );
            if ( preg_match( '/^(.+)-\\d+x\\d+$/i', (string) $stem, $m ) ) {
                $stem = $m[1];
            }
        }
    } elseif ( 'media_library' === $type && !empty( $item['id'] ) ) {
        $af = get_attached_file( absint( $item['id'] ) );
        if ( is_string( $af ) && '' !== $af ) {
            $stem = pathinfo( $af, PATHINFO_FILENAME );
        }
    }
    $stem = strtolower( (string) $stem );
    $stem = str_replace( '-scaled', '', $stem );
    return ( '' !== $stem ? $stem : 'zzzz-unknown' );
}

/**
 * Display order within a filename cluster (Media Library first, then originals, siblings, thumbnails).
 *
 * @param array $item Decoded scan result item.
 * @return int Rank (lower first).
 */
function pt_wuclean_image_scan_result_type_rank(  array $item  ) {
    $type = ( isset( $item['type'] ) ? (string) $item['type'] : '' );
    $subtype = ( isset( $item['subtype'] ) ? (string) $item['subtype'] : '' );
    if ( 'media_library' === $type ) {
        return 0;
    }
    if ( 'orphaned_file' !== $type ) {
        return 50;
    }
    if ( 'scaled_original' === $subtype || 'scaled_original_no_scaled' === $subtype ) {
        return 1;
    }
    if ( 'sibling' === $subtype || 'sibling_no_parent' === $subtype ) {
        return 2;
    }
    if ( 'thumbnail' === $subtype || 'thumbnail_no_parent' === $subtype ) {
        return 3;
    }
    return 10;
}

/**
 * Sort scan result items so related files (same image stem) appear together.
 *
 * @param array $items Decoded result items.
 * @return array Sorted items.
 */
function pt_wuclean_image_scan_sort_results_clustered(  array $items  ) {
    if ( empty( $items ) ) {
        return $items;
    }
    usort( $items, static function ( $a, $b ) {
        if ( !is_array( $a ) || !is_array( $b ) ) {
            return 0;
        }
        $ka = pt_wuclean_image_scan_result_cluster_stem( $a );
        $kb = pt_wuclean_image_scan_result_cluster_stem( $b );
        if ( $ka !== $kb ) {
            return strcmp( $ka, $kb );
        }
        $ra = pt_wuclean_image_scan_result_type_rank( $a );
        $rb = pt_wuclean_image_scan_result_type_rank( $b );
        if ( $ra !== $rb ) {
            return $ra <=> $rb;
        }
        $fa = '';
        $fb = '';
        if ( isset( $a['data']['filename'] ) && is_string( $a['data']['filename'] ) ) {
            $fa = $a['data']['filename'];
        } elseif ( isset( $a['id'] ) ) {
            $fa = (string) $a['id'];
        }
        if ( isset( $b['data']['filename'] ) && is_string( $b['data']['filename'] ) ) {
            $fb = $b['data']['filename'];
        } elseif ( isset( $b['id'] ) ) {
            $fb = (string) $b['id'];
        }
        return strcmp( $fa, $fb );
    } );
    return $items;
}

/**
 * Pick the best attachment ID for a wanted relative path from candidate meta rows.
 *
 * @param string $want_relpath Wanted _wp_attached_file relative path.
 * @param array  $rows         List of arrays with keys post_id, meta_value.
 * @return int 0 if ambiguous or none.
 */
function pt_wuclean_pick_post_id_for_attached_file_path(  $want_relpath, array $rows  ) {
    $want_full = pt_wuclean_normalize_uploads_attached_relpath( $want_relpath );
    if ( '' === $want_full ) {
        return 0;
    }
    $want_dir = dirname( $want_full );
    if ( '.' === $want_dir ) {
        $want_dir = '';
    }
    $exact = array();
    $dir_match = array();
    $basename_only = array();
    $want_basename = basename( $want_full );
    foreach ( $rows as $r ) {
        if ( !is_array( $r ) ) {
            continue;
        }
        $pid = ( isset( $r['post_id'] ) ? absint( $r['post_id'] ) : 0 );
        $mv = ( isset( $r['meta_value'] ) ? pt_wuclean_normalize_uploads_attached_relpath( (string) $r['meta_value'] ) : '' );
        if ( $pid <= 0 || '' === $mv ) {
            continue;
        }
        if ( $mv === $want_full ) {
            $exact[] = $pid;
            continue;
        }
        $mv_dir = dirname( $mv );
        if ( '.' === $mv_dir ) {
            $mv_dir = '';
        }
        if ( 0 === strcasecmp( (string) $mv_dir, (string) $want_dir ) && basename( $mv ) === $want_basename ) {
            $dir_match[] = $pid;
            continue;
        }
        if ( $mv === $want_basename ) {
            $basename_only[] = $pid;
        }
    }
    if ( !empty( $exact ) ) {
        return (int) min( $exact );
    }
    if ( !empty( $dir_match ) ) {
        return (int) min( $dir_match );
    }
    if ( 1 === count( $basename_only ) ) {
        return (int) $basename_only[0];
    }
    return 0;
}

/**
 * Batch-resolve attachment post IDs from _wp_attached_file relative paths (exact match first, then basename disambiguation).
 *
 * @param string[] $relpaths Relative paths (e.g. "2025/04/foo.jpg").
 * @return array<string,int> Map normalized relpath => post_id (0 if unresolved).
 */
function pt_wuclean_batch_resolve_attachment_ids_from_attached_file_relpaths(  array $relpaths  ) {
    global $wpdb;
    $normalized_keys = array();
    foreach ( $relpaths as $p ) {
        $n = pt_wuclean_normalize_uploads_attached_relpath( ( is_string( $p ) ? $p : '' ) );
        if ( '' !== $n ) {
            $normalized_keys[$n] = true;
        }
    }
    $keys = array_keys( $normalized_keys );
    $result = array();
    foreach ( $keys as $k ) {
        $result[$k] = 0;
    }
    if ( empty( $keys ) ) {
        return $result;
    }
    $chunk_size = 200;
    foreach ( array_chunk( $keys, $chunk_size ) as $chunk ) {
        $placeholders = implode( ', ', array_fill( 0, count( $chunk ), '%s' ) );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- placeholders from count($chunk); values bound via prepare().
        $db_rows = $wpdb->get_results( $wpdb->prepare( "SELECT meta_value, MIN(post_id) AS post_id FROM {$wpdb->postmeta}\n\t\t\t\t WHERE meta_key = '_wp_attached_file'\n\t\t\t\t AND meta_value IN ({$placeholders})\n\t\t\t\t GROUP BY meta_value", ...$chunk ), ARRAY_A );
        // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
        if ( is_array( $db_rows ) ) {
            foreach ( $db_rows as $row ) {
                if ( !is_array( $row ) ) {
                    continue;
                }
                $mv = ( isset( $row['meta_value'] ) ? pt_wuclean_normalize_uploads_attached_relpath( (string) $row['meta_value'] ) : '' );
                $pid = ( isset( $row['post_id'] ) ? absint( $row['post_id'] ) : 0 );
                if ( '' !== $mv && $pid > 0 && array_key_exists( $mv, $result ) ) {
                    $result[$mv] = $pid;
                }
            }
        }
    }
    $missing = array();
    foreach ( $result as $path => $pid ) {
        if ( $pid <= 0 ) {
            $missing[] = $path;
        }
    }
    if ( empty( $missing ) ) {
        return $result;
    }
    $by_basename = array();
    foreach ( $missing as $want ) {
        $b = basename( $want );
        if ( '' === $b ) {
            continue;
        }
        if ( !isset( $by_basename[$b] ) ) {
            $by_basename[$b] = array();
        }
        $by_basename[$b][] = $want;
    }
    foreach ( $by_basename as $basename => $wanted_paths ) {
        $like_suffix = '%/' . $wpdb->esc_like( $basename );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $rows = $wpdb->get_results( $wpdb->prepare( "SELECT pm.post_id, pm.meta_value FROM {$wpdb->postmeta} pm\n\t\t\t\t INNER JOIN {$wpdb->posts} p ON pm.post_id = p.ID\n\t\t\t\t WHERE pm.meta_key = '_wp_attached_file'\n\t\t\t\t AND p.post_type = 'attachment'\n\t\t\t\t AND p.post_status IN ('inherit','trash')\n\t\t\t\t AND (pm.meta_value = %s OR pm.meta_value LIKE %s)", $basename, $like_suffix ) );
        if ( !is_array( $rows ) || empty( $rows ) ) {
            continue;
        }
        $row_list = array();
        foreach ( $rows as $r ) {
            if ( !is_object( $r ) ) {
                continue;
            }
            $row_list[] = array(
                'post_id'    => absint( $r->post_id ?? 0 ),
                'meta_value' => ( isset( $r->meta_value ) ? (string) $r->meta_value : '' ),
            );
        }
        foreach ( $wanted_paths as $want_full ) {
            if ( isset( $result[$want_full] ) && $result[$want_full] > 0 ) {
                continue;
            }
            $picked = pt_wuclean_pick_post_id_for_attached_file_path( $want_full, $row_list );
            if ( $picked > 0 ) {
                $result[$want_full] = $picked;
            }
        }
    }
    return $result;
}

/**
 * Find Media Library ID for a filename in uploads folder.
 *
 * @param string $filename         Image basename (no path segments).
 * @param string $upload_base_dir  Uploads base directory (reserved for path-scoped search).
 * @param string $dir_part         Optional relative directory under uploads (e.g. `2026/05`) to resolve `-scaled` and thumbnails.
 * @return int|false Attachment ID or false.
 */
function pt_wuclean_find_attachment_by_filename(  $filename, $upload_base_dir, $dir_part = ''  ) {
    // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- $upload_base_dir reserved for future path-scoped search
    static $cache = array();
    $filename = ( is_string( $filename ) ? trim( $filename ) : '' );
    if ( '' === $filename ) {
        return false;
    }
    $dir_part = ( is_string( $dir_part ) ? trim( str_replace( '\\', '/', $dir_part ), '/' ) : '' );
    if ( '.' === $dir_part ) {
        $dir_part = '';
    }
    $cache_key = (( '' !== $dir_part ? $dir_part . '/' : '' )) . $filename;
    if ( array_key_exists( $cache_key, $cache ) ) {
        return $cache[$cache_key];
    }
    $candidates = array();
    if ( '' !== $dir_part && function_exists( 'pt_wuclean_attached_file_relpath_resolution_candidates' ) ) {
        foreach ( pt_wuclean_attached_file_relpath_resolution_candidates( $dir_part, $filename ) as $c ) {
            $n = pt_wuclean_normalize_uploads_attached_relpath( $c );
            if ( '' !== $n ) {
                $candidates[] = $n;
            }
        }
    }
    $candidates[] = pt_wuclean_normalize_uploads_attached_relpath( $filename );
    $path_info = pathinfo( $filename );
    $base = ( isset( $path_info['filename'] ) && is_string( $path_info['filename'] ) ? (string) $path_info['filename'] : '' );
    $ext = ( isset( $path_info['extension'] ) && is_string( $path_info['extension'] ) ? strtolower( (string) $path_info['extension'] ) : '' );
    if ( '' !== $base && '' !== $ext && false === strpos( $base, '-scaled' ) ) {
        $candidates[] = pt_wuclean_normalize_uploads_attached_relpath( $base . '-scaled.' . $ext );
    }
    $candidates = array_values( array_filter( array_unique( $candidates ) ) );
    $map = pt_wuclean_batch_resolve_attachment_ids_from_attached_file_relpaths( $candidates );
    $attachment_id = 0;
    foreach ( $candidates as $c ) {
        if ( isset( $map[$c] ) && $map[$c] > 0 ) {
            $attachment_id = (int) $map[$c];
            break;
        }
    }
    $result = ( $attachment_id > 0 ? $attachment_id : false );
    $cache[$cache_key] = $result;
    return $result;
}

/**
 * Check if an attachment ID is actually used (runs same database checks as Phase 1).
 * Returns array with usage status and details for debugging.
 *
 * @param int $attachment_id Attachment ID.
 * @return array{used:bool,reason:null|string}
 */
function pt_wuclean_is_attachment_used(  $attachment_id  ) {
    global $wpdb;
    if ( !$attachment_id || $attachment_id <= 0 ) {
        return array(
            'used'   => false,
            'reason' => null,
        );
    }
    $pt_in_parts = pt_wuclean_image_scan_sql_post_type_in_parts();
    $pt_in_sql = $pt_in_parts['in'];
    $pt_in_types = $pt_in_parts['types'];
    $st_parts = pt_wuclean_image_scan_sql_post_status_in_parts();
    $st_in_sql = $st_parts['in'];
    $st_statuses = $st_parts['statuses'];
    // Get the image filename and URL for more reliable detection.
    $file_path = get_attached_file( $attachment_id );
    $file_url = wp_get_attachment_url( $attachment_id );
    $filename = ( $file_path ? basename( $file_path ) : '' );
    // Checks 1–3: Site Identity and WooCommerce catalog placeholder (cheap; before postmeta and posts scans).
    $custom_logo_id = get_theme_mod( 'custom_logo' );
    if ( $custom_logo_id && intval( $custom_logo_id ) === intval( $attachment_id ) ) {
        return array(
            'used'   => true,
            'reason' => 'custom_logo',
        );
    }
    $site_icon_id = get_option( 'site_icon' );
    if ( $site_icon_id && intval( $site_icon_id ) === intval( $attachment_id ) ) {
        return array(
            'used'   => true,
            'reason' => 'site_icon',
        );
    }
    $wc_placeholder = absint( get_option( 'woocommerce_placeholder_image', 0 ) );
    if ( 0 < $wc_placeholder && absint( $attachment_id ) === $wc_placeholder ) {
        return array(
            'used'   => true,
            'reason' => 'woocommerce_placeholder',
        );
    }
    // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $st_in_sql / $pt_in_sql are IN(%s,...) from sanitized lists; variadic merge.
    // Check 4: Featured image.
    $featured_post_id = $wpdb->get_var( $wpdb->prepare( "SELECT pm.post_id FROM {$wpdb->postmeta} pm\n         INNER JOIN {$wpdb->posts} p ON pm.post_id = p.ID\n         WHERE pm.meta_key = '_thumbnail_id'\n         AND p.post_status IN ({$st_in_sql})\n         AND ( CAST(pm.meta_value AS UNSIGNED) = %d OR pm.meta_value = %s )\n         LIMIT 1", ...array_merge( $st_statuses, array(absint( $attachment_id ), (string) absint( $attachment_id )) ) ) );
    if ( $featured_post_id ) {
        return array(
            'used'    => true,
            'reason'  => 'featured_image',
            'post_id' => $featured_post_id,
        );
    }
    // Check 5: Product gallery.
    $gallery_post_id = $wpdb->get_var( $wpdb->prepare( "SELECT pm.post_id FROM {$wpdb->postmeta} pm\n         INNER JOIN {$wpdb->posts} p ON pm.post_id = p.ID\n         WHERE pm.meta_key = '_product_image_gallery'\n         AND p.post_type IN ('product', 'product_variation')\n         AND p.post_status IN ({$st_in_sql})\n         AND pm.meta_value LIKE %s\n         LIMIT 1", ...array_merge( $st_statuses, array('%' . $wpdb->esc_like( (string) $attachment_id ) . '%') ) ) );
    if ( $gallery_post_id ) {
        return array(
            'used'    => true,
            'reason'  => 'product_gallery',
            'post_id' => $gallery_post_id,
        );
    }
    // Check 6: Term thumbnail.
    $term_id = $wpdb->get_var( $wpdb->prepare( "SELECT term_id FROM {$wpdb->termmeta} \n         WHERE meta_key = 'thumbnail_id'\n         AND ( CAST(meta_value AS UNSIGNED) = %d OR meta_value = %s )\n         LIMIT 1", absint( $attachment_id ), (string) absint( $attachment_id ) ) );
    if ( $term_id ) {
        return array(
            'used'    => true,
            'reason'  => 'term_thumbnail',
            'term_id' => $term_id,
        );
    }
    // Checks 7a–7e: post content / excerpt (wp-image class, filename, URL, registered sizes, basename pattern).
    // Check 7a: wp-image-{id} in post content or excerpt.
    $in_content = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} \n         WHERE post_type IN ({$pt_in_sql})\n         AND (post_content LIKE %s OR post_excerpt LIKE %s)\n         AND post_status IN ({$st_in_sql})\n         LIMIT 1", ...array_merge( $pt_in_types, array('%wp-image-' . $attachment_id . '%', '%wp-image-' . $attachment_id . '%'), $st_statuses ) ) );
    if ( $in_content ) {
        return array(
            'used'    => true,
            'reason'  => 'in_post_content',
            'post_id' => $in_content,
        );
    }
    // Check 7b: Image referenced by filename in post content or excerpt (more reliable for media inserted without wp-image class).
    if ( $filename ) {
        $in_content_by_filename = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} \n             WHERE post_type IN ({$pt_in_sql})\n             AND (post_content LIKE %s OR post_excerpt LIKE %s)\n             AND post_status IN ({$st_in_sql})\n             LIMIT 1", ...array_merge( $pt_in_types, array('%' . $wpdb->esc_like( $filename ) . '%', '%' . $wpdb->esc_like( $filename ) . '%'), $st_statuses ) ) );
        if ( $in_content_by_filename ) {
            return array(
                'used'    => true,
                'reason'  => 'in_post_content',
                'post_id' => $in_content_by_filename,
            );
        }
    }
    // Check 7c: Image referenced by URL in post content or excerpt.
    if ( $file_url ) {
        // Get relative URL for better matching.
        $relative_url = str_replace( home_url(), '', $file_url );
        $in_content_by_url = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} \n             WHERE post_type IN ({$pt_in_sql})\n             AND (post_content LIKE %s OR post_excerpt LIKE %s)\n             AND post_status IN ({$st_in_sql})\n             LIMIT 1", ...array_merge( $pt_in_types, array('%' . $wpdb->esc_like( $relative_url ) . '%', '%' . $wpdb->esc_like( $relative_url ) . '%'), $st_statuses ) ) );
        if ( $in_content_by_url ) {
            return array(
                'used'    => true,
                'reason'  => 'in_post_content',
                'post_id' => $in_content_by_url,
            );
        }
    }
    // Check 7d: WordPress-generated resized/thumbnail versions in content.
    // WordPress creates resized images like image-150x150.jpg, image-1024x538.jpg.
    $metadata = wp_get_attachment_metadata( $attachment_id );
    if ( isset( $metadata['sizes'] ) && is_array( $metadata['sizes'] ) ) {
        foreach ( $metadata['sizes'] as $size_name => $size_data ) {
            if ( isset( $size_data['file'] ) ) {
                $size_filename = $size_data['file'];
                $in_content_by_size = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} \n                     WHERE post_type IN ({$pt_in_sql})\n                     AND (post_content LIKE %s OR post_excerpt LIKE %s)\n                     AND post_status IN ({$st_in_sql})\n                     LIMIT 1", ...array_merge( $pt_in_types, array('%' . $wpdb->esc_like( $size_filename ) . '%', '%' . $wpdb->esc_like( $size_filename ) . '%'), $st_statuses ) ) );
                if ( $in_content_by_size ) {
                    return array(
                        'used'    => true,
                        'reason'  => 'in_post_content_resized',
                        'post_id' => $in_content_by_size,
                        'size'    => $size_name,
                    );
                }
            }
        }
    }
    // Check 7e: Resized filename pattern in post content or excerpt (when not listed in metadata sizes).
    // Example matches: basename-1024x576.jpg, basename-1536x864.png, etc.
    if ( !empty( $filename ) ) {
        $path_info = pathinfo( $filename );
        $base_no_ext = ( isset( $path_info['filename'] ) && is_string( $path_info['filename'] ) ? $path_info['filename'] : '' );
        $ext = ( isset( $path_info['extension'] ) && is_string( $path_info['extension'] ) ? strtolower( $path_info['extension'] ) : '' );
        if ( !empty( $base_no_ext ) && !empty( $ext ) ) {
            // Build LIKE patterns safely: require a dash after basename to indicate a resized variant and confirm extension.
            $like_name = '%' . $wpdb->esc_like( $base_no_ext . '-' ) . '%';
            $like_ext = '%.' . $wpdb->esc_like( $ext ) . '%';
            // Look for both fragments in content and excerpt to reduce false positives.
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $found_id = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts}\n                    WHERE post_type IN ({$pt_in_sql})\n                    AND post_status IN ({$st_in_sql}) AND (\n                        (post_content LIKE %s AND post_content LIKE %s)\n                        OR\n                        (post_excerpt LIKE %s AND post_excerpt LIKE %s)\n                    )\n                    LIMIT 1", ...array_merge( $pt_in_types, $st_statuses, array(
                $like_name,
                $like_ext,
                $like_name,
                $like_ext
            ) ) ) );
            if ( $found_id ) {
                return array(
                    'used'    => true,
                    'reason'  => 'in_post_content_resized',
                    'post_id' => $found_id,
                );
            }
        }
    }
    // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, PluginCheck.Security.DirectDB.UnescapedDBParameter
    // Not found in any common usage location.
    return array(
        'used'   => false,
        'reason' => null,
    );
}

/**
 * Batch refinement function - checks ONLY what the initial scan missed
 * Much more efficient than individual checks
 *
 * This runs ADDITIONAL checks beyond the batch scan:
 * - Filename in content (catches images inserted without wp-image class)
 * - URL in content
 * - Resized/thumbnail versions in content
 * - WooCommerce catalog placeholder (option)
 * - Custom logo
 * - Site icon
 *
 * Does NOT re-check: featured images, galleries, thumbnails, wp-image-{id} (already done in batch)
 *
 * @param array $unused_image_ids Array of image IDs marked as unused.
 * @return array Array of image IDs that are actually used.
 */
function pt_wuclean_batch_refine_unused_images(  $unused_image_ids  ) {
    global $wpdb;
    if ( empty( $unused_image_ids ) ) {
        return array();
    }
    $used_image_ids = array();
    $pt_in_parts = pt_wuclean_image_scan_sql_post_type_in_parts();
    $pt_in_sql = $pt_in_parts['in'];
    $pt_in_types = $pt_in_parts['types'];
    $st_parts = pt_wuclean_image_scan_sql_post_status_in_parts();
    $st_in_sql = $st_parts['in'];
    $st_statuses = $st_parts['statuses'];
    // Get custom logo and site icon (very fast, no queries).
    $custom_logo_id = get_theme_mod( 'custom_logo' );
    $site_icon_id = get_option( 'site_icon' );
    if ( $custom_logo_id && in_array( $custom_logo_id, $unused_image_ids, true ) ) {
        $used_image_ids[] = $custom_logo_id;
    }
    if ( $site_icon_id && in_array( $site_icon_id, $unused_image_ids, true ) ) {
        $used_image_ids[] = $site_icon_id;
    }
    $wc_placeholder = absint( get_option( 'woocommerce_placeholder_image', 0 ) );
    if ( $wc_placeholder > 0 && in_array( $wc_placeholder, $unused_image_ids, true ) ) {
        $used_image_ids[] = $wc_placeholder;
    }
    // Build filename and URL mappings for all images.
    $id_to_filename = array();
    $id_to_url = array();
    $id_to_sizes = array();
    foreach ( $unused_image_ids as $id ) {
        $file_path = get_attached_file( $id );
        if ( $file_path ) {
            $filename = basename( $file_path );
            $id_to_filename[$id] = $filename;
            $file_url = wp_get_attachment_url( $id );
            if ( $file_url ) {
                $relative_url = str_replace( home_url(), '', $file_url );
                $id_to_url[$id] = $relative_url;
            }
            // Get resized versions.
            $metadata = wp_get_attachment_metadata( $id );
            if ( isset( $metadata['sizes'] ) && is_array( $metadata['sizes'] ) ) {
                $id_to_sizes[$id] = array();
                foreach ( $metadata['sizes'] as $size_name => $size_data ) {
                    if ( isset( $size_data['file'] ) ) {
                        $id_to_sizes[$id][] = $size_data['file'];
                    }
                }
            }
        }
    }
    // BATCH CHECK 1: Filenames in content.
    if ( !empty( $id_to_filename ) ) {
        $filename_placeholders = array();
        $filename_args = array();
        foreach ( $id_to_filename as $id => $filename ) {
            $filename_placeholders[] = '(post_content LIKE %s OR post_excerpt LIKE %s)';
            $like = '%' . $wpdb->esc_like( $filename ) . '%';
            $filename_args[] = $like;
            $filename_args[] = $like;
        }
        if ( !empty( $filename_placeholders ) ) {
            // Batch query - check all filenames at once.
            $placeholders = implode( ' OR ', $filename_placeholders );
            // PCP-compliant because prepare() is *directly* inside get_results().
            // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $placeholders SQL structure; $wpdb->posts; variadic $filename_args
            // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- $placeholders contains SQL structure tokens (e.g., "post_content LIKE %s"), not user data. User data is passed separately via $filename_args.
            $posts_with_filenames = $wpdb->get_results( $wpdb->prepare( "SELECT post_content, post_excerpt FROM {$wpdb->posts}\n                     WHERE post_status IN ({$st_in_sql})\n                     AND post_type IN ({$pt_in_sql})\n                     AND ({$placeholders})\n                     LIMIT 500", ...array_merge( $st_statuses, $pt_in_types, $filename_args ) ) );
            // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
            // Match found posts back to image IDs.
            if ( $posts_with_filenames ) {
                foreach ( $posts_with_filenames as $post ) {
                    $combined_content = $post->post_content . ' ' . $post->post_excerpt;
                    foreach ( $id_to_filename as $id => $filename ) {
                        if ( stripos( $combined_content, $filename ) !== false ) {
                            $used_image_ids[] = $id;
                        }
                    }
                }
            }
        }
    }
    // BATCH CHECK 2: URLs in content.
    if ( !empty( $id_to_url ) ) {
        $url_placeholders = array();
        $url_args = array();
        foreach ( $id_to_url as $id => $url ) {
            $url_placeholders[] = '(post_content LIKE %s OR post_excerpt LIKE %s)';
            $like = '%' . $wpdb->esc_like( $url ) . '%';
            $url_args[] = $like;
            $url_args[] = $like;
        }
        if ( !empty( $url_placeholders ) ) {
            // Batch query - check all URLs at once.
            $placeholders = implode( ' OR ', $url_placeholders );
            // PCP-compliant because prepare() is *directly* inside get_results().
            // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $placeholders SQL structure; $wpdb->posts; variadic $url_args
            // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- $placeholders contains SQL structure tokens (e.g., "post_content LIKE %s"), not user data. User data is passed separately via $url_args.
            $posts_with_urls = $wpdb->get_results( $wpdb->prepare( "SELECT post_content, post_excerpt FROM {$wpdb->posts}\n                     WHERE post_status IN ({$st_in_sql})\n                     AND post_type IN ({$pt_in_sql})\n                     AND ({$placeholders})\n                     LIMIT 500", ...array_merge( $st_statuses, $pt_in_types, $url_args ) ) );
            // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
            // Match found posts back to image IDs.
            if ( $posts_with_urls ) {
                foreach ( $posts_with_urls as $post ) {
                    $combined_content = $post->post_content . ' ' . $post->post_excerpt;
                    foreach ( $id_to_url as $id => $url ) {
                        if ( false !== stripos( $combined_content, $url ) ) {
                            $used_image_ids[] = $id;
                        }
                    }
                }
            }
        }
    }
    // BATCH CHECK 3: Resized versions in content.
    if ( !empty( $id_to_sizes ) ) {
        $size_placeholders = array();
        $size_args = array();
        $size_to_id = array();
        // Reverse mapping for matching.
        foreach ( $id_to_sizes as $id => $sizes ) {
            foreach ( $sizes as $size_filename ) {
                $size_placeholders[] = '(post_content LIKE %s OR post_excerpt LIKE %s)';
                $like = '%' . $wpdb->esc_like( $size_filename ) . '%';
                $size_args[] = $like;
                $size_args[] = $like;
                $size_to_id[$size_filename] = $id;
            }
        }
        if ( !empty( $size_placeholders ) ) {
            // Batch query - check all resized versions at once.
            $placeholders = implode( ' OR ', $size_placeholders );
            // PCP-compliant because prepare() is *directly* inside get_results().
            // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $placeholders SQL structure; $wpdb->posts; variadic $size_args
            // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- $placeholders contains SQL structure tokens (e.g., "post_content LIKE %s"), not user data. User data is passed separately via $size_args.
            $posts_with_sizes = $wpdb->get_results( $wpdb->prepare( "SELECT post_content, post_excerpt FROM {$wpdb->posts}\n                     WHERE post_status IN ({$st_in_sql})\n                     AND post_type IN ({$pt_in_sql})\n                     AND ({$placeholders})\n                     LIMIT 500", ...array_merge( $st_statuses, $pt_in_types, $size_args ) ) );
            // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
            // Match found posts back to image IDs.
            if ( $posts_with_sizes ) {
                foreach ( $posts_with_sizes as $post ) {
                    $combined_content = $post->post_content . ' ' . $post->post_excerpt;
                    foreach ( $size_to_id as $size_filename => $id ) {
                        if ( false !== stripos( $combined_content, $size_filename ) ) {
                            $used_image_ids[] = $id;
                        }
                    }
                }
            }
        }
    }
    // BATCH CHECK 4: Pattern-based resized variants (fallback for resized versions not in metadata).
    // Detect resized variants by pattern even if not listed in metadata sizes.
    // Example matches: basename-1024x576.jpg, basename-1536x864.png, etc.
    if ( !empty( $id_to_filename ) ) {
        // Build mapping of base filename patterns to image IDs.
        $pattern_to_ids = array();
        // pattern_key => array of image IDs.
        foreach ( $id_to_filename as $id => $filename ) {
            // Skip if already marked as used.
            if ( in_array( $id, $used_image_ids, true ) ) {
                continue;
            }
            $path_info = pathinfo( $filename );
            $base_no_ext = ( isset( $path_info['filename'] ) && is_string( $path_info['filename'] ) ? $path_info['filename'] : '' );
            $ext = ( isset( $path_info['extension'] ) && is_string( $path_info['extension'] ) ? strtolower( $path_info['extension'] ) : '' );
            if ( !empty( $base_no_ext ) && !empty( $ext ) ) {
                // Build LIKE patterns safely.: require a dash after basename to indicate a resized variant and confirm extension.
                $like_name = '%' . $wpdb->esc_like( $base_no_ext . '-' ) . '%';
                $like_ext = '%.' . $wpdb->esc_like( $ext ) . '%';
                // Create a unique pattern key for this image's pattern.
                $pattern_key = $base_no_ext . '|' . $ext;
                if ( !isset( $pattern_to_ids[$pattern_key] ) ) {
                    $pattern_to_ids[$pattern_key] = array();
                }
                $pattern_to_ids[$pattern_key][] = $id;
            }
        }
        // Process each unique pattern.
        foreach ( $pattern_to_ids as $pattern_key => $image_ids ) {
            list( $base_no_ext, $ext ) = explode( '|', $pattern_key );
            // Build LIKE patterns safely.
            $like_name = '%' . $wpdb->esc_like( $base_no_ext . '-' ) . '%';
            $like_ext = '%.' . $wpdb->esc_like( $ext ) . '%';
            // Query for posts containing this pattern.
            // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- $pt_in_sql / $st_in_sql from sanitized lists; variadic merge.
            $found_id = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts}\n                    WHERE post_type IN ({$pt_in_sql})\n                    AND post_status IN ({$st_in_sql}) AND (\n                        (post_content LIKE %s AND post_content LIKE %s)\n                        OR\n                        (post_excerpt LIKE %s AND post_excerpt LIKE %s)\n                    )\n                    LIMIT 1", ...array_merge( $pt_in_types, $st_statuses, array(
                $like_name,
                $like_ext,
                $like_name,
                $like_ext
            ) ) ) );
            // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            if ( $found_id ) {
                // Mark all images with this pattern as used.
                foreach ( $image_ids as $id ) {
                    $used_image_ids[] = $id;
                }
            }
        }
    }
    // Remove duplicates.
    $used_image_ids = array_unique( $used_image_ids );
    return $used_image_ids;
}

// ============================================================================
// AJAX HANDLERS.
// ============================================================================
/**
 * AJAX: Start image scan
 */
function pt_wuclean_ajax_start_image_scan() {
    if ( ob_get_length() ) {
        ob_clean();
    }
    // Check permissions first.
    if ( !current_user_can( pt_wuclean_get_required_capability() ) ) {
        wp_send_json_error( __( 'Insufficient permissions', 'wuclean-database-cleaner-for-woocommerce' ) );
        return;
    }
    // Check nonce.
    if ( !isset( $_POST['nonce'] ) || !wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'pt_wuclean_image_scan' ) ) {
        wp_send_json_error( __( 'Security check failed', 'wuclean-database-cleaner-for-woocommerce' ) );
        return;
    }
    // Acquire lock so scheduled scans can't clobber manual scans (and vice versa).
    if ( !pt_wuclean_image_scan_lock_acquire( 'manual' ) ) {
        wp_send_json_error( __( 'Another image scan is already running. Please wait for it to finish.', 'wuclean-database-cleaner-for-woocommerce' ) );
        return;
    }
    $settings = get_option( 'pt_wuclean_image_settings', array() );
    $scope_raw = ( isset( $_POST['scope'] ) ? sanitize_text_field( wp_unslash( $_POST['scope'] ) ) : '' );
    if ( !in_array( $scope_raw, array('media_only', 'media_and_uploads'), true ) ) {
        $scope_raw = 'media_only';
    }
    $allow_combined_scan = false;
    if ( 'media_and_uploads' === $scope_raw ) {
        if ( !$allow_combined_scan ) {
            pt_wuclean_image_scan_lock_release( 'manual' );
            wp_send_json_error( array(
                'message' => __( 'The combined Media Library and uploads folder scan is not available in this version.', 'wuclean-database-cleaner-for-woocommerce' ),
            ) );
            return;
        }
    }
    $scope = pt_wuclean_image_scan_engine_normalize_scan_scope( $scope_raw );
    $settings['scan_scope'] = $scope;
    $started = pt_wuclean_image_scan_engine_start_new_run( 'manual', $settings );
    if ( is_wp_error( $started ) ) {
        pt_wuclean_image_scan_lock_release( 'manual' );
        wp_send_json_error( array(
            'code'    => $started->get_error_code(),
            'message' => $started->get_error_message(),
        ) );
        return;
    }
    wp_send_json_success( array(
        'message'      => __( 'Scan started', 'wuclean-database-cleaner-for-woocommerce' ),
        'run_id'       => absint( $started['run_id'] ),
        'total_images' => ( isset( $started['progress']['total_images'] ) ? absint( $started['progress']['total_images'] ) : 0 ),
        'batch_size'   => ( isset( $started['progress']['batch_size'] ) ? absint( $started['progress']['batch_size'] ) : pt_wuclean_image_scan_resolve_batch_size( ( is_array( $settings ) ? $settings : array() ), 'ajax' ) ),
    ) );
}

/**
 * AJAX: Process scan batch (using proven bulk query pattern)
 */
function pt_wuclean_ajax_process_scan_batch() {
    if ( ob_get_length() ) {
        ob_clean();
    }
    pt_wuclean_image_scan_engine_apply_request_time_limit();
    // Check permissions first.
    if ( !current_user_can( pt_wuclean_get_required_capability() ) ) {
        wp_send_json_error( __( 'Insufficient permissions', 'wuclean-database-cleaner-for-woocommerce' ) );
        return;
    }
    // Check nonce.
    if ( !isset( $_POST['nonce'] ) || !wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'pt_wuclean_image_scan' ) ) {
        wp_send_json_error( __( 'Security check failed', 'wuclean-database-cleaner-for-woocommerce' ) );
        return;
    }
    // Ensure the manual scan still owns the lock.
    if ( !pt_wuclean_image_scan_lock_acquire( 'manual' ) ) {
        wp_send_json_error( __( 'Another image scan is already running. Please wait for it to finish.', 'wuclean-database-cleaner-for-woocommerce' ) );
        return;
    }
    pt_wuclean_image_scan_lock_touch( 'manual' );
    $latest = pt_wuclean_image_scan_db_get_latest_run( 'running' );
    if ( !is_array( $latest ) || empty( $latest['id'] ) || 'manual' !== $latest['mode'] ) {
        wp_send_json_error( __( 'No active scan found', 'wuclean-database-cleaner-for-woocommerce' ) );
        return;
    }
    $result = pt_wuclean_image_scan_engine_advance_run( absint( $latest['id'] ), 'manual' );
    if ( is_wp_error( $result ) ) {
        wp_send_json_error( $result->get_error_message() );
        return;
    }
    if ( !empty( $result['completed'] ) ) {
        pt_wuclean_image_scan_lock_release( 'manual' );
    }
    wp_send_json_success( $result );
}

/**
 * AJAX: Stop scan.
 */
function pt_wuclean_ajax_stop_image_scan() {
    if ( ob_get_length() ) {
        ob_clean();
    }
    // Check permissions first.
    if ( !current_user_can( pt_wuclean_get_required_capability() ) ) {
        wp_send_json_error( __( 'Insufficient permissions', 'wuclean-database-cleaner-for-woocommerce' ) );
        return;
    }
    // Check nonce.
    if ( !isset( $_POST['nonce'] ) || !wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'pt_wuclean_image_scan' ) ) {
        wp_send_json_error( __( 'Security check failed', 'wuclean-database-cleaner-for-woocommerce' ) );
        return;
    }
    $latest = pt_wuclean_image_scan_db_get_latest_run();
    if ( is_array( $latest ) && isset( $latest['id'], $latest['mode'] ) && 'manual' === $latest['mode'] ) {
        pt_wuclean_image_scan_db_delete_run( absint( $latest['id'] ) );
    }
    pt_wuclean_image_scan_lock_release( 'manual' );
    wp_send_json_success( __( 'Scan cancelled', 'wuclean-database-cleaner-for-woocommerce' ) );
}

/**
 * AJAX: Resume manual scan.
 */
function pt_wuclean_ajax_resume_image_scan() {
    if ( ob_get_length() ) {
        ob_clean();
    }
    pt_wuclean_image_scan_engine_apply_request_time_limit();
    if ( !current_user_can( pt_wuclean_get_required_capability() ) ) {
        wp_send_json_error( __( 'Insufficient permissions', 'wuclean-database-cleaner-for-woocommerce' ) );
        return;
    }
    if ( !isset( $_POST['nonce'] ) || !wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'pt_wuclean_image_scan' ) ) {
        wp_send_json_error( __( 'Security check failed', 'wuclean-database-cleaner-for-woocommerce' ) );
        return;
    }
    if ( !pt_wuclean_image_scan_lock_acquire( 'manual' ) ) {
        wp_send_json_error( __( 'Another image scan is already running. Please wait for it to finish.', 'wuclean-database-cleaner-for-woocommerce' ) );
        return;
    }
    $latest = pt_wuclean_image_scan_db_get_latest_run();
    if ( !is_array( $latest ) || empty( $latest['id'] ) || 'manual' !== $latest['mode'] ) {
        pt_wuclean_image_scan_lock_release( 'manual' );
        wp_send_json_error( __( 'No paused scan found', 'wuclean-database-cleaner-for-woocommerce' ) );
        return;
    }
    if ( 'stopped' === $latest['status'] || 'failed' === $latest['status'] ) {
        pt_wuclean_image_scan_db_update_status( absint( $latest['id'] ), 'running' );
    }
    wp_send_json_success( array(
        'message' => __( 'Scan resumed', 'wuclean-database-cleaner-for-woocommerce' ),
        'run_id'  => absint( $latest['id'] ),
    ) );
}

/**
 * Ensure we have a filesystem instance available for safe file operations.
 * Used for restore, delete, and other safe file moves in the admin.
 *
 * @return WP_Filesystem_Base|null
 */
function pt_wuclean_get_filesystem() {
    global $wp_filesystem;
    if ( !function_exists( 'WP_Filesystem' ) ) {
        require_once ABSPATH . 'wp-admin/includes/file.php';
    }
    if ( empty( $wp_filesystem ) || !$wp_filesystem instanceof WP_Filesystem_Base ) {
        WP_Filesystem();
    }
    if ( !$wp_filesystem instanceof WP_Filesystem_Base ) {
        if ( !class_exists( 'WP_Filesystem_Direct' ) ) {
            require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php';
            require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php';
        }
        // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Intentional; WP_Filesystem() did not set $wp_filesystem, fallback to Direct.
        $wp_filesystem = new WP_Filesystem_Direct(new stdClass());
    }
    return ( $wp_filesystem instanceof WP_Filesystem_Base ? $wp_filesystem : null );
}

if ( !function_exists( 'pt_wuclean_move_file' ) ) {
    /**
     * Move a file using the WordPress filesystem API.
     *
     * Used by trash/restore and related file moves in the admin.
     *
     * @param string $source      Absolute path to the source file.
     * @param string $destination Absolute path to the destination.
     * @return bool True on success, false on failure.
     */
    function pt_wuclean_move_file(  $source, $destination  ) {
        // Cast to string to prevent PHP 8.1+ deprecation warnings if null/false passed to wp_normalize_path.
        $source = ( is_string( $source ) ? $source : '' );
        $destination = ( is_string( $destination ) ? $destination : '' );
        if ( empty( $source ) || empty( $destination ) ) {
            return false;
        }
        $source_normalized = wp_normalize_path( $source );
        $destination_normalized = wp_normalize_path( $destination );
        $filesystem = pt_wuclean_get_filesystem();
        if ( !$filesystem ) {
            return false;
        }
        if ( $filesystem->exists( $destination_normalized ) ) {
            $filesystem->delete( $destination_normalized );
        }
        return $filesystem->move( $source_normalized, $destination_normalized, true );
    }

}
if ( !function_exists( 'pt_wuclean_move_webp_avif_siblings_of_file_to_trash' ) ) {
    /**
     * Move WebP/AVIF siblings next to a source image into the plugin trash (same naming as main attachment trash).
     *
     * @param string $reference_image_path Absolute path to the image file (used for dirname/basename; the image file may already have been moved).
     * @param int    $image_id             Attachment ID.
     * @param string $trash_dir_system     Trash directory with system directory separators.
     * @return void
     */
    function pt_wuclean_move_webp_avif_siblings_of_file_to_trash(  $reference_image_path, $image_id, $trash_dir_system  ) {
        $reference_image_path = ( is_string( $reference_image_path ) ? $reference_image_path : '' );
        $trash_dir_system = ( is_string( $trash_dir_system ) ? $trash_dir_system : '' );
        $image_id = absint( $image_id );
        if ( '' === $reference_image_path || $image_id <= 0 || '' === $trash_dir_system ) {
            return;
        }
        if ( !function_exists( 'pt_wuclean_move_file' ) ) {
            return;
        }
        $path_info = pathinfo( $reference_image_path );
        $dir = ( isset( $path_info['dirname'] ) ? $path_info['dirname'] : '' );
        $file_base = ( isset( $path_info['filename'] ) ? $path_info['filename'] : '' );
        if ( '' === $dir || '' === $file_base ) {
            return;
        }
        $sibling_formats = array('webp', 'avif');
        foreach ( $sibling_formats as $format ) {
            // Check for image.webp or image.avif (replaced extension).
            $sibling_path = $dir . DIRECTORY_SEPARATOR . $file_base . '.' . $format;
            if ( file_exists( $sibling_path ) ) {
                $trash_sibling_system = $trash_dir_system . DIRECTORY_SEPARATOR . $image_id . '_' . $file_base . '.' . $format;
                pt_wuclean_move_file( $sibling_path, $trash_sibling_system );
            }
            // Check for image.jpg.webp or image.jpg.avif (appended format).
            $sibling_appended = $reference_image_path . '.' . $format;
            if ( file_exists( $sibling_appended ) ) {
                $trash_sibling_system = $trash_dir_system . DIRECTORY_SEPARATOR . $image_id . '_' . basename( $reference_image_path ) . '.' . $format;
                pt_wuclean_move_file( $sibling_appended, $trash_sibling_system );
            }
        }
    }

}
if ( !function_exists( 'pt_wuclean_get_big_image_original_absolute_path' ) ) {
    /**
     * Resolve the on-disk path for WordPress "big image" original (pre-scaled) when present.
     *
     * @param int    $image_id              Attachment ID.
     * @param array  $metadata              Attachment metadata array (may be empty).
     * @param string $main_attached_abs_path Absolute path to the current main attached file (usually *-scaled.jpg).
     * @return string|false Absolute path, or false when not applicable.
     */
    function pt_wuclean_get_big_image_original_absolute_path(  $image_id, $metadata, $main_attached_abs_path  ) {
        $image_id = absint( $image_id );
        if ( $image_id <= 0 || !is_string( $main_attached_abs_path ) || '' === $main_attached_abs_path ) {
            return false;
        }
        if ( !is_array( $metadata ) ) {
            $metadata = array();
        }
        $candidate = '';
        if ( function_exists( 'wp_get_original_image_path' ) ) {
            $p = wp_get_original_image_path( $image_id );
            if ( is_string( $p ) && '' !== $p ) {
                $candidate = $p;
            }
        }
        if ( '' === $candidate && !empty( $metadata['original_image'] ) && is_string( $metadata['original_image'] ) ) {
            $orig_name = basename( $metadata['original_image'] );
            if ( '' !== $orig_name ) {
                $candidate = dirname( $main_attached_abs_path ) . DIRECTORY_SEPARATOR . $orig_name;
            }
        }
        if ( '' === $candidate ) {
            return false;
        }
        return $candidate;
    }

}
if ( !function_exists( 'pt_wuclean_restore_trashed_file_to_destination' ) ) {
    /**
     * Move a file from the plugin trash back to its original location (mirrors restore AJAX move logic).
     *
     * @param string $trash_path_system Absolute path to the file inside the trash directory.
     * @param string $dest_path_system  Absolute destination path.
     * @return bool True when the file was moved or copied to the destination.
     */
    function pt_wuclean_restore_trashed_file_to_destination(  $trash_path_system, $dest_path_system  ) {
        $trash_path_system = ( is_string( $trash_path_system ) ? $trash_path_system : '' );
        $dest_path_system = ( is_string( $dest_path_system ) ? $dest_path_system : '' );
        if ( '' === $trash_path_system || '' === $dest_path_system || !file_exists( $trash_path_system ) ) {
            return false;
        }
        $dest_dir = dirname( $dest_path_system );
        if ( !file_exists( $dest_dir ) ) {
            wp_mkdir_p( $dest_dir );
        }
        if ( function_exists( 'pt_wuclean_move_file' ) ) {
            // phpcs:ignore Universal.ControlStructures.DisallowLonelyIf.Found -- priority check for move_file; else is fallback chain
            return (bool) pt_wuclean_move_file( $trash_path_system, $dest_path_system );
        }
        $filesystem = pt_wuclean_get_filesystem();
        if ( $filesystem ) {
            $trash_path_normalized = wp_normalize_path( $trash_path_system );
            $dest_path_normalized = wp_normalize_path( $dest_path_system );
            if ( $filesystem->exists( $dest_path_normalized ) ) {
                $filesystem->delete( $dest_path_normalized );
            }
            return (bool) $filesystem->move( $trash_path_normalized, $dest_path_normalized, true );
        }
        if ( @copy( $trash_path_system, $dest_path_system ) ) {
            // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- copy as fallback when WP_Filesystem unavailable
            wp_delete_file( $trash_path_system );
            return true;
        }
        return false;
    }

}
if ( !function_exists( 'pt_wuclean_move_media_library_attachment_files_to_trash' ) ) {
    /**
     * Move a Media Library attachment's files (main, sizes, WebP/AVIF siblings, big-image original + its siblings) to the plugin trash folder.
     *
     * Does not update post meta, call wp_trash_post(), or modify the image scan DB — callers do that after success.
     *
     * @param int $image_id Attachment ID.
     * @return array|WP_Error {
     *     @type string $trash_path            Trash path with forward slashes (for _pt_wuclean_trash_path).
     *     @type string $file_path_normalized Original absolute path with forward slashes.
     *     @type string $filename             Basename of the main file.
     * }
     */
    function pt_wuclean_move_media_library_attachment_files_to_trash(  $image_id  ) {
        $image_id = absint( $image_id );
        if ( $image_id <= 0 ) {
            return new WP_Error('pt_wuclean_trash_invalid_id', __( 'Invalid image ID', 'wuclean-database-cleaner-for-woocommerce' ));
        }
        if ( !function_exists( 'pt_wuclean_move_file' ) ) {
            return new WP_Error('pt_wuclean_trash_no_move', __( 'Failed to move image file to trash', 'wuclean-database-cleaner-for-woocommerce' ));
        }
        $file_path = get_attached_file( $image_id );
        if ( !$file_path || !file_exists( $file_path ) ) {
            return new WP_Error('pt_wuclean_trash_file_missing', __( 'Image file not found', 'wuclean-database-cleaner-for-woocommerce' ));
        }
        $trash_location = pt_wuclean_get_trash_location();
        $trash_dir = str_replace( '\\', '/', rtrim( $trash_location['dir'], '/\\' ) );
        $trash_dir_system = str_replace( '/', DIRECTORY_SEPARATOR, $trash_dir );
        if ( !file_exists( $trash_dir_system ) ) {
            wp_mkdir_p( $trash_dir_system );
        }
        $filename = basename( $file_path );
        $trash_path = $trash_dir . '/' . $image_id . '_' . $filename;
        $trash_path_system = str_replace( '/', DIRECTORY_SEPARATOR, $trash_path );
        if ( !pt_wuclean_move_file( $file_path, $trash_path_system ) ) {
            return new WP_Error('pt_wuclean_trash_move_failed', __( 'Failed to move image file to trash', 'wuclean-database-cleaner-for-woocommerce' ));
        }
        // Move all thumbnail sizes.
        $metadata = wp_get_attachment_metadata( $image_id );
        if ( !is_array( $metadata ) ) {
            $metadata = array();
        }
        if ( isset( $metadata['sizes'] ) && is_array( $metadata['sizes'] ) ) {
            $upload_path = dirname( $file_path );
            foreach ( $metadata['sizes'] as $size => $size_data ) {
                $thumb_file = $upload_path . DIRECTORY_SEPARATOR . $size_data['file'];
                if ( file_exists( $thumb_file ) ) {
                    $trash_thumb_system = $trash_dir_system . DIRECTORY_SEPARATOR . $image_id . '_' . $size_data['file'];
                    pt_wuclean_move_file( $thumb_file, $trash_thumb_system );
                    // Also move WebP/AVIF siblings for this thumbnail.
                    $thumb_path_info = pathinfo( $thumb_file );
                    $thumb_dir = $thumb_path_info['dirname'];
                    $thumb_base = $thumb_path_info['filename'];
                    $sibling_formats = array('webp', 'avif');
                    foreach ( $sibling_formats as $format ) {
                        // Check for thumbnail.webp or thumbnail.avif (replaced extension).
                        $thumb_sibling_path = $thumb_dir . DIRECTORY_SEPARATOR . $thumb_base . '.' . $format;
                        if ( file_exists( $thumb_sibling_path ) ) {
                            $trash_thumb_sibling_system = $trash_dir_system . DIRECTORY_SEPARATOR . $image_id . '_' . $thumb_base . '.' . $format;
                            pt_wuclean_move_file( $thumb_sibling_path, $trash_thumb_sibling_system );
                        }
                        // Check for thumbnail.jpg.webp or thumbnail.jpg.avif (appended format).
                        $thumb_sibling_appended = $thumb_file . '.' . $format;
                        if ( file_exists( $thumb_sibling_appended ) ) {
                            $trash_thumb_sibling_system = $trash_dir_system . DIRECTORY_SEPARATOR . $image_id . '_' . $size_data['file'] . '.' . $format;
                            pt_wuclean_move_file( $thumb_sibling_appended, $trash_thumb_sibling_system );
                        }
                    }
                }
            }
        }
        // Move any sibling files (WebP/AVIF) for the main (e.g. scaled) image.
        pt_wuclean_move_webp_avif_siblings_of_file_to_trash( $file_path, $image_id, $trash_dir_system );
        // Move WordPress "big image" original (pre-scaled) and its WebP/AVIF siblings when they exist beside the scaled file.
        $big_original_path = pt_wuclean_get_big_image_original_absolute_path( $image_id, $metadata, $file_path );
        if ( false !== $big_original_path && file_exists( $big_original_path ) ) {
            $main_norm = wp_normalize_path( (string) $file_path );
            $orig_norm = wp_normalize_path( $big_original_path );
            if ( $orig_norm !== $main_norm ) {
                $orig_basename = basename( $big_original_path );
                $orig_trash_system = $trash_dir_system . DIRECTORY_SEPARATOR . $image_id . '_' . $orig_basename;
                $orig_moved_to_trash = pt_wuclean_move_file( $big_original_path, $orig_trash_system );
                if ( $orig_moved_to_trash ) {
                    pt_wuclean_move_webp_avif_siblings_of_file_to_trash( $big_original_path, $image_id, $trash_dir_system );
                }
            }
        }
        $file_path_normalized = str_replace( '\\', '/', $file_path );
        return array(
            'trash_path'           => $trash_path,
            'file_path_normalized' => $file_path_normalized,
            'filename'             => $filename,
        );
    }

}
/**
 * AJAX: Restore image from trash.
 */
function pt_wuclean_ajax_restore_image() {
    ob_clean();
    if ( !isset( $_POST['nonce'] ) || !wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'pt_wuclean_restore_image' ) ) {
        wp_send_json_error( __( 'Security check failed', 'wuclean-database-cleaner-for-woocommerce' ) );
        return;
    }
    if ( !current_user_can( pt_wuclean_get_required_capability() ) ) {
        wp_send_json_error( __( 'Insufficient permissions', 'wuclean-database-cleaner-for-woocommerce' ) );
    }
    $image_id = ( isset( $_POST['image_id'] ) ? intval( $_POST['image_id'] ) : 0 );
    if ( !$image_id ) {
        wp_send_json_error( __( 'Invalid image ID', 'wuclean-database-cleaner-for-woocommerce' ) );
    }
    // Get trash and original paths from meta (stored with forward slashes).
    // Check new meta key first, fallback to old for backward compatibility.
    $trash_path = get_post_meta( $image_id, '_pt_wuclean_trash_path', true );
    if ( empty( $trash_path ) ) {
        $trash_path = get_post_meta( $image_id, '_wuclean_trash_path', true );
        if ( !empty( $trash_path ) ) {
            update_post_meta( $image_id, '_pt_wuclean_trash_path', $trash_path );
            delete_post_meta( $image_id, '_wuclean_trash_path' );
        }
    }
    $original_path = get_post_meta( $image_id, '_pt_wuclean_original_path', true );
    if ( empty( $original_path ) ) {
        $original_path = get_post_meta( $image_id, '_wuclean_original_path', true );
        if ( !empty( $original_path ) ) {
            update_post_meta( $image_id, '_pt_wuclean_original_path', $original_path );
            delete_post_meta( $image_id, '_wuclean_original_path' );
        }
    }
    // Convert to system paths for file operations.
    $trash_path_system = ( !empty( $trash_path ) && is_string( $trash_path ) ? str_replace( '/', DIRECTORY_SEPARATOR, $trash_path ) : '' );
    $original_path_system = ( !empty( $original_path ) && is_string( $original_path ) ? str_replace( '/', DIRECTORY_SEPARATOR, $original_path ) : '' );
    if ( $trash_path_system && $original_path_system && file_exists( $trash_path_system ) ) {
        // Ensure the destination directory exists.
        $original_dir = dirname( $original_path_system );
        if ( !file_exists( $original_dir ) ) {
            wp_mkdir_p( $original_dir );
        }
        // Restore main file.
        $restored = pt_wuclean_move_file( $trash_path_system, $original_path_system );
        if ( !$restored ) {
            $dest_dir = dirname( $original_path_system );
            if ( !file_exists( $dest_dir ) ) {
                wp_mkdir_p( $dest_dir );
            }
            $filesystem = pt_wuclean_get_filesystem();
            if ( $filesystem ) {
                $trash_path_normalized = wp_normalize_path( $trash_path_system );
                $original_path_normalized = wp_normalize_path( $original_path_system );
                if ( $filesystem->exists( $original_path_normalized ) ) {
                    $filesystem->delete( $original_path_normalized );
                }
                $restored = $filesystem->move( $trash_path_normalized, $original_path_normalized, true );
            } elseif ( @copy( $trash_path_system, $original_path_system ) ) {
                // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- copy as fallback when WP_Filesystem unavailable
                wp_delete_file( $trash_path_system );
                $restored = true;
            }
        }
        if ( !$restored ) {
            wp_send_json_error( __( 'Failed to restore image file', 'wuclean-database-cleaner-for-woocommerce' ) );
        }
        // Restore all thumbnail sizes.
        $metadata = wp_get_attachment_metadata( $image_id );
        if ( !is_array( $metadata ) ) {
            $metadata = array();
        }
        $trash_location = pt_wuclean_get_trash_location();
        $trash_dir = str_replace( '\\', '/', rtrim( $trash_location['dir'], '/\\' ) );
        $trash_dir_system = str_replace( '/', DIRECTORY_SEPARATOR, $trash_dir );
        if ( isset( $metadata['sizes'] ) && is_array( $metadata['sizes'] ) ) {
            $upload_path = dirname( $original_path_system );
            // Ensure upload directory exists.
            if ( !file_exists( $upload_path ) ) {
                wp_mkdir_p( $upload_path );
            }
            foreach ( $metadata['sizes'] as $size => $size_data ) {
                $trash_thumb = $trash_dir_system . DIRECTORY_SEPARATOR . $image_id . '_' . $size_data['file'];
                $original_thumb = $upload_path . DIRECTORY_SEPARATOR . $size_data['file'];
                if ( file_exists( $trash_thumb ) ) {
                    if ( !pt_wuclean_move_file( $trash_thumb, $original_thumb ) ) {
                        $filesystem = pt_wuclean_get_filesystem();
                        if ( $filesystem ) {
                            $trash_thumb_normalized = wp_normalize_path( $trash_thumb );
                            $original_thumb_normalized = wp_normalize_path( $original_thumb );
                            if ( $filesystem->exists( $original_thumb_normalized ) ) {
                                $filesystem->delete( $original_thumb_normalized );
                            }
                            $filesystem->move( $trash_thumb_normalized, $original_thumb_normalized, true );
                        } elseif ( @copy( $trash_thumb, $original_thumb ) ) {
                            // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- copy as fallback when WP_Filesystem unavailable
                            wp_delete_file( $trash_thumb );
                        }
                    }
                }
                // Also restore WebP/AVIF siblings for this thumbnail.
                $thumb_path_info = pathinfo( $original_thumb );
                $thumb_dir = $thumb_path_info['dirname'];
                $thumb_base = $thumb_path_info['filename'];
                $sibling_formats = array('webp', 'avif');
                foreach ( $sibling_formats as $format ) {
                    // Check for thumbnail.webp or thumbnail.avif in trash (replaced extension).
                    $trash_thumb_sibling = $trash_dir_system . DIRECTORY_SEPARATOR . $image_id . '_' . $thumb_base . '.' . $format;
                    if ( file_exists( $trash_thumb_sibling ) ) {
                        $original_thumb_sibling = $thumb_dir . DIRECTORY_SEPARATOR . $thumb_base . '.' . $format;
                        if ( !pt_wuclean_move_file( $trash_thumb_sibling, $original_thumb_sibling ) ) {
                            $filesystem = pt_wuclean_get_filesystem();
                            if ( $filesystem ) {
                                $trash_thumb_sibling_normalized = wp_normalize_path( $trash_thumb_sibling );
                                $original_thumb_sibling_normalized = wp_normalize_path( $original_thumb_sibling );
                                if ( $filesystem->exists( $original_thumb_sibling_normalized ) ) {
                                    $filesystem->delete( $original_thumb_sibling_normalized );
                                }
                                $filesystem->move( $trash_thumb_sibling_normalized, $original_thumb_sibling_normalized, true );
                            } elseif ( @copy( $trash_thumb_sibling, $original_thumb_sibling ) ) {
                                // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- copy as fallback when WP_Filesystem unavailable
                                wp_delete_file( $trash_thumb_sibling );
                            }
                        }
                    }
                    // Check for thumbnail.jpg.webp or thumbnail.jpg.avif in trash (appended format).
                    $trash_thumb_sibling_appended = $trash_dir_system . DIRECTORY_SEPARATOR . $image_id . '_' . $size_data['file'] . '.' . $format;
                    if ( file_exists( $trash_thumb_sibling_appended ) ) {
                        $original_thumb_sibling_appended = $original_thumb . '.' . $format;
                        if ( !pt_wuclean_move_file( $trash_thumb_sibling_appended, $original_thumb_sibling_appended ) ) {
                            $filesystem = pt_wuclean_get_filesystem();
                            if ( $filesystem ) {
                                $trash_thumb_sibling_appended_normalized = wp_normalize_path( $trash_thumb_sibling_appended );
                                $original_thumb_sibling_appended_normalized = wp_normalize_path( $original_thumb_sibling_appended );
                                if ( $filesystem->exists( $original_thumb_sibling_appended_normalized ) ) {
                                    $filesystem->delete( $original_thumb_sibling_appended_normalized );
                                }
                                $filesystem->move( $trash_thumb_sibling_appended_normalized, $original_thumb_sibling_appended_normalized, true );
                            } elseif ( @copy( $trash_thumb_sibling_appended, $original_thumb_sibling_appended ) ) {
                                // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- copy as fallback when WP_Filesystem unavailable
                                wp_delete_file( $trash_thumb_sibling_appended );
                            }
                        }
                    }
                }
            }
        }
        // Restore any sibling files (WebP/AVIF) for the main image.
        $path_info = pathinfo( $original_path_system );
        $dir = $path_info['dirname'];
        $file_base = $path_info['filename'];
        $sibling_formats = array('webp', 'avif');
        foreach ( $sibling_formats as $format ) {
            // Check for image.webp or image.avif in trash.
            $trash_sibling = $trash_dir_system . DIRECTORY_SEPARATOR . $image_id . '_' . $file_base . '.' . $format;
            if ( file_exists( $trash_sibling ) ) {
                $original_sibling = $dir . DIRECTORY_SEPARATOR . $file_base . '.' . $format;
                if ( !pt_wuclean_move_file( $trash_sibling, $original_sibling ) ) {
                    $filesystem = pt_wuclean_get_filesystem();
                    if ( $filesystem ) {
                        $trash_sibling_normalized = wp_normalize_path( $trash_sibling );
                        $original_sibling_normalized = wp_normalize_path( $original_sibling );
                        if ( $filesystem->exists( $original_sibling_normalized ) ) {
                            $filesystem->delete( $original_sibling_normalized );
                        }
                        $filesystem->move( $trash_sibling_normalized, $original_sibling_normalized, true );
                    } elseif ( @copy( $trash_sibling, $original_sibling ) ) {
                        // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- copy as fallback when WP_Filesystem unavailable
                        wp_delete_file( $trash_sibling );
                    }
                }
            }
            // Check for image.jpg.webp or image.jpg.avif (appended format) in trash.
            $trash_sibling_appended = $trash_dir_system . DIRECTORY_SEPARATOR . $image_id . '_' . basename( $original_path_system ) . '.' . $format;
            if ( file_exists( $trash_sibling_appended ) ) {
                $original_sibling_appended = $original_path_system . '.' . $format;
                if ( !pt_wuclean_move_file( $trash_sibling_appended, $original_sibling_appended ) ) {
                    $filesystem = pt_wuclean_get_filesystem();
                    if ( $filesystem ) {
                        $trash_sibling_appended_normalized = wp_normalize_path( $trash_sibling_appended );
                        $original_sibling_appended_normalized = wp_normalize_path( $original_sibling_appended );
                        if ( $filesystem->exists( $original_sibling_appended_normalized ) ) {
                            $filesystem->delete( $original_sibling_appended_normalized );
                        }
                        $filesystem->move( $trash_sibling_appended_normalized, $original_sibling_appended_normalized, true );
                    } elseif ( @copy( $trash_sibling_appended, $original_sibling_appended ) ) {
                        // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- copy as fallback when WP_Filesystem unavailable
                        wp_delete_file( $trash_sibling_appended );
                    }
                }
            }
        }
        // Restore WordPress "big image" original (pre-scaled) and WebP/AVIF siblings when they were moved to trash.
        $big_original_dest_system = pt_wuclean_get_big_image_original_absolute_path( $image_id, $metadata, $original_path_system );
        if ( false !== $big_original_dest_system ) {
            $main_restore_norm = wp_normalize_path( $original_path_system );
            $big_restore_norm = wp_normalize_path( $big_original_dest_system );
            if ( $big_restore_norm !== $main_restore_norm ) {
                $big_basename = basename( $big_original_dest_system );
                $trash_big_original = $trash_dir_system . DIRECTORY_SEPARATOR . $image_id . '_' . $big_basename;
                if ( file_exists( $trash_big_original ) ) {
                    pt_wuclean_restore_trashed_file_to_destination( $trash_big_original, $big_original_dest_system );
                }
                $path_info_big = pathinfo( $big_original_dest_system );
                $dir_big = ( isset( $path_info_big['dirname'] ) ? $path_info_big['dirname'] : '' );
                $file_base_big = ( isset( $path_info_big['filename'] ) ? $path_info_big['filename'] : '' );
                $sibling_formats_big = array('webp', 'avif');
                foreach ( $sibling_formats_big as $format ) {
                    $trash_sibling_big = $trash_dir_system . DIRECTORY_SEPARATOR . $image_id . '_' . $file_base_big . '.' . $format;
                    if ( file_exists( $trash_sibling_big ) ) {
                        $original_sibling_big = $dir_big . DIRECTORY_SEPARATOR . $file_base_big . '.' . $format;
                        pt_wuclean_restore_trashed_file_to_destination( $trash_sibling_big, $original_sibling_big );
                    }
                    $trash_sibling_big_appended = $trash_dir_system . DIRECTORY_SEPARATOR . $image_id . '_' . basename( $big_original_dest_system ) . '.' . $format;
                    if ( file_exists( $trash_sibling_big_appended ) ) {
                        $original_sibling_big_appended = $big_original_dest_system . '.' . $format;
                        pt_wuclean_restore_trashed_file_to_destination( $trash_sibling_big_appended, $original_sibling_big_appended );
                    }
                }
            }
        }
        // Clean up meta.
        delete_post_meta( $image_id, '_pt_wuclean_trash_path' );
        delete_post_meta( $image_id, '_pt_wuclean_original_path' );
        // Restore from WordPress trash.
        $result = wp_untrash_post( $image_id );
        if ( $result ) {
            wp_send_json_success( __( 'Image restored successfully', 'wuclean-database-cleaner-for-woocommerce' ) );
        } else {
            wp_send_json_error( __( 'Failed to restore image in WordPress', 'wuclean-database-cleaner-for-woocommerce' ) );
        }
    } else {
        // Files couldn't be restored, don't untrash the WordPress post.
        wp_send_json_error( __( 'Image files not found in trash. The image may have been permanently deleted.', 'wuclean-database-cleaner-for-woocommerce' ) );
    }
}

/**
 * AJAX: Delete image permanently.
 *
 * Available to all users with the plugin capability for Scan Results, Protected, and Trash (per-row).
 */
function pt_wuclean_ajax_delete_image() {
    ob_clean();
    if ( !isset( $_POST['nonce'] ) || !wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'pt_wuclean_delete_image' ) ) {
        wp_send_json_error( __( 'Security check failed', 'wuclean-database-cleaner-for-woocommerce' ) );
        return;
    }
    if ( !current_user_can( pt_wuclean_get_required_capability() ) ) {
        wp_send_json_error( __( 'Insufficient permissions', 'wuclean-database-cleaner-for-woocommerce' ) );
        return;
    }
    $image_id = ( isset( $_POST['image_id'] ) ? intval( $_POST['image_id'] ) : 0 );
    if ( !$image_id ) {
        wp_send_json_error( __( 'Invalid image ID', 'wuclean-database-cleaner-for-woocommerce' ) );
        return;
    }
    // Verify it's an attachment.
    if ( 'attachment' !== get_post_type( $image_id ) ) {
        wp_send_json_error( __( 'Not a valid attachment', 'wuclean-database-cleaner-for-woocommerce' ) );
        return;
    }
    // Check if file is in wuclean trash directory.
    // Check new meta key first, fallback to old for backward compatibility.
    $trash_path = get_post_meta( $image_id, '_pt_wuclean_trash_path', true );
    if ( empty( $trash_path ) ) {
        $trash_path = get_post_meta( $image_id, '_wuclean_trash_path', true );
        if ( !empty( $trash_path ) ) {
            update_post_meta( $image_id, '_pt_wuclean_trash_path', $trash_path );
            delete_post_meta( $image_id, '_wuclean_trash_path' );
        }
    }
    $deleted_siblings = 0;
    if ( $trash_path && file_exists( $trash_path ) ) {
        $trash_location = pt_wuclean_get_trash_location();
        $trash_dir_system = str_replace( '/', DIRECTORY_SEPARATOR, rtrim( $trash_location['dir'], '/\\' ) );
        // Delete from trash directory.
        wp_delete_file( wp_normalize_path( $trash_path ) );
        // Delete all thumbnail sizes from trash.
        $metadata = wp_get_attachment_metadata( $image_id );
        if ( !is_array( $metadata ) ) {
            $metadata = array();
        }
        if ( isset( $metadata['sizes'] ) && is_array( $metadata['sizes'] ) ) {
            foreach ( $metadata['sizes'] as $size => $size_data ) {
                $trash_thumb = $trash_dir_system . DIRECTORY_SEPARATOR . $image_id . '_' . $size_data['file'];
                if ( file_exists( $trash_thumb ) ) {
                    wp_delete_file( wp_normalize_path( $trash_thumb ) );
                }
            }
        }
        // Delete WordPress "big image" original from trash when present (JPEG/PNG; not covered by WebP/AVIF-only scan below).
        if ( !empty( $metadata['original_image'] ) && is_string( $metadata['original_image'] ) ) {
            $orig_trash_basename = basename( $metadata['original_image'] );
            if ( '' !== $orig_trash_basename ) {
                $trash_big_original = $trash_dir_system . DIRECTORY_SEPARATOR . $image_id . '_' . $orig_trash_basename;
                if ( file_exists( $trash_big_original ) ) {
                    wp_delete_file( wp_normalize_path( $trash_big_original ) );
                }
            }
        }
        // Clean up meta.
        delete_post_meta( $image_id, '_pt_wuclean_trash_path' );
        delete_post_meta( $image_id, '_pt_wuclean_original_path' );
    }
    // Get filename for sibling detection (before deletion).
    $file_path_before = get_attached_file( $image_id );
    $filename = ( $file_path_before ? basename( $file_path_before ) : '' );
    // Delete permanently (skip trash).
    $result = wp_delete_attachment( $image_id, true );
    if ( $result ) {
        // Remove from protected list if it was protected.
        $protected_images = pt_wuclean_get_protected_images();
        if ( in_array( $image_id, $protected_images, true ) ) {
            $protected_images = array_diff( $protected_images, array($image_id) );
            update_option( 'pt_wuclean_protected_images', array_values( $protected_images ), false );
        }
        // Remove from DB results: the Media Library image itself and any orphaned items tied to this filename.
        pt_wuclean_image_scan_db_remove_media_item_and_siblings( $image_id, $filename );
        $message = __( 'Image deleted permanently', 'wuclean-database-cleaner-for-woocommerce' );
        if ( $deleted_siblings > 0 ) {
            // translators: %d: number of WebP/AVIF files deleted.
            $message .= ' ' . sprintf( _n(
                '(%d WebP/AVIF file also deleted)',
                '(%d WebP/AVIF files also deleted)',
                $deleted_siblings,
                'wuclean-database-cleaner-for-woocommerce'
            ), $deleted_siblings );
        }
        wp_send_json_success( $message );
    } else {
        wp_send_json_error( __( 'Failed to delete image', 'wuclean-database-cleaner-for-woocommerce' ) );
    }
}

if ( !function_exists( 'pt_wuclean_ajax_protect_image' ) ) {
    /**
     * AJAX: Protect image (single-row action).
     *
     * @return void
     */
    function pt_wuclean_ajax_protect_image() {
        if ( ob_get_length() ) {
            ob_clean();
        }
        if ( !isset( $_POST['nonce'] ) || !wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'pt_wuclean_protect_image' ) ) {
            wp_send_json_error( __( 'Security check failed', 'wuclean-database-cleaner-for-woocommerce' ) );
            return;
        }
        if ( !current_user_can( pt_wuclean_get_required_capability() ) ) {
            wp_send_json_error( __( 'Insufficient permissions', 'wuclean-database-cleaner-for-woocommerce' ) );
            return;
        }
        $image_id = ( isset( $_POST['image_id'] ) ? absint( $_POST['image_id'] ) : 0 );
        if ( $image_id <= 0 ) {
            wp_send_json_error( __( 'Invalid image ID', 'wuclean-database-cleaner-for-woocommerce' ) );
            return;
        }
        $protected_images = pt_wuclean_get_protected_images();
        if ( !in_array( $image_id, $protected_images, true ) ) {
            $protected_images[] = $image_id;
            update_option( 'pt_wuclean_protected_images', $protected_images, false );
        }
        $file_path = get_attached_file( $image_id );
        $filename = ( $file_path ? basename( $file_path ) : '' );
        pt_wuclean_image_scan_db_remove_media_item_and_siblings( $image_id, $filename );
        wp_send_json_success( __( 'Image and related siblings protected successfully', 'wuclean-database-cleaner-for-woocommerce' ) );
    }

}
if ( !function_exists( 'pt_wuclean_ajax_trash_image' ) ) {
    /**
     * AJAX: Trash image (single-row action).
     *
     * @return void
     */
    function pt_wuclean_ajax_trash_image() {
        if ( ob_get_length() ) {
            ob_clean();
        }
        if ( !isset( $_POST['nonce'] ) || !wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'pt_wuclean_trash_image' ) ) {
            wp_send_json_error( __( 'Security check failed', 'wuclean-database-cleaner-for-woocommerce' ) );
            return;
        }
        if ( !current_user_can( pt_wuclean_get_required_capability() ) ) {
            wp_send_json_error( __( 'Insufficient permissions', 'wuclean-database-cleaner-for-woocommerce' ) );
            return;
        }
        $image_id = ( isset( $_POST['image_id'] ) ? absint( $_POST['image_id'] ) : 0 );
        if ( $image_id <= 0 ) {
            wp_send_json_error( __( 'Invalid image ID', 'wuclean-database-cleaner-for-woocommerce' ) );
            return;
        }
        if ( 'attachment' !== get_post_type( $image_id ) ) {
            wp_send_json_error( __( 'Not a valid attachment', 'wuclean-database-cleaner-for-woocommerce' ) );
            return;
        }
        $moved = pt_wuclean_move_media_library_attachment_files_to_trash( $image_id );
        if ( is_wp_error( $moved ) ) {
            wp_send_json_error( $moved->get_error_message() );
            return;
        }
        update_post_meta( $image_id, '_pt_wuclean_trash_path', $moved['trash_path'] );
        update_post_meta( $image_id, '_pt_wuclean_original_path', $moved['file_path_normalized'] );
        wp_trash_post( $image_id );
        $protected_images = pt_wuclean_get_protected_images();
        if ( in_array( $image_id, $protected_images, true ) ) {
            $protected_images = array_diff( $protected_images, array($image_id) );
            update_option( 'pt_wuclean_protected_images', array_values( $protected_images ), false );
        }
        pt_wuclean_image_scan_db_remove_media_item_and_siblings( $image_id, $moved['filename'] );
        wp_send_json_success( __( 'Image and related siblings moved to trash successfully', 'wuclean-database-cleaner-for-woocommerce' ) );
    }

}
if ( !function_exists( 'pt_wuclean_ajax_unprotect_image' ) ) {
    /**
     * AJAX: Unprotect image (single-row action).
     *
     * @return void
     */
    function pt_wuclean_ajax_unprotect_image() {
        if ( ob_get_length() ) {
            ob_clean();
        }
        if ( !isset( $_POST['nonce'] ) || !wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'pt_wuclean_unprotect_image' ) ) {
            wp_send_json_error( __( 'Security check failed', 'wuclean-database-cleaner-for-woocommerce' ) );
            return;
        }
        if ( !current_user_can( pt_wuclean_get_required_capability() ) ) {
            wp_send_json_error( __( 'Insufficient permissions', 'wuclean-database-cleaner-for-woocommerce' ) );
            return;
        }
        $image_id = ( isset( $_POST['image_id'] ) ? absint( $_POST['image_id'] ) : 0 );
        if ( $image_id <= 0 ) {
            wp_send_json_error( __( 'Invalid image ID', 'wuclean-database-cleaner-for-woocommerce' ) );
            return;
        }
        $protected_images = pt_wuclean_get_protected_images();
        $protected_images = array_diff( $protected_images, array($image_id) );
        update_option( 'pt_wuclean_protected_images', array_values( $protected_images ), false );
        wp_send_json_success( __( 'Image unprotected successfully', 'wuclean-database-cleaner-for-woocommerce' ) );
    }

}
/**
 * Resolve the best preview/download filename for an orphan group in plugin trash.
 *
 * @param int    $attachment_id Attachment ID prefix.
 * @param string $trash_dir     Absolute path to trash directory.
 * @return string|WP_Error Filename (basename within trash dir) or WP_Error on failure.
 */
function pt_wuclean_get_best_trash_orphan_filename(  $attachment_id, $trash_dir  ) {
    $attachment_id = absint( $attachment_id );
    $trash_dir = ( is_string( $trash_dir ) ? rtrim( $trash_dir, "/\\ \t\n\r\x00\v" ) : '' );
    if ( $attachment_id <= 0 || '' === $trash_dir || !is_dir( $trash_dir ) || !is_readable( $trash_dir ) ) {
        return new WP_Error('pt_wuclean_trash_orphan_invalid', __( 'Invalid trash reference', 'wuclean-database-cleaner-for-woocommerce' ));
    }
    $files = scandir( $trash_dir );
    if ( false === $files ) {
        return new WP_Error('pt_wuclean_trash_orphan_scan_failed', __( 'Failed to read trash directory', 'wuclean-database-cleaner-for-woocommerce' ));
    }
    $prefix = (string) $attachment_id . '_';
    $file_list = array();
    foreach ( $files as $file ) {
        if ( '.' === $file || '..' === $file ) {
            continue;
        }
        if ( !is_string( $file ) || '' === $file ) {
            continue;
        }
        if ( 0 !== strpos( $file, $prefix ) ) {
            continue;
        }
        $path = $trash_dir . DIRECTORY_SEPARATOR . $file;
        if ( is_dir( $path ) ) {
            continue;
        }
        $file_list[] = $file;
    }
    if ( empty( $file_list ) ) {
        return new WP_Error('pt_wuclean_trash_orphan_not_found', __( 'File not found in trash', 'wuclean-database-cleaner-for-woocommerce' ));
    }
    sort( $file_list, SORT_STRING );
    $raster_ext_re = '/\\.(jpe?g|png|gif|bmp|tiff?)$/i';
    $thumb_suffix_re = '/-\\d+x\\d+\\.[^.]+$/i';
    $non_thumb = array();
    $all_raster = array();
    foreach ( $file_list as $f ) {
        if ( preg_match( $raster_ext_re, $f ) ) {
            $all_raster[] = $f;
            if ( !preg_match( $thumb_suffix_re, $f ) ) {
                $non_thumb[] = $f;
            }
        }
    }
    $candidates = ( !empty( $non_thumb ) ? $non_thumb : $all_raster );
    if ( empty( $candidates ) ) {
        // Fall back to first file in group if nothing matches raster extensions.
        return (string) $file_list[0];
    }
    $best_file = '';
    $best_size = -1;
    foreach ( $candidates as $f ) {
        $path = $trash_dir . DIRECTORY_SEPARATOR . $f;
        if ( !file_exists( $path ) || is_dir( $path ) ) {
            continue;
        }
        $size = @filesize( $path );
        // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- may warn; treat as unreadable.
        if ( false === $size ) {
            continue;
        }
        if ( (int) $size > $best_size ) {
            $best_size = (int) $size;
            $best_file = (string) $f;
        }
    }
    return ( '' !== $best_file ? $best_file : (string) $candidates[0] );
}

/**
 * Prevent WordPress from auto-deleting attachments trashed by wuclean.
 *
 * WordPress runs wp_scheduled_delete daily and permanently deletes trashed posts
 * after EMPTY_TRASH_DAYS (default 30) via wp_delete_post( $id, true ), so
 * $force_delete is true. This filter blocks that for wuclean-trashed attachments
 * by also preventing deletion when the call is from cron (DOING_CRON). Manual
 * "Delete Permanently" from the plugin runs in admin context (not cron), so it
 * continues to work.
 *
 * @param bool|null $delete        Whether to delete the post. Return false to prevent deletion.
 * @param WP_Post   $post          Post object.
 * @param bool      $force_delete  Whether to bypass trash and force deletion.
 * @return bool|null False to prevent deletion, null to allow WordPress default behavior.
 */
function pt_wuclean_prevent_auto_delete_trashed_images(  $delete, $post, $force_delete  ) {
    if ( 'attachment' !== $post->post_type || 'trash' !== $post->post_status ) {
        return $delete;
    }
    // Check if this attachment was trashed by wuclean (has our meta).
    // Check new meta key first, fallback to old for backward compatibility.
    $trash_path = get_post_meta( $post->ID, '_pt_wuclean_trash_path', true );
    if ( empty( $trash_path ) ) {
        $trash_path = get_post_meta( $post->ID, '_wuclean_trash_path', true );
        if ( !empty( $trash_path ) ) {
            update_post_meta( $post->ID, '_pt_wuclean_trash_path', $trash_path );
            delete_post_meta( $post->ID, '_wuclean_trash_path' );
        }
    }
    if ( empty( $trash_path ) ) {
        return $delete;
    }
    // Prevent deletion when: (a) not force delete, or (b) force delete but from cron (scheduled cleanup).
    // Admin "Delete Permanently" in plugin is not cron, so it is not blocked.
    $is_scheduled_cleanup = $force_delete && defined( 'DOING_CRON' ) && DOING_CRON;
    if ( !$force_delete || $is_scheduled_cleanup ) {
        return false;
    }
    return $delete;
}

add_filter(
    'pre_delete_post',
    'pt_wuclean_prevent_auto_delete_trashed_images',
    10,
    3
);
/**
 * AJAX: Download the best orphan trash file for an attachment ID prefix.
 *
 * Security: capability + nonce + directory and filename validation.
 *
 * @return void
 */
function pt_wuclean_ajax_download_trash_orphan() {
    if ( ob_get_length() ) {
        ob_clean();
    }
    if ( !isset( $_GET['_wpnonce'] ) || !wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'pt_wuclean_download_trash_orphan' ) ) {
        wp_die( esc_html__( 'Security check failed', 'wuclean-database-cleaner-for-woocommerce' ) );
    }
    if ( !current_user_can( pt_wuclean_get_required_capability() ) ) {
        wp_die( esc_html__( 'Insufficient permissions', 'wuclean-database-cleaner-for-woocommerce' ) );
    }
    $attachment_id = ( isset( $_GET['attachment_id'] ) ? absint( $_GET['attachment_id'] ) : 0 );
    if ( $attachment_id <= 0 ) {
        wp_die( esc_html__( 'Invalid image reference', 'wuclean-database-cleaner-for-woocommerce' ) );
    }
    $trash_location = pt_wuclean_get_trash_location();
    $trash_dir = ( isset( $trash_location['dir'] ) ? (string) $trash_location['dir'] : '' );
    $trash_dir = rtrim( $trash_dir, "/\\ \t\n\r\x00\v" );
    if ( '' === $trash_dir || !is_dir( $trash_dir ) || !is_readable( $trash_dir ) ) {
        wp_die( esc_html__( 'Trash directory not accessible', 'wuclean-database-cleaner-for-woocommerce' ) );
    }
    // Security: Ensure trash dir is within WP_CONTENT_DIR.
    $wp_content_dir = str_replace( '\\', '/', WP_CONTENT_DIR );
    $trash_dir_normalized = str_replace( '\\', '/', $trash_dir );
    if ( 0 !== strpos( $trash_dir_normalized, $wp_content_dir ) ) {
        wp_die( esc_html__( 'Invalid trash directory', 'wuclean-database-cleaner-for-woocommerce' ) );
    }
    $filename = pt_wuclean_get_best_trash_orphan_filename( $attachment_id, $trash_dir );
    if ( is_wp_error( $filename ) ) {
        wp_die( esc_html( $filename->get_error_message() ) );
    }
    $filename = ( is_string( $filename ) ? $filename : '' );
    if ( '' === $filename ) {
        wp_die( esc_html__( 'File not found in trash', 'wuclean-database-cleaner-for-woocommerce' ) );
    }
    // Security: filename must match prefix.
    $prefix = (string) $attachment_id . '_';
    if ( 0 !== strpos( $filename, $prefix ) ) {
        wp_die( esc_html__( 'Invalid file reference', 'wuclean-database-cleaner-for-woocommerce' ) );
    }
    $path = $trash_dir . DIRECTORY_SEPARATOR . $filename;
    if ( !file_exists( $path ) || is_dir( $path ) || !is_readable( $path ) ) {
        wp_die( esc_html__( 'File not readable', 'wuclean-database-cleaner-for-woocommerce' ) );
    }
    // Ensure the resolved path is inside the trash directory.
    $real = realpath( $path );
    if ( !$real ) {
        wp_die( esc_html__( 'File not found', 'wuclean-database-cleaner-for-woocommerce' ) );
    }
    $real_norm = str_replace( '\\', '/', $real );
    $trash_norm = str_replace( '\\', '/', $trash_dir );
    $trash_norm = rtrim( $trash_norm, '/' ) . '/';
    if ( 0 !== strpos( $real_norm, $trash_norm ) ) {
        wp_die( esc_html__( 'Invalid file path', 'wuclean-database-cleaner-for-woocommerce' ) );
    }
    $type = wp_check_filetype( $filename );
    $mime = ( isset( $type['type'] ) && is_string( $type['type'] ) && '' !== $type['type'] ? $type['type'] : 'application/octet-stream' );
    nocache_headers();
    header( 'Content-Description: File Transfer' );
    header( 'Content-Type: ' . $mime );
    header( 'Content-Disposition: attachment; filename="' . rawurlencode( $filename ) . '"' );
    header( 'Content-Transfer-Encoding: binary' );
    $size = filesize( $real );
    if ( false !== $size ) {
        header( 'Content-Length: ' . (string) $size );
    }
    // Stream file and exit.
    // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_readfile, WordPress.WP.AlternativeFunctions.file_system_operations_readfile -- streaming download is required here.
    readfile( $real );
    exit;
}

/**
 * AJAX: Delete orphaned plugin-trash files for an attachment ID prefix.
 *
 * This is used when WordPress has already removed the attachment record, but files
 * still remain in the plugin trash directory (e.g. wuclean-trash/images).
 *
 * @return void
 */
function pt_wuclean_ajax_delete_trash_orphan() {
    if ( ob_get_length() ) {
        ob_clean();
    }
    if ( !isset( $_POST['nonce'] ) || !wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'pt_wuclean_delete_trash_orphan' ) ) {
        wp_send_json_error( __( 'Security check failed', 'wuclean-database-cleaner-for-woocommerce' ) );
        return;
    }
    if ( !current_user_can( pt_wuclean_get_required_capability() ) ) {
        wp_send_json_error( __( 'Insufficient permissions', 'wuclean-database-cleaner-for-woocommerce' ) );
        return;
    }
    $attachment_id = ( isset( $_POST['attachment_id'] ) ? absint( $_POST['attachment_id'] ) : 0 );
    if ( $attachment_id <= 0 ) {
        wp_send_json_error( __( 'Invalid image reference', 'wuclean-database-cleaner-for-woocommerce' ) );
        return;
    }
    $trash_location = pt_wuclean_get_trash_location();
    $trash_dir = ( isset( $trash_location['dir'] ) ? (string) $trash_location['dir'] : '' );
    $trash_dir = rtrim( $trash_dir, "/\\ \t\n\r\x00\v" );
    if ( '' === $trash_dir || !is_dir( $trash_dir ) || !is_readable( $trash_dir ) ) {
        wp_send_json_error( __( 'Trash directory not accessible', 'wuclean-database-cleaner-for-woocommerce' ) );
        return;
    }
    // Security: Ensure trash dir is within WP_CONTENT_DIR.
    $wp_content_dir = str_replace( '\\', '/', WP_CONTENT_DIR );
    $trash_dir_normalized = str_replace( '\\', '/', $trash_dir );
    if ( 0 !== strpos( $trash_dir_normalized, $wp_content_dir ) ) {
        wp_send_json_error( __( 'Invalid trash directory', 'wuclean-database-cleaner-for-woocommerce' ) );
        return;
    }
    $files = scandir( $trash_dir );
    if ( false === $files ) {
        wp_send_json_error( __( 'Failed to read trash directory', 'wuclean-database-cleaner-for-woocommerce' ) );
        return;
    }
    $prefix = (string) $attachment_id . '_';
    $deleted_count = 0;
    foreach ( $files as $file ) {
        if ( '.' === $file || '..' === $file ) {
            continue;
        }
        if ( !is_string( $file ) || '' === $file ) {
            continue;
        }
        if ( 0 !== strpos( $file, $prefix ) ) {
            continue;
        }
        $path = $trash_dir . DIRECTORY_SEPARATOR . $file;
        if ( is_dir( $path ) ) {
            continue;
        }
        if ( wp_delete_file( wp_normalize_path( $path ) ) ) {
            ++$deleted_count;
        }
    }
    if ( $deleted_count <= 0 ) {
        wp_send_json_error( __( 'No matching files found in trash', 'wuclean-database-cleaner-for-woocommerce' ) );
        return;
    }
    /* translators: %d: number of files deleted */
    wp_send_json_success( sprintf( _n(
        '%d file deleted permanently',
        '%d files deleted permanently',
        $deleted_count,
        'wuclean-database-cleaner-for-woocommerce'
    ), $deleted_count ) );
}

/**
 * AJAX: Persist Scan Results "results per page" preference.
 *
 * @return void
 */
function pt_wuclean_ajax_save_results_per_page() {
    if ( !isset( $_POST['nonce'] ) || !wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'pt_wuclean_save_results_per_page' ) ) {
        wp_send_json_error( __( 'Security check failed', 'wuclean-database-cleaner-for-woocommerce' ) );
        return;
    }
    if ( !current_user_can( pt_wuclean_get_required_capability() ) ) {
        wp_send_json_error( __( 'Insufficient permissions', 'wuclean-database-cleaner-for-woocommerce' ) );
        return;
    }
    $per_page = ( isset( $_POST['per_page'] ) ? absint( wp_unslash( $_POST['per_page'] ) ) : 25 );
    $per_page = max( 25, min( 200, $per_page ) );
    $allowed = array(
        25,
        50,
        100,
        200
    );
    if ( !in_array( $per_page, $allowed, true ) ) {
        $per_page = 25;
    }
    $settings = get_option( 'pt_wuclean_image_settings', array() );
    if ( !is_array( $settings ) ) {
        $settings = array();
    }
    $settings['results_per_page'] = $per_page;
    update_option( 'pt_wuclean_image_settings', $settings, false );
    wp_send_json_success();
}

// Register AJAX handlers.
add_action( 'wp_ajax_pt_wuclean_save_results_per_page', 'pt_wuclean_ajax_save_results_per_page' );
add_action( 'wp_ajax_pt_wuclean_start_image_scan', 'pt_wuclean_ajax_start_image_scan' );
add_action( 'wp_ajax_pt_wuclean_process_scan_batch', 'pt_wuclean_ajax_process_scan_batch' );
add_action( 'wp_ajax_pt_wuclean_stop_image_scan', 'pt_wuclean_ajax_stop_image_scan' );
add_action( 'wp_ajax_pt_wuclean_resume_image_scan', 'pt_wuclean_ajax_resume_image_scan' );
// Delete image - available for all users.
add_action( 'wp_ajax_pt_wuclean_delete_image', 'pt_wuclean_ajax_delete_image' );
// Restore image.
add_action( 'wp_ajax_pt_wuclean_restore_image', 'pt_wuclean_ajax_restore_image' );
// Download orphaned plugin-trash file - available for all users.
add_action( 'wp_ajax_pt_wuclean_download_trash_orphan', 'pt_wuclean_ajax_download_trash_orphan' );
// Delete orphaned plugin-trash files - available for all users.
add_action( 'wp_ajax_pt_wuclean_delete_trash_orphan', 'pt_wuclean_ajax_delete_trash_orphan' );
// Protect and Trash image - available for all users (single-row actions).
if ( function_exists( 'pt_wuclean_ajax_protect_image' ) ) {
    add_action( 'wp_ajax_pt_wuclean_protect_image', 'pt_wuclean_ajax_protect_image' );
}
if ( function_exists( 'pt_wuclean_ajax_trash_image' ) ) {
    add_action( 'wp_ajax_pt_wuclean_trash_image', 'pt_wuclean_ajax_trash_image' );
}
// Unprotect image - available for all users (single-row actions).
if ( function_exists( 'pt_wuclean_ajax_unprotect_image' ) ) {
    add_action( 'wp_ajax_pt_wuclean_unprotect_image', 'pt_wuclean_ajax_unprotect_image' );
}
// ============================================================================
// ENQUEUE SCRIPTS
// ============================================================================
/**
 * Enqueue image scanner scripts.
 */
function pt_wuclean_enqueue_image_scripts() {
    $screen = get_current_screen();
    if ( $screen && isset( $screen->id ) && is_string( $screen->id ) && false !== strpos( $screen->id, 'wuclean-images' ) ) {
        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-images',
            plugin_dir_url( __FILE__ ) . 'assets/js/images.js',
            array('jquery', 'pt-wuclean-common'),
            '2.2.2',
            true
        );
        wp_localize_script( 'pt-wuclean-common', 'pt_wuclean_common_vars', pt_wuclean_get_common_script_strings() );
        wp_localize_script( 'pt-wuclean-images', 'pt_wuclean_images_vars', pt_wuclean_get_images_script_strings() );
    }
}

add_action( 'admin_enqueue_scripts', 'pt_wuclean_enqueue_image_scripts' );