<?php
/**
 * Media usage scanning and reversible media cleanup.
 *
 * @package Shrinkwell_Image_Optimizer
 */

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

class Shrinkwell_Media_Cleanup
{
    private const SCAN_OPTION = 'shrinkwell_media_cleanup_scan';
    private const DB_VERSION_OPTION = 'shrinkwell_media_cleanup_db_version';
    private const DB_VERSION = '1';
    private const TRASH_DAYS = 30;
    private const CRON_HOOK = 'shrinkwell_cleanup_expired_trash';
    private const MAX_SCAN_VALUE_BYTES = 5242880;

    private static ?self $instance = null;

    public static function instance(): self
    {
        if (null === self::$instance) {
            self::$instance = new self();
        }

        return self::$instance;
    }

    private function __construct()
    {
        add_action('init', [$this, 'maybe_install']);
        add_action(self::CRON_HOOK, [$this, 'purge_expired_trash']);
        add_action('wp_ajax_shrinkwell_media_cleanup_scan', [$this, 'ajax_scan']);
        add_action('wp_ajax_shrinkwell_media_cleanup_trash', [$this, 'ajax_move_to_trash']);
        add_action('wp_ajax_shrinkwell_media_cleanup_restore', [$this, 'ajax_restore_all']);
        add_action('wp_ajax_shrinkwell_media_cleanup_delete', [$this, 'ajax_delete_permanently']);
    }

    public static function activate(): void
    {
        self::install_table();

        if (!wp_next_scheduled(self::CRON_HOOK)) {
            wp_schedule_event(time() + HOUR_IN_SECONDS, 'daily', self::CRON_HOOK);
        }
    }

    public function maybe_install(): void
    {
        if (self::DB_VERSION !== get_option(self::DB_VERSION_OPTION)) {
            self::install_table();
        }

        if (!wp_next_scheduled(self::CRON_HOOK)) {
            wp_schedule_event(time() + HOUR_IN_SECONDS, 'daily', self::CRON_HOOK);
        }
    }

    private static function install_table(): void
    {
        global $wpdb;

        require_once ABSPATH . 'wp-admin/includes/upgrade.php';

        $table = $wpdb->prefix . 'shrinkwell_trash';
        $charset_collate = $wpdb->get_charset_collate();
        $sql = "CREATE TABLE {$table} (
            id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
            attachment_id bigint(20) unsigned NOT NULL,
            file_path text NOT NULL,
            original_meta longtext NOT NULL,
            trashed_at datetime NOT NULL,
            expires_at datetime NOT NULL,
            status varchar(20) NOT NULL DEFAULT 'trashed',
            PRIMARY KEY  (id),
            UNIQUE KEY attachment_id (attachment_id),
            KEY expires_at (expires_at)
        ) {$charset_collate};";

        dbDelta($sql);
        update_option(self::DB_VERSION_OPTION, self::DB_VERSION, false);
    }

    private function table_name(): string
    {
        global $wpdb;

        return $wpdb->prefix . 'shrinkwell_trash';
    }

    public function cached_scan(): array
    {
        return wp_parse_args((array) get_option(self::SCAN_OPTION, []), [
            'scanned_at' => '',
            'items' => [],
            'summary' => $this->empty_summary(),
        ]);
    }

    public function tab_badge_count(): int
    {
        $scan = $this->cached_scan();

        return (int) ($scan['summary']['possibly_unused'] ?? 0);
    }

    private function empty_summary(): array
    {
        return [
            'total_scanned' => 0,
            'used' => 0,
            'possibly_unused' => 0,
            'possibly_unused_high_confidence' => 0,
            'old' => 0,
            'needs_review' => 0,
            'protected' => 0,
            'selected_by_default' => 0,
            'potential_bytes' => 0,
            'potential_formatted' => size_format(0),
            'selected_bytes' => 0,
            'selected_formatted' => size_format(0),
        ];
    }

    public function trash_summary(): array
    {
        global $wpdb;

        $rows = $wpdb->get_results("SELECT original_meta, trashed_at, expires_at FROM {$this->table_name()} WHERE status = 'trashed' ORDER BY trashed_at ASC", ARRAY_A); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $bytes = 0;

        foreach ($rows as $row) {
            $meta = json_decode((string) $row['original_meta'], true);
            $bytes += (int) ($meta['total_bytes'] ?? 0);
        }

        $oldest_expiry = !empty($rows[0]['expires_at']) ? strtotime((string) $rows[0]['expires_at'] . ' UTC') : 0;
        $days = $oldest_expiry ? max(0, (int) ceil(($oldest_expiry - time()) / DAY_IN_SECONDS)) : 0;

        return [
            'count' => count($rows),
            'bytes' => $bytes,
            'formatted' => size_format($bytes, 1),
            'days' => $days,
        ];
    }

    public function render_tab(): void
    {
        $scan = $this->cached_scan();
        $summary = wp_parse_args((array) $scan['summary'], $this->empty_summary());
        $trash = $this->trash_summary();
        $can_cleanup = function_exists('shrinkwell_can_bulk_cleanup') && shrinkwell_can_bulk_cleanup();
        $upgrade_url = function_exists('shrinkwell_get_upgrade_url') ? shrinkwell_get_upgrade_url() : 'https://shrinkwell.com/';
        ?>
        <section class="shrinkwell-cleanup-stats" aria-label="<?php esc_attr_e('Media cleanup summary', 'shrinkwell-image-optimizer'); ?>">
            <div class="shrinkwell-cleanup-stat">
                <strong data-cleanup-stat="total_scanned"><?php echo esc_html(number_format_i18n((int) $summary['total_scanned'])); ?></strong>
                <span><?php esc_html_e('Images scanned', 'shrinkwell-image-optimizer'); ?></span>
            </div>
            <div class="shrinkwell-cleanup-stat">
                <strong data-cleanup-stat="used"><?php echo esc_html(number_format_i18n((int) $summary['used'])); ?></strong>
                <span><?php esc_html_e('Used on site', 'shrinkwell-image-optimizer'); ?></span>
            </div>
            <div class="shrinkwell-cleanup-stat">
                <strong data-cleanup-stat="protected"><?php echo esc_html(number_format_i18n((int) $summary['protected'])); ?></strong>
                <span><?php esc_html_e('Protected media', 'shrinkwell-image-optimizer'); ?></span>
            </div>
            <div class="shrinkwell-cleanup-stat">
                <strong data-cleanup-stat="possibly_unused_high_confidence"><?php echo esc_html(number_format_i18n((int) $summary['possibly_unused_high_confidence'])); ?></strong>
                <span><?php esc_html_e('Possibly unused, high confidence', 'shrinkwell-image-optimizer'); ?></span>
            </div>
            <div class="shrinkwell-cleanup-stat">
                <strong data-cleanup-stat="needs_review"><?php echo esc_html(number_format_i18n((int) $summary['needs_review'])); ?></strong>
                <span><?php esc_html_e('Needs review', 'shrinkwell-image-optimizer'); ?></span>
            </div>
            <div class="shrinkwell-cleanup-stat">
                <strong data-cleanup-stat="selected_by_default"><?php echo esc_html(number_format_i18n((int) $summary['selected_by_default'])); ?></strong>
                <span><?php esc_html_e('Selected by default', 'shrinkwell-image-optimizer'); ?></span>
            </div>
            <div class="shrinkwell-cleanup-stat">
                <strong data-cleanup-stat="selected_formatted"><?php echo esc_html((string) $summary['selected_formatted']); ?></strong>
                <span><?php esc_html_e('Estimated selected size', 'shrinkwell-image-optimizer'); ?></span>
            </div>
        </section>

        <section class="shrinkwell-panel shrinkwell-cleanup-scan-panel">
            <div class="shrinkwell-panel-header">
                <div>
                    <h2><?php esc_html_e('Scan for possibly unused media', 'shrinkwell-image-optimizer'); ?></h2>
                    <p><?php esc_html_e('Shrinkwell scans your WordPress database to detect where media files are used — across posts, pages, products, custom fields, page builders, widgets, menus and theme settings.', 'shrinkwell-image-optimizer'); ?></p>
                </div>
            </div>
            <div class="shrinkwell-scan-mode">
                <div class="shrinkwell-scan-mode-card is-active">
                    <strong><?php esc_html_e('Database scan', 'shrinkwell-image-optimizer'); ?></strong>
                    <span><?php esc_html_e('Fast', 'shrinkwell-image-optimizer'); ?></span>
                </div>
            </div>
            <p class="shrinkwell-cleanup-actions">
                <button type="button" class="button button-primary" id="shrinkwell-cleanup-scan"><?php esc_html_e('Scan now', 'shrinkwell-image-optimizer'); ?></button>
                <span class="spinner" id="shrinkwell-cleanup-spinner"></span>
                <span id="shrinkwell-cleanup-status" aria-live="polite"></span>
            </p>
            <div class="shrinkwell-cleanup-progress" id="shrinkwell-cleanup-progress" hidden>
                <span></span>
            </div>
        </section>

        <section class="shrinkwell-panel shrinkwell-cleanup-results" id="shrinkwell-cleanup-results" <?php echo empty($scan['scanned_at']) ? 'hidden' : ''; ?>>
            <div class="shrinkwell-panel-header">
                <div>
                    <h2><?php esc_html_e('Review results', 'shrinkwell-image-optimizer'); ?></h2>
                    <p><?php esc_html_e('Database scan found where images are used. Review results before removing anything.', 'shrinkwell-image-optimizer'); ?></p>
                    <p class="shrinkwell-cleanup-safety-note"><?php esc_html_e('Only high-confidence possibly unused images are selected by default. Review all results before moving files to Shrinkwell Trash.', 'shrinkwell-image-optimizer'); ?></p>
                </div>
                <span class="shrinkwell-last-scan"><?php esc_html_e('Last scan:', 'shrinkwell-image-optimizer'); ?> <strong id="shrinkwell-cleanup-last-scan"><?php echo esc_html((string) ($scan['scanned_at'] ?: '—')); ?></strong></span>
            </div>
            <div class="shrinkwell-cleanup-counts">
                <?php foreach (['used' => __('Used on site', 'shrinkwell-image-optimizer'), 'possibly_unused' => __('Possibly unused', 'shrinkwell-image-optimizer'), 'protected' => __('Protected', 'shrinkwell-image-optimizer'), 'needs_review' => __('Needs review', 'shrinkwell-image-optimizer')] as $key => $label) : ?>
                    <div class="is-<?php echo esc_attr($key); ?>"><strong data-cleanup-count="<?php echo esc_attr($key); ?>"><?php echo esc_html(number_format_i18n((int) $summary[$key])); ?></strong><span><?php echo esc_html($label); ?></span></div>
                <?php endforeach; ?>
            </div>
            <div class="shrinkwell-cleanup-bulkbar">
                <?php if ($can_cleanup) : ?>
                    <label><input type="checkbox" id="shrinkwell-cleanup-select-all"> <?php esc_html_e('Select all high-confidence possibly unused', 'shrinkwell-image-optimizer'); ?></label>
                    <span><strong id="shrinkwell-cleanup-selected">0</strong> <?php esc_html_e('selected', 'shrinkwell-image-optimizer'); ?></span>
                    <span class="shrinkwell-cleanup-selection-help" id="shrinkwell-cleanup-selection-help"><?php esc_html_e('Select high-confidence possibly unused images to continue.', 'shrinkwell-image-optimizer'); ?></span>
                    <button type="button" class="button button-primary" id="shrinkwell-cleanup-trash-selected" disabled><?php esc_html_e('Move to Shrinkwell Trash', 'shrinkwell-image-optimizer'); ?></button>
                <?php else : ?>
                    <span><?php esc_html_e('Find possibly unused media for free. Clean it up safely with Pro.', 'shrinkwell-image-optimizer'); ?></span>
                    <button type="button" class="button button-primary" id="shrinkwell-cleanup-trash-selected" data-cleanup-locked="1"><?php esc_html_e('Move to Shrinkwell Trash', 'shrinkwell-image-optimizer'); ?> <span class="shrinkwell-pro-label"><?php esc_html_e('Pro', 'shrinkwell-image-optimizer'); ?></span></button>
                <?php endif; ?>
            </div>
            <?php if (!$can_cleanup) : ?>
                <div class="shrinkwell-cleanup-upgrade" id="shrinkwell-cleanup-upgrade" hidden>
                    <h3><?php esc_html_e('Clean up possibly unused media with Pro', 'shrinkwell-image-optimizer'); ?></h3>
                    <p>
                        <?php esc_html_e('Shrinkwell found', 'shrinkwell-image-optimizer'); ?>
                        <strong data-cleanup-upgrade="count"><?php echo esc_html(number_format_i18n((int) $summary['possibly_unused'])); ?></strong>
                        <?php esc_html_e('possibly unused images taking up', 'shrinkwell-image-optimizer'); ?>
                        <strong data-cleanup-upgrade="size"><?php echo esc_html((string) $summary['potential_formatted']); ?></strong>.
                        <?php esc_html_e('Upgrade to Pro to move possibly unused files to Shrinkwell Trash, safely clean your media library and reduce future backup size.', 'shrinkwell-image-optimizer'); ?>
                    </p>
                    <div class="shrinkwell-cleanup-upgrade-stats">
                        <span><strong data-cleanup-upgrade="count"><?php echo esc_html(number_format_i18n((int) $summary['possibly_unused'])); ?></strong> <?php esc_html_e('possibly unused images', 'shrinkwell-image-optimizer'); ?></span>
                        <span><strong data-cleanup-upgrade="size"><?php echo esc_html((string) $summary['potential_formatted']); ?></strong> <?php esc_html_e('to potentially free', 'shrinkwell-image-optimizer'); ?></span>
                        <span><strong data-cleanup-upgrade="old"><?php echo esc_html(number_format_i18n((int) $summary['old'])); ?></strong> <?php esc_html_e('not seen in 90+ days', 'shrinkwell-image-optimizer'); ?></span>
                    </div>
                    <a class="button button-primary" id="shrinkwell-cleanup-upgrade-button" href="<?php echo esc_url($upgrade_url); ?>"><?php echo esc_html(!empty($summary['potential_bytes']) ? sprintf(
                        /* translators: %s: Human-readable storage amount that could potentially be freed. */
                        __('Clean up %s with Pro', 'shrinkwell-image-optimizer'),
                        (string) $summary['potential_formatted']
                    ) : __('Clean up with Pro', 'shrinkwell-image-optimizer')); ?></a>
                </div>
            <?php endif; ?>
            <div class="shrinkwell-cleanup-table-wrap">
                <table class="widefat striped shrinkwell-cleanup-table">
                    <thead><tr><th class="check-column"></th><th><?php esc_html_e('Image', 'shrinkwell-image-optimizer'); ?></th><th><?php esc_html_e('Filename and usage', 'shrinkwell-image-optimizer'); ?></th><th><?php esc_html_e('Status', 'shrinkwell-image-optimizer'); ?></th><th><?php esc_html_e('File size', 'shrinkwell-image-optimizer'); ?></th><th><?php esc_html_e('Uploaded', 'shrinkwell-image-optimizer'); ?></th></tr></thead>
                    <tbody id="shrinkwell-cleanup-rows"><?php $this->render_item_rows((array) $scan['items']); ?></tbody>
                </table>
            </div>
        </section>

        <section class="shrinkwell-panel shrinkwell-cleanup-trash" id="shrinkwell-cleanup-trash">
            <div class="shrinkwell-panel-header">
                <div>
                    <h2><?php esc_html_e('Shrinkwell Trash', 'shrinkwell-image-optimizer'); ?></h2>
                    <p><?php esc_html_e('Quarantined images stay on your server for 30 days and can be restored during that period.', 'shrinkwell-image-optimizer'); ?></p>
                </div>
            </div>
            <div class="shrinkwell-trash-summary">
                <span><strong data-trash-stat="count"><?php echo esc_html(number_format_i18n((int) $trash['count'])); ?></strong> <?php esc_html_e('files', 'shrinkwell-image-optimizer'); ?></span>
                <span><strong data-trash-stat="formatted"><?php echo esc_html((string) $trash['formatted']); ?></strong> <?php esc_html_e('stored', 'shrinkwell-image-optimizer'); ?></span>
                <span><strong data-trash-stat="days"><?php echo esc_html(number_format_i18n((int) $trash['days'])); ?></strong> <?php esc_html_e('days until the oldest item expires', 'shrinkwell-image-optimizer'); ?></span>
            </div>
            <div class="shrinkwell-cleanup-actions">
                <button type="button" class="button" id="shrinkwell-cleanup-restore-all" <?php if ($can_cleanup) { disabled(empty($trash['count'])); } else { echo 'data-cleanup-locked="1"'; } ?>><?php esc_html_e('Restore all', 'shrinkwell-image-optimizer'); ?><?php if (!$can_cleanup) : ?> <span class="shrinkwell-pro-label"><?php esc_html_e('Pro', 'shrinkwell-image-optimizer'); ?></span><?php endif; ?></button>
                <button type="button" class="button shrinkwell-button-danger" id="shrinkwell-cleanup-delete-all" <?php if ($can_cleanup) { disabled(empty($trash['count'])); } else { echo 'data-cleanup-locked="1"'; } ?>><?php esc_html_e('Delete permanently', 'shrinkwell-image-optimizer'); ?><?php if (!$can_cleanup) : ?> <span class="shrinkwell-pro-label"><?php esc_html_e('Pro', 'shrinkwell-image-optimizer'); ?></span><?php endif; ?></button>
                <span id="shrinkwell-trash-action-status" aria-live="polite"></span>
            </div>
        </section>

        <div class="shrinkwell-confirm-modal" id="shrinkwell-cleanup-confirm" hidden role="dialog" aria-modal="true" aria-labelledby="shrinkwell-cleanup-confirm-title">
            <div class="shrinkwell-confirm-modal-card">
                <h2 id="shrinkwell-cleanup-confirm-title"><?php esc_html_e('Delete permanently?', 'shrinkwell-image-optimizer'); ?></h2>
                <p id="shrinkwell-cleanup-confirm-message"></p>
                <div class="shrinkwell-confirm-modal-actions">
                    <button type="button" class="button" data-cleanup-cancel><?php esc_html_e('Cancel', 'shrinkwell-image-optimizer'); ?></button>
                    <button type="button" class="button shrinkwell-button-danger" data-cleanup-confirm><?php esc_html_e('Continue', 'shrinkwell-image-optimizer'); ?></button>
                </div>
            </div>
        </div>
        <?php
    }

    private function render_item_rows(array $items): void
    {
        if (!$items) {
            echo '<tr class="shrinkwell-cleanup-empty"><td colspan="6">' . esc_html__('Run a scan to review media usage.', 'shrinkwell-image-optimizer') . '</td></tr>';
            return;
        }

        foreach ($items as $item) {
            $cleanup_eligible = !empty($item['cleanup_eligible']) || !empty($item['eligible']);
            $can_select = $cleanup_eligible && function_exists('shrinkwell_can_bulk_cleanup') && shrinkwell_can_bulk_cleanup();
            $checked = $can_select && !empty($item['selected_by_default']);
            $details_id = 'shrinkwell-cleanup-evidence-' . absint($item['id']);
            echo '<tr class="shrinkwell-cleanup-result-row" data-status="' . esc_attr((string) $item['status']) . '">';
            echo '<th class="check-column"><input type="checkbox" class="shrinkwell-cleanup-item" value="' . esc_attr((string) $item['id']) . '" ' . disabled(!$can_select, true, false) . checked($checked, true, false) . '></th>';
            echo '<td><img class="shrinkwell-cleanup-thumb" src="' . esc_url((string) $item['thumbnail']) . '" alt=""></td>';
            /* translators: %d: Number of usage locations. */
            $usage_text = sprintf(__('Found in: %d locations', 'shrinkwell-image-optimizer'), (int) $item['found_count']);
            if (!empty($item['last_attached'])) {
                /* translators: %s: Last content or settings location containing the image. */
                $usage_text .= ' · ' . sprintf(__('Last attached to: %s', 'shrinkwell-image-optimizer'), (string) $item['last_attached']);
            }
            echo '<td><strong>' . esc_html((string) $item['filename']) . '</strong><small>' . esc_html($usage_text) . '</small><button type="button" class="button-link shrinkwell-cleanup-evidence-toggle" aria-expanded="false" aria-controls="' . esc_attr($details_id) . '">' . esc_html__('Why this status?', 'shrinkwell-image-optimizer') . '</button></td>';
            echo '<td><span class="shrinkwell-cleanup-pill is-' . esc_attr((string) $item['status']) . '">' . esc_html((string) $item['status_label']) . '</span><span class="shrinkwell-confidence is-' . esc_attr((string) ($item['confidence'] ?? 'low')) . '">' . esc_html((string) ($item['confidence_label'] ?? $this->confidence_label('low'))) . '</span></td>';
            echo '<td>' . esc_html((string) $item['size']) . '</td>';
            echo '<td title="' . esc_attr((string) $item['uploaded_at']) . '">' . esc_html((string) $item['uploaded_ago']) . '</td>';
            echo '</tr>';
            echo '<tr class="shrinkwell-cleanup-evidence-row" id="' . esc_attr($details_id) . '" data-evidence-for="' . esc_attr((string) $item['id']) . '" hidden><td colspan="6">';
            $this->render_item_evidence($item);
            echo '</td></tr>';
        }
    }

    private function render_item_evidence(array $item): void
    {
        echo '<div class="shrinkwell-cleanup-evidence">';
        echo '<div class="shrinkwell-cleanup-evidence__heading"><strong>' . esc_html__('Why this status?', 'shrinkwell-image-optimizer') . '</strong><span class="shrinkwell-confidence is-' . esc_attr((string) ($item['confidence'] ?? 'low')) . '">' . esc_html((string) ($item['confidence_label'] ?? $this->confidence_label('low'))) . '</span></div>';
        echo '<p>' . esc_html((string) ($item['status_reason'] ?? __('This result needs a new scan before detailed evidence is available.', 'shrinkwell-image-optimizer'))) . '</p>';

        if (!empty($item['evidence']) && is_array($item['evidence'])) {
            echo '<ul>';
            foreach ($item['evidence'] as $evidence) {
                if (!is_array($evidence)) {
                    continue;
                }
                echo '<li><strong>' . esc_html((string) ($evidence['source_label'] ?? __('Database', 'shrinkwell-image-optimizer'))) . '</strong>';
                echo '<span>' . esc_html((string) ($evidence['reason'] ?? $evidence['detail'] ?? '')) . '</span>';
                echo '<span>' . esc_html((string) ($evidence['match_type'] ?? $evidence['source_type'] ?? '')) . '</span>';
                echo '<span class="shrinkwell-evidence-strength is-' . esc_attr((string) ($evidence['confidence'] ?? 'low')) . '">' . esc_html((string) ($evidence['confidence_label'] ?? $this->confidence_label('low'))) . '</span></li>';
            }
            echo '</ul>';
        }

        if (empty($item['cleanup_eligible']) && empty($item['eligible'])) {
            echo '<p class="shrinkwell-cleanup-evidence__safety">' . esc_html__('This item is not selectable for cleanup from the scan results.', 'shrinkwell-image-optimizer') . '</p>';
        } else {
            echo '<p class="shrinkwell-cleanup-evidence__safety is-eligible">' . esc_html__('High-confidence possibly unused result. Review it before moving it to Shrinkwell Trash.', 'shrinkwell-image-optimizer') . '</p>';
        }
        echo '</div>';
    }

    public function ajax_scan(): void
    {
        $this->verify_ajax();
        $post_data = wp_unslash($_POST); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verified in verify_ajax() above.
        $mode = sanitize_key((string) ($post_data['mode'] ?? 'database'));

        if ('frontend' === $mode) {
            wp_send_json_error(['message' => __('This scan mode is not available.', 'shrinkwell-image-optimizer')], 400);
        }

        wp_raise_memory_limit('admin');
        if (function_exists('set_time_limit')) {
            @set_time_limit(180); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Hosts may disable runtime changes.
        }

        try {
            $scan = $this->scan_database();
        } catch (Throwable $exception) {
            wp_send_json_error([
                'message' => __('Shrinkwell could not finish the media scan on this site. Please try again, or contact support if it keeps happening.', 'shrinkwell-image-optimizer'),
            ], 500);
        }

        update_option(self::SCAN_OPTION, $scan, false);
        wp_send_json_success([
            'scan' => $scan,
            'trash' => $this->trash_summary(),
        ]);
    }

    public function ajax_move_to_trash(): void
    {
        $this->verify_ajax();
        $this->require_pro_cleanup('move');
        $post_data = wp_unslash($_POST); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verified in verify_ajax() above.
        $ids = array_values(array_unique(array_filter(array_map('absint', (array) ($post_data['ids'] ?? [])))));
        $scan = $this->cached_scan();
        $eligible = [];

        foreach ((array) $scan['items'] as $item) {
            if (
                (!empty($item['cleanup_eligible']) || !empty($item['eligible']))
                && 'possibly_unused' === ($item['status'] ?? '')
                && 'high' === ($item['confidence'] ?? '')
            ) {
                $eligible[(int) $item['id']] = true;
            }
        }

        $moved = 0;
        $errors = [];

        foreach ($ids as $attachment_id) {
            if (empty($eligible[$attachment_id]) || !current_user_can('delete_post', $attachment_id)) {
                continue;
            }

            $result = $this->move_attachment_to_trash($attachment_id);
            if (is_wp_error($result)) {
                $errors[] = $result->get_error_message();
            } else {
                $moved++;
            }
        }

        $fresh_scan = $this->scan_database();
        update_option(self::SCAN_OPTION, $fresh_scan, false);

        wp_send_json_success([
            'moved' => $moved,
            'errors' => $errors,
            'scan' => $fresh_scan,
            'trash' => $this->trash_summary(),
        ]);
    }

    public function ajax_restore_all(): void
    {
        $this->verify_ajax();
        $this->require_pro_cleanup('restore');
        global $wpdb;

        $rows = $wpdb->get_results("SELECT attachment_id, original_meta FROM {$this->table_name()} WHERE status = 'trashed' ORDER BY id ASC", ARRAY_A); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $restored = 0;

        foreach ($rows as $row) {
            $attachment_id = (int) $row['attachment_id'];
            $meta = json_decode((string) $row['original_meta'], true);
            $updated = $this->restore_attachment_from_trash($attachment_id, is_array($meta) ? $meta : []);

            if (!is_wp_error($updated)) {
                $restored++;
            }
        }

        $scan = $this->scan_database();
        update_option(self::SCAN_OPTION, $scan, false);
        wp_send_json_success([
            'restored' => $restored,
            'scan' => $scan,
            'trash' => $this->trash_summary(),
        ]);
    }

    private function restore_attachment_from_trash(int $attachment_id, array $meta)
    {
        global $wpdb;

        if (!function_exists('shrinkwell_can_bulk_cleanup') || !shrinkwell_can_bulk_cleanup() || !current_user_can('manage_options') || !current_user_can('edit_post', $attachment_id) || !current_user_can('delete_post', $attachment_id)) {
            return new WP_Error('restore_denied', __('You cannot restore this attachment.', 'shrinkwell-image-optimizer'));
        }

        $original_status = sanitize_key((string) ($meta['post']['post_status'] ?? 'inherit')) ?: 'inherit';
        $updated = wp_untrash_post($attachment_id);

        if (!$updated && get_post($attachment_id)) {
            $updated = wp_update_post(['ID' => $attachment_id, 'post_status' => $original_status], true);
        }

        if (is_wp_error($updated) || !$updated) {
            return is_wp_error($updated) ? $updated : new WP_Error('restore_failed', __('WordPress could not restore this attachment.', 'shrinkwell-image-optimizer'));
        }

        if ($original_status !== get_post_status($attachment_id)) {
            wp_update_post(['ID' => $attachment_id, 'post_status' => $original_status]);
        }

        if ($original_status !== get_post_status($attachment_id)) {
            $wpdb->update($wpdb->posts, ['post_status' => $original_status], ['ID' => $attachment_id], ['%s'], ['%d']);
            clean_post_cache($attachment_id);
        }

        if (!wp_get_attachment_metadata($attachment_id) && !empty($meta['attachment_metadata'])) {
            wp_update_attachment_metadata($attachment_id, (array) $meta['attachment_metadata']);
        }
        if (!get_post_meta($attachment_id, '_wp_attached_file', true) && !empty($meta['attached_file'])) {
            update_post_meta($attachment_id, '_wp_attached_file', (string) $meta['attached_file']);
        }

        $wpdb->delete($this->table_name(), ['attachment_id' => $attachment_id], ['%d']);

        return $attachment_id;
    }

    public function ajax_delete_permanently(): void
    {
        $this->verify_ajax();
        $this->require_pro_cleanup('delete');
        $result = $this->delete_trash_rows(false);
        $scan = $this->scan_database();
        update_option(self::SCAN_OPTION, $scan, false);
        wp_send_json_success([
            'deleted' => $result['deleted'],
            'freed_bytes' => $result['freed_bytes'],
            'freed_formatted' => size_format($result['freed_bytes'], 1),
            'scan' => $scan,
            'trash' => $this->trash_summary(),
        ]);
    }

    public function purge_expired_trash(): void
    {
        if (!function_exists('shrinkwell_can_permanently_delete_media') || !shrinkwell_can_permanently_delete_media()) {
            return;
        }

        $this->delete_trash_rows(true);
    }

    private function delete_trash_rows(bool $expired_only): array
    {
        global $wpdb;

        if (!function_exists('shrinkwell_can_permanently_delete_media') || !shrinkwell_can_permanently_delete_media()) {
            return ['deleted' => 0, 'freed_bytes' => 0];
        }

        if ($expired_only) {
            $rows = $wpdb->get_results(
                $wpdb->prepare("SELECT attachment_id, original_meta FROM {$this->table_name()} WHERE status = 'trashed' AND expires_at <= %s", current_time('mysql', true)),
                ARRAY_A
            ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        } else {
            $rows = $wpdb->get_results("SELECT attachment_id, original_meta FROM {$this->table_name()} WHERE status = 'trashed'", ARRAY_A); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        }
        $deleted = 0;
        $freed_bytes = 0;

        foreach ($rows as $row) {
            $attachment_id = (int) $row['attachment_id'];
            $meta = json_decode((string) $row['original_meta'], true);
            $files = array_values(array_unique(array_filter((array) ($meta['files'] ?? []))));

            if ('trash' !== get_post_status($attachment_id)) {
                continue;
            }

            if (!wp_doing_cron() && (!current_user_can('manage_options') || !current_user_can('delete_post', $attachment_id))) {
                continue;
            }

            $deleted_attachment = wp_delete_attachment($attachment_id, true);
            if (!$deleted_attachment) {
                continue;
            }

            foreach ($files as $file) {
                $this->delete_file_family((string) $file);
            }

            $wpdb->delete($this->table_name(), ['attachment_id' => $attachment_id], ['%d']);
            $deleted++;
            $freed_bytes += (int) ($meta['total_bytes'] ?? 0);
        }

        return compact('deleted', 'freed_bytes');
    }

    private function move_attachment_to_trash(int $attachment_id)
    {
        global $wpdb;

        if (!function_exists('shrinkwell_can_move_to_shrinkwell_trash') || !shrinkwell_can_move_to_shrinkwell_trash() || !current_user_can('manage_options') || !current_user_can('delete_post', $attachment_id)) {
            return new WP_Error('trash_denied', __('You cannot move this attachment to Shrinkwell Trash.', 'shrinkwell-image-optimizer'));
        }

        $post = get_post($attachment_id, ARRAY_A);
        if (!$post || 'attachment' !== $post['post_type'] || 0 !== strpos((string) $post['post_mime_type'], 'image/')) {
            return new WP_Error('invalid_attachment', __('This image could not be moved to Shrinkwell Trash.', 'shrinkwell-image-optimizer'));
        }

        $files = $this->attachment_files($attachment_id);
        $meta = [
            'post' => $post,
            'attachment_metadata' => wp_get_attachment_metadata($attachment_id),
            'attached_file' => get_post_meta($attachment_id, '_wp_attached_file', true),
            'files' => $files,
            'total_bytes' => $this->files_bytes($files),
        ];
        $now = current_time('mysql', true);
        $expires = gmdate('Y-m-d H:i:s', strtotime($now . ' +' . self::TRASH_DAYS . ' days'));

        $saved = $wpdb->replace(
            $this->table_name(),
            [
                'attachment_id' => $attachment_id,
                'file_path' => (string) get_attached_file($attachment_id),
                'original_meta' => wp_json_encode($meta),
                'trashed_at' => $now,
                'expires_at' => $expires,
                'status' => 'trashed',
            ],
            ['%d', '%s', '%s', '%s', '%s', '%s']
        );

        if (false === $saved) {
            return new WP_Error('trash_store_failed', __('Shrinkwell could not save the restore information.', 'shrinkwell-image-optimizer'));
        }

        // Always use the WordPress soft-delete transition. Physical files and the attachment ID stay intact.
        $updated = wp_trash_post($attachment_id);
        if (is_wp_error($updated) || !$updated) {
            $wpdb->delete($this->table_name(), ['attachment_id' => $attachment_id], ['%d']);
            return is_wp_error($updated) ? $updated : new WP_Error('trash_failed', __('WordPress could not move this attachment to Trash.', 'shrinkwell-image-optimizer'));
        }

        return true;
    }

    private function verify_ajax(): void
    {
        check_ajax_referer('shrinkwell_media_cleanup_nonce', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(['message' => __('Permission denied.', 'shrinkwell-image-optimizer')], 403);
        }
    }

    private function require_pro_cleanup(string $action): void
    {
        $allowed = 'delete' === $action
            ? function_exists('shrinkwell_can_permanently_delete_media') && shrinkwell_can_permanently_delete_media()
            : function_exists('shrinkwell_can_bulk_cleanup') && shrinkwell_can_bulk_cleanup();

        if (!$allowed) {
            wp_send_json_error([
                'code' => 'shrinkwell_cleanup_requires_pro',
                'message' => __('Safe Media Cleanup actions are available in Shrinkwell Pro.', 'shrinkwell-image-optimizer'),
            ], 403);
        }
    }

    private function scan_database(): array
    {
        global $wpdb;

        $attachments = get_posts([
            'post_type' => 'attachment',
            'post_mime_type' => 'image',
            'post_status' => 'inherit',
            'posts_per_page' => -1,
            'orderby' => 'ID',
            'order' => 'ASC',
        ]);
        $items = [];
        $path_map = [];
        $basename_map = [];
        $has_unreadable_builder_data = false;
        $has_skipped_large_values = false;
        $protected = array_filter([
            (int) get_option('site_icon') => __('WordPress site icon', 'shrinkwell-image-optimizer'),
            (int) get_theme_mod('custom_logo') => __('Active theme logo', 'shrinkwell-image-optimizer'),
        ]);

        foreach ($attachments as $attachment) {
            $attachment_id = (int) $attachment->ID;
            $files = $this->attachment_files($attachment_id);
            $bytes = $this->files_bytes($files);
            $items[$attachment_id] = [
                'id' => $attachment_id,
                'filename' => wp_basename((string) get_attached_file($attachment_id)),
                'thumbnail' => (string) wp_get_attachment_image_url($attachment_id, 'thumbnail'),
                'bytes' => $bytes,
                'size' => size_format($bytes, 1),
                'uploaded_at' => get_post_time('c', true, $attachment),
                /* translators: %s: Human-readable time difference, such as "9 months". */
                'uploaded_ago' => sprintf(_x('%s ago', 'media upload age', 'shrinkwell-image-optimizer'), human_time_diff(get_post_time('U', true, $attachment), time())),
                'parent_title' => $attachment->post_parent ? get_the_title((int) $attachment->post_parent) : '',
                'locations' => [],
                'review_locations' => [],
                'evidence' => [],
                'last_seen' => 0,
                'last_attached' => '',
            ];

            foreach ($files as $file) {
                $relative = $this->normalize_media_path($file);
                if ($relative) {
                    $path_map[$relative] = $attachment_id;
                }
                $basename = strtolower(wp_basename($file));
                $basename_map[$basename][] = $attachment_id;
            }
        }

        $post_rows = $wpdb->get_results(
            "SELECT ID, post_title, post_modified_gmt, post_modified, post_content
             FROM {$wpdb->posts}
             WHERE post_type <> 'attachment'
               AND post_status NOT IN ('trash', 'auto-draft', 'inherit')
               AND post_content <> ''",
            ARRAY_A
        ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared

        foreach ($post_rows as $row) {
            $timestamp = strtotime((string) ($row['post_modified_gmt'] ?: $row['post_modified']) . ' UTC') ?: time();
            $source = 'post:' . (int) $row['ID'];
            $label = (string) ($row['post_title'] ?: __('Untitled content', 'shrinkwell-image-optimizer'));
            $this->scan_blocks((string) $row['post_content'], $source, $label, $timestamp, $items, $path_map, $basename_map);
            $this->scan_text((string) $row['post_content'], $source, $label, $timestamp, $items, $path_map, $basename_map);
        }

		$meta_rows = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT pm.post_id, pm.meta_key, pm.meta_value, p.post_title, p.post_modified_gmt, p.post_modified
	             FROM {$wpdb->postmeta} pm
	             LEFT JOIN {$wpdb->posts} p ON p.ID = pm.post_id
	             WHERE pm.meta_value <> ''
	               AND LENGTH(pm.meta_value) <= %d
	               AND pm.meta_key NOT LIKE %s
	               AND (p.post_type IS NULL OR p.post_type <> 'attachment')
	               AND (p.post_status IS NULL OR p.post_status NOT IN ('trash', 'auto-draft'))",
				(int) self::MAX_SCAN_VALUE_BYTES,
				$wpdb->esc_like('_shrinkwell') . '%'
			),
			ARRAY_A
		); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared

		$large_meta_rows = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT pm.meta_key, LEFT(pm.meta_value, 4096) AS meta_value
	             FROM {$wpdb->postmeta} pm
	             LEFT JOIN {$wpdb->posts} p ON p.ID = pm.post_id
	             WHERE pm.meta_value <> ''
	               AND LENGTH(pm.meta_value) > %d
	               AND pm.meta_key NOT LIKE %s
	               AND (p.post_type IS NULL OR p.post_type <> 'attachment')
	               AND (p.post_status IS NULL OR p.post_status NOT IN ('trash', 'auto-draft'))
	             LIMIT 50",
				(int) self::MAX_SCAN_VALUE_BYTES,
				$wpdb->esc_like('_shrinkwell') . '%'
			),
			ARRAY_A
		); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared

        foreach ($large_meta_rows as $row) {
            $has_skipped_large_values = true;
            if ($this->builder_data_is_unreadable((string) $row['meta_key'], (string) $row['meta_value'])) {
                $has_unreadable_builder_data = true;
                break;
            }
        }

        foreach ($meta_rows as $row) {
            $timestamp = strtotime((string) ($row['post_modified_gmt'] ?: $row['post_modified']) . ' UTC') ?: time();
            /* translators: %d: WordPress post ID. */
            $fallback_label = sprintf(__('Content #%d', 'shrinkwell-image-optimizer'), (int) $row['post_id']);
            $label = (string) ($row['post_title'] ?: $fallback_label);
            $source = 'meta:' . (int) $row['post_id'] . ':' . (string) $row['meta_key'];

            if ('_thumbnail_id' === $row['meta_key']) {
                $this->mark_used(absint($row['meta_value']), $source, $label, $timestamp, $items, 'featured_image');
            } elseif ('_product_image_gallery' === $row['meta_key']) {
                foreach (wp_parse_id_list((string) $row['meta_value']) as $gallery_id) {
                    $this->mark_used($gallery_id, $source, $label, $timestamp, $items, 'product_gallery');
                }
            }

            $this->scan_keyed_media_value((string) $row['meta_key'], (string) $row['meta_value'], $source, $label, $timestamp, $items);

            if ($this->builder_data_is_unreadable((string) $row['meta_key'], (string) $row['meta_value'])) {
                $has_unreadable_builder_data = true;
            }

            $this->scan_text((string) $row['meta_value'], $source, $label, $timestamp, $items, $path_map, $basename_map);
        }

		$option_rows = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT option_name, option_value FROM {$wpdb->options}
	             WHERE option_value <> ''
	               AND option_name NOT LIKE %s
	               AND option_name NOT LIKE %s
	               AND LENGTH(option_value) <= %d",
				$wpdb->esc_like('_transient') . '%',
				$wpdb->esc_like('shrinkwell_media_cleanup') . '%',
				(int) self::MAX_SCAN_VALUE_BYTES
			),
			ARRAY_A
		); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared

		$large_option_rows = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT option_name, LEFT(option_value, 4096) AS option_value FROM {$wpdb->options}
	             WHERE option_value <> ''
	               AND option_name NOT LIKE %s
	               AND option_name NOT LIKE %s
	               AND LENGTH(option_value) > %d
	             LIMIT 50",
				$wpdb->esc_like('_transient') . '%',
				$wpdb->esc_like('shrinkwell_media_cleanup') . '%',
				(int) self::MAX_SCAN_VALUE_BYTES
			),
			ARRAY_A
		); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared

        foreach ($large_option_rows as $row) {
            $has_skipped_large_values = true;
            if ($this->builder_data_is_unreadable((string) $row['option_name'], (string) $row['option_value'])) {
                $has_unreadable_builder_data = true;
            }
        }

        foreach ($option_rows as $row) {
            if ($this->builder_data_is_unreadable((string) $row['option_name'], (string) $row['option_value'])) {
                $has_unreadable_builder_data = true;
            }
            $this->scan_keyed_media_value((string) $row['option_name'], (string) $row['option_value'], 'option:' . (string) $row['option_name'], __('Site settings', 'shrinkwell-image-optimizer'), time(), $items);
            $this->scan_text((string) $row['option_value'], 'option:' . (string) $row['option_name'], __('Site settings', 'shrinkwell-image-optimizer'), time(), $items, $path_map, $basename_map);
        }

        if (!empty($wpdb->termmeta)) {
            $term_rows = $wpdb->get_results(
                "SELECT tm.term_id, tm.meta_key, tm.meta_value, t.name
                 FROM {$wpdb->termmeta} tm
                 LEFT JOIN {$wpdb->terms} t ON t.term_id = tm.term_id
                 WHERE tm.meta_value <> ''",
                ARRAY_A
            ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared

            foreach ($term_rows as $row) {
                $label = (string) ($row['name'] ?: sprintf(__('Term #%d', 'shrinkwell-image-optimizer'), (int) $row['term_id']));
                $source = 'termmeta:' . (int) $row['term_id'] . ':' . (string) $row['meta_key'];
                if ('thumbnail_id' === $row['meta_key']) {
                    $this->mark_used(absint($row['meta_value']), $source, $label, time(), $items, 'term_thumbnail');
                }
                $this->scan_keyed_media_value((string) $row['meta_key'], (string) $row['meta_value'], $source, $label, time(), $items);
                $this->scan_text((string) $row['meta_value'], $source, $label, time(), $items, $path_map, $basename_map);
            }
        }

        $this->scan_theme_files($items, $path_map, $basename_map);

        $summary = $this->empty_summary();
        $summary['total_scanned'] = count($items);
        $cutoff = time() - (90 * DAY_IN_SECONDS);
        $result_items = [];

        foreach ($items as $attachment_id => $item) {
            $found = count($item['locations']);
            if (isset($protected[$attachment_id])) {
                $status = 'protected';
                $confidence = 'high';
                $reason = sprintf(
                    /* translators: %s: Protected WordPress media role. */
                    __('Protected because it is configured as the %s.', 'shrinkwell-image-optimizer'),
                    (string) $protected[$attachment_id]
                );
                $item['evidence'][] = $this->evidence_entry(
                    'setting',
                    (string) $protected[$attachment_id],
                    'protected_media_setting',
                    'high',
                    __('WordPress settings', 'shrinkwell-image-optimizer'),
                    (string) $protected[$attachment_id]
                );
                $summary['protected']++;
                $summary['used']++;
            } elseif ($found > 0 && $item['last_seen'] > 0 && $item['last_seen'] < $cutoff) {
                $status = 'old';
                $confidence = $this->evidence_confidence($item['evidence']);
                $reason = __('References were found, but the newest matching content has not changed in more than 90 days.', 'shrinkwell-image-optimizer');
                $summary['old']++;
            } elseif ($found > 0) {
                $status = 'used';
                $confidence = $this->evidence_confidence($item['evidence']);
                $reason = __('A media reference matching this attachment was found in the database or active theme.', 'shrinkwell-image-optimizer');
                $summary['used']++;
            } elseif (!empty($item['review_locations']) || $has_unreadable_builder_data || $has_skipped_large_values) {
                $status = 'needs_review';
                $confidence = 'low';
                if (!empty($item['review_locations'])) {
                    $reason = __('A possible filename-only reference was found, but it is not strong enough to prove usage.', 'shrinkwell-image-optimizer');
                } elseif ($has_unreadable_builder_data) {
                    $reason = __('Some builder data could not be decoded safely, so Shrinkwell cannot rule out a hidden reference.', 'shrinkwell-image-optimizer');
                } else {
                    $reason = __('Some large metadata or option values were skipped for safety, so Shrinkwell cannot rule out a hidden reference.', 'shrinkwell-image-optimizer');
                }
                if ($has_unreadable_builder_data) {
                    $item['evidence'][] = $this->evidence_entry(
                        'builder_data',
                        'unreadable builder value',
                        'unreadable_builder_data',
                        'low',
                        __('Page builder data', 'shrinkwell-image-optimizer'),
                        __('At least one builder value could not be decoded safely.', 'shrinkwell-image-optimizer')
                    );
                }
                if ($has_skipped_large_values) {
                    $item['evidence'][] = $this->evidence_entry(
                        'database',
                        'large skipped value',
                        'skipped_large_value',
                        'low',
                        __('Large database value', 'shrinkwell-image-optimizer'),
                        __('At least one metadata or option value was too large to scan safely.', 'shrinkwell-image-optimizer')
                    );
                }
                $summary['needs_review']++;
            } else {
                $status = 'possibly_unused';
                $confidence = 'high';
                $reason = __('No matching attachment ID, upload path, URL, protected setting, or readable builder reference was found.', 'shrinkwell-image-optimizer');
                $item['evidence'][] = $this->evidence_entry(
                    'database_scan',
                    'completed scan',
                    'no_reference_found',
                    'high',
                    __('Completed database scan', 'shrinkwell-image-optimizer'),
                    __('No reference was found in readable posts, metadata, options, or active theme files.', 'shrinkwell-image-optimizer')
                );
                $summary['possibly_unused']++;
                $summary['possibly_unused_high_confidence']++;
                $summary['selected_by_default']++;
                $summary['potential_bytes'] += (int) $item['bytes'];
                $summary['selected_bytes'] += (int) $item['bytes'];
            }

            $cleanup_eligible = 'possibly_unused' === $status && 'high' === $confidence;

            $result_items[] = [
                'id' => $attachment_id,
                'filename' => $item['filename'],
                'thumbnail' => $item['thumbnail'],
                'bytes' => $item['bytes'],
                'size' => $item['size'],
                'uploaded_at' => $item['uploaded_at'],
                'uploaded_ago' => $item['uploaded_ago'],
                'found_count' => $found,
                'last_attached' => $item['last_attached'] ?: $item['parent_title'],
                'status' => $status,
                'status_label' => $this->status_label($status),
                'confidence' => $confidence,
                'confidence_label' => $this->confidence_label($confidence),
                'status_reason' => $reason,
                'evidence' => array_values($item['evidence']),
                'cleanup_eligible' => $cleanup_eligible,
                'selected_by_default' => $cleanup_eligible,
                'eligible' => $cleanup_eligible,
            ];
        }

        usort($result_items, static function (array $a, array $b): int {
            $order = ['possibly_unused' => 0, 'needs_review' => 1, 'old' => 2, 'protected' => 3, 'used' => 4];
            return ($order[$a['status']] ?? 9) <=> ($order[$b['status']] ?? 9) ?: $b['bytes'] <=> $a['bytes'];
        });

        $summary['potential_formatted'] = size_format($summary['potential_bytes'], 1);
        $summary['selected_formatted'] = size_format($summary['selected_bytes'], 1);

        return [
            'scanned_at' => current_time('mysql'),
            'items' => $result_items,
            'summary' => $summary,
        ];
    }

    private function scan_blocks(string $content, string $source, string $label, int $timestamp, array &$items, array $path_map, array $basename_map): void
    {
        if (!function_exists('parse_blocks') || false === strpos($content, '<!-- wp:')) {
            return;
        }

        foreach (parse_blocks($content) as $block) {
            $this->scan_block($block, $source, $label, $timestamp, $items, $path_map, $basename_map);
        }
    }

    private function scan_block(array $block, string $source, string $label, int $timestamp, array &$items, array $path_map, array $basename_map): void
    {
        $attrs = is_array($block['attrs'] ?? null) ? $block['attrs'] : [];

        foreach (['id', 'mediaId', 'media_id', 'imageId', 'image_id', 'logoId', 'logo_id'] as $key) {
            if (isset($attrs[$key])) {
                foreach ($this->numeric_ids_from_value($attrs[$key], true) as $attachment_id) {
                    $this->mark_usage($attachment_id, $source, $label, $timestamp, $items, false, 'gutenberg_image_id', 'high');
                }
            }
        }

        foreach (['ids', 'images', 'mediaIds', 'media_ids', 'galleryIds', 'gallery_ids'] as $key) {
            if (isset($attrs[$key])) {
                foreach ($this->numeric_ids_from_value($attrs[$key]) as $attachment_id) {
                    $this->mark_usage($attachment_id, $source, $label, $timestamp, $items, false, 'gutenberg_gallery_id', 'high');
                }
            }
        }

        if (!empty($attrs['url']) && is_string($attrs['url'])) {
            $this->scan_text((string) $attrs['url'], $source, $label, $timestamp, $items, $path_map, $basename_map);
        }

        if (!empty($block['innerHTML']) && is_string($block['innerHTML'])) {
            $this->scan_text((string) $block['innerHTML'], $source, $label, $timestamp, $items, $path_map, $basename_map);
        }

        foreach ((array) ($block['innerBlocks'] ?? []) as $inner_block) {
            if (is_array($inner_block)) {
                $this->scan_block($inner_block, $source, $label, $timestamp, $items, $path_map, $basename_map);
            }
        }
    }

    private function scan_text(string $text, string $source, string $label, int $timestamp, array &$items, array $path_map, array $basename_map, bool $review_only = false): void
    {
        if ('' === $text) {
            return;
        }

        $search_text = html_entity_decode(str_replace('\/', '/', $text), ENT_QUOTES | ENT_HTML5, 'UTF-8');

        if (preg_match_all('/(?:wp-image-|attachment[_-]?id["\']?\s*[:=]\s*|["\'](?:image|image_id|media_id|photo_id|logo_id)["\']\s*[:=]\s*["\']?|s:\d+:["\'](?:image|image_id|media_id|attachment_id|photo_id|logo_id)["\'];(?:i|s:\d+):["\']?)(\d+)/i', $search_text, $matches)) {
            foreach ($matches[1] as $match) {
                $this->mark_usage(absint($match), $source, $label, $timestamp, $items, $review_only, 'explicit_id', 'medium');
            }
        }

        $this->scan_builder_id_lists($search_text, $source, $label, $timestamp, $items, $review_only);
        $this->scan_structured_media_data($search_text, $source, $label, $timestamp, $items, $review_only);

        if (preg_match_all('~(?:https?:)?//[^\s"\'<>)]*\.(?:jpe?g|png|gif|webp|avif)(?:\?[^\s"\'<>)]*)?|/[^\s"\'<>)]*\.(?:jpe?g|png|gif|webp|avif)(?:\?[^\s"\'<>)]*)?~i', $search_text, $url_matches)) {
            foreach ($url_matches[0] as $url) {
                $url_review_only = $review_only;
                $path = $this->normalize_media_path((string) $url);
                $url_host = strtolower((string) wp_parse_url((string) $url, PHP_URL_HOST));
                $site_host = strtolower((string) wp_parse_url(home_url('/'), PHP_URL_HOST));
                $external_url = '' !== $url_host && '' !== $site_host && $url_host !== $site_host;
                $attachment_id = !$external_url && $path && isset($path_map[$path]) ? (int) $path_map[$path] : 0;

                if (!$attachment_id) {
                    $basename = strtolower(wp_basename((string) (wp_parse_url($url, PHP_URL_PATH) ?: $url)));
                    if (1 === count($basename_map[$basename] ?? [])) {
                        $attachment_id = (int) $basename_map[$basename][0];
                        $url_review_only = true;
                    }
                }

                $this->mark_usage(
                    $attachment_id,
                    $source,
                    $label,
                    $timestamp,
                    $items,
                    $url_review_only,
                    $url_review_only ? 'filename' : 'exact_path',
                    $url_review_only ? 'low' : $this->path_match_confidence($source)
                );
            }
        }

        if (preg_match_all('/[A-Za-z0-9+\/=]{160,}/', $search_text, $encoded_matches)) {
            foreach (array_slice($encoded_matches[0], 0, 20) as $encoded) {
                $decoded = base64_decode($encoded, true);
                if (false !== $decoded && $decoded !== $text && preg_match('/\.(?:jpe?g|png|gif|webp|avif)|(?:image|media|attachment)[_-]?id/i', $decoded)) {
                    $this->scan_text($decoded, $source . ':decoded', $label, $timestamp, $items, $path_map, $basename_map, $review_only);
                }
            }
        }
    }

    private function scan_keyed_media_value(string $key, string $value, string $source, string $label, int $timestamp, array &$items): void
    {
        $normalized_key = ltrim(strtolower(str_replace('-', '_', $key)), '_');
        if (!$this->is_media_field_key($normalized_key) && !$this->is_media_list_key($normalized_key)) {
            return;
        }

        $decoded = is_serialized($value) ? maybe_unserialize($value) : $value;
        $single = $this->is_media_field_key($normalized_key);
        foreach ($this->numeric_ids_from_value($decoded, $single) as $attachment_id) {
            $this->mark_used($attachment_id, $source, $label, $timestamp, $items, 'keyed_media_id');
        }
    }

    private function scan_builder_id_lists(string $text, string $source, string $label, int $timestamp, array &$items, bool $review_only): void
    {
        $single_keys = 'image|image_id|media|media_id|attachment|attachment_id|photo|photo_id|logo|logo_id|poster|poster_id|thumbnail|thumbnail_id|featured_image|background_image|background_image_id|bg_image|bg_image_id|video_poster';
        $list_keys = 'ids|gallery|gallery_ids|images|image_ids|media_ids|attachment_ids|selected_images|slides|include';
        $single_patterns = [
            '/(?<![A-Za-z0-9_])(?:' . $single_keys . ')\s*=\s*["\']?(\d+)(?::[A-Za-z0-9_-]+)?["\']?/i',
            '/["\'](?:' . $single_keys . ')["\']\s*:\s*["\']?(\d+)(?::[A-Za-z0-9_-]+)?["\']?/i',
            '/s:\d+:["\'](?:' . $single_keys . ')["\'];(?:i|s:\d+):["\']?(\d+)/i',
        ];
        $list_patterns = [
            '/(?<![A-Za-z0-9_])(?:' . $list_keys . ')\s*=\s*["\']([\d,\s]+)["\']/i',
            '/["\'](?:' . $list_keys . ')["\']\s*:\s*["\']([\d,\s]+)["\']/i',
            '/["\'](?:' . $list_keys . ')["\']\s*:\s*\[([\d,\s]+)\]/i',
            '/s:\d+:["\'](?:' . $list_keys . ')["\'];s:\d+:["\']([\d,\s]+)["\']/i',
        ];

        foreach ($single_patterns as $pattern) {
            if (!preg_match_all($pattern, $text, $matches)) {
                continue;
            }

            foreach ($matches[1] as $attachment_id) {
                $this->mark_usage(absint($attachment_id), $source, $label, $timestamp, $items, $review_only, 'builder_id', 'medium');
            }
        }

        foreach ($list_patterns as $pattern) {
            if (!preg_match_all($pattern, $text, $matches)) {
                continue;
            }

            foreach ($matches[1] as $ids) {
                foreach (wp_parse_id_list((string) $ids) as $attachment_id) {
                    $this->mark_usage($attachment_id, $source, $label, $timestamp, $items, $review_only, 'builder_id_list', 'medium');
                }
            }
        }
    }

    private function scan_structured_media_data(string $text, string $source, string $label, int $timestamp, array &$items, bool $review_only): void
    {
        $trimmed = trim($text);
        if ('' === $trimmed) {
            return;
        }

        $data = null;
        if (in_array($trimmed[0], ['{', '['], true)) {
            $decoded = json_decode($trimmed, true);
            if (JSON_ERROR_NONE === json_last_error()) {
                $data = $decoded;
            }
        } elseif (is_serialized($trimmed)) {
            $data = maybe_unserialize($trimmed);
        }

        if (!is_array($data)) {
            return;
        }

        $this->scan_structured_media_value($data, '', $source, $label, $timestamp, $items, $review_only, 0);
    }

    private function scan_structured_media_value($value, string $context_key, string $source, string $label, int $timestamp, array &$items, bool $review_only, int $depth): void
    {
        if ($depth > 20 || !is_array($value)) {
            return;
        }

        $normalized_context = strtolower(str_replace('-', '_', $context_key));
        $media_context = $this->is_media_field_key($normalized_context) || $this->is_media_list_key($normalized_context) || $this->structured_array_looks_like_media($value);

        foreach ($value as $key => $child) {
            $normalized_key = strtolower(str_replace('-', '_', (string) $key));

            if ($this->is_media_list_key($normalized_key)) {
                foreach ($this->numeric_ids_from_value($child) as $attachment_id) {
                    $this->mark_usage($attachment_id, $source, $label, $timestamp, $items, $review_only, 'structured_media_id', 'medium');
                }
            } elseif ($this->is_media_field_key($normalized_key) || ('id' === $normalized_key && $media_context)) {
                foreach ($this->numeric_ids_from_value($child, true) as $attachment_id) {
                    $this->mark_usage($attachment_id, $source, $label, $timestamp, $items, $review_only, 'structured_media_id', 'medium');
                }
            }

            if (is_array($child)) {
                $child_context = is_int($key) || ctype_digit((string) $key) ? $normalized_context : $normalized_key;
                $this->scan_structured_media_value($child, $child_context, $source, $label, $timestamp, $items, $review_only, $depth + 1);
            }
        }
    }

    private function structured_array_looks_like_media(array $value): bool
    {
        foreach ($value as $key => $child) {
            $normalized_key = strtolower(str_replace('-', '_', (string) $key));
            if ($this->is_media_field_key($normalized_key) || $this->is_media_list_key($normalized_key)) {
                return true;
            }

            if (in_array($normalized_key, ['type', 'widgettype', 'widget_type', 'element_type'], true) && is_string($child) && preg_match('/(?:image|gallery|media|photo|carousel|slider|logo)/i', $child)) {
                return true;
            }
        }

        return false;
    }

    private function is_media_field_key(string $key): bool
    {
        return (bool) preg_match('/^(?:image|image_id|media|media_id|attachment|attachment_id|photo|photo_id|logo|logo_id|poster|poster_id|thumbnail|thumbnail_id|featured_image|background_image|background_image_id|bg_image|bg_image_id|video_poster)$/', $key);
    }

    private function is_media_list_key(string $key): bool
    {
        return (bool) preg_match('/^(?:ids|gallery|gallery_ids|images|image_ids|media_ids|attachment_ids|selected_images|slides|include)$/', $key);
    }

    private function numeric_ids_from_value($value, bool $single = false): array
    {
        if (is_int($value) || (is_string($value) && preg_match('/^\d+(?::[A-Za-z0-9_-]+)?$/', trim($value)))) {
            return [absint($value)];
        }

        if (is_string($value) && !$single && preg_match('/^[\d,\s]+$/', $value)) {
            return wp_parse_id_list($value);
        }

        if (!is_array($value)) {
            return [];
        }

        $ids = [];
        foreach ($value as $child) {
            if (is_int($child) || (is_string($child) && ctype_digit(trim($child)))) {
                $ids[] = absint($child);
            }
        }

        return array_values(array_unique(array_filter($ids)));
    }

    private function builder_data_is_unreadable(string $key, string $value): bool
    {
        if (!preg_match('/(?:elementor|et_pb|divi|wpb|vc_|fl_builder|beaver|bricks|oxygen|cornerstone|fusion|avada|builder|page[_-]?data|editor[_-]?data|content[_-]?data|module[_-]?data|widget[_-]?data|components?)/i', $key)) {
            return false;
        }

        $trimmed = ltrim($value);
        if ('' === $trimmed) {
            return false;
        }

        $looks_like_shortcode = '[' === $trimmed[0] && (bool) preg_match('/^\[[a-z0-9_:-]+(?:\s|\/?\])/i', $trimmed);
        if (!$looks_like_shortcode && in_array($trimmed[0], ['{', '['], true)) {
            json_decode($trimmed, true);
            if (JSON_ERROR_NONE !== json_last_error()) {
                return true;
            }
        }

        if (preg_match('/^(?:a|O|C|s|i|b|d):/i', $trimmed) && !is_serialized($trimmed)) {
            return true;
        }

        if (!preg_match_all('/[A-Za-z0-9+\/=]{160,}/', $value, $matches)) {
            return false;
        }

        foreach (array_slice($matches[0], 0, 20) as $encoded) {
            $decoded = base64_decode($encoded, true);
            if (false === $decoded || ('' !== $decoded && !wp_check_invalid_utf8($decoded))) {
                return true;
            }
        }

        return false;
    }

    private function mark_used(int $attachment_id, string $source, string $label, int $timestamp, array &$items, string $match_type = 'explicit_id'): void
    {
        $this->mark_usage($attachment_id, $source, $label, $timestamp, $items, false, $match_type, 'high');
    }

    private function mark_usage(
        int $attachment_id,
        string $source,
        string $label,
        int $timestamp,
        array &$items,
        bool $review_only,
        string $match_type = 'explicit_id',
        string $confidence = 'high'
    ): void
    {
        if (!$attachment_id || !isset($items[$attachment_id])) {
            return;
        }

        $source_label = $this->source_label($source, $label);
        $evidence = $this->evidence_entry(
            $this->source_type($source),
            $source,
            $match_type,
            $review_only ? 'low' : $confidence,
            $source_label,
            $this->match_detail($match_type)
        );
        $evidence_key = md5($match_type . '|' . $source . '|' . $source_label);
        $items[$attachment_id]['evidence'][$evidence_key] = $evidence;

        if ($review_only) {
            $items[$attachment_id]['review_locations'][$source] = $source_label;
            return;
        }

        $items[$attachment_id]['locations'][$source] = $source_label;
        if ($timestamp >= (int) $items[$attachment_id]['last_seen']) {
            $items[$attachment_id]['last_seen'] = $timestamp;
            $items[$attachment_id]['last_attached'] = $source_label;
        }
    }

    private function path_match_confidence(string $source): string
    {
        if (0 === strpos($source, 'option:')) {
            return 'medium';
        }

        if (0 === strpos($source, 'meta:') && preg_match('/(?:elementor|et_pb|divi|wpb|vc_|fl_builder|beaver|bricks|oxygen|cornerstone|fusion|avada|builder|page[_-]?data|editor[_-]?data|content[_-]?data|module[_-]?data|widget[_-]?data|components?)/i', $source)) {
            return 'medium';
        }

        return 'high';
    }

    private function scan_theme_files(array &$items, array $path_map, array $basename_map): void
    {
        $theme = wp_get_theme();
        $root = $theme->get_stylesheet_directory();
        if (!is_dir($root)) {
            return;
        }

        $directory = new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS);
        $filtered = new RecursiveCallbackFilterIterator(
            $directory,
            static function (SplFileInfo $file): bool {
                if (!$file->isDir()) {
                    return true;
                }

                return !in_array(strtolower($file->getFilename()), [
                    '.git',
                    'build',
                    'cache',
                    'dist',
                    'node_modules',
                    'tests',
                    'vendor',
                ], true);
            }
        );
        $iterator = new RecursiveIteratorIterator($filtered);
        $checked = 0;

        foreach ($iterator as $file) {
            if ($checked >= 120) {
                break;
            }

            if (!$file->isFile() || $file->getSize() > 512 * KB_IN_BYTES || !preg_match('/\.(?:php|css|scss|html?)$/i', $file->getFilename())) {
                continue;
            }

            $contents = file_get_contents($file->getPathname());
            if (false !== $contents) {
                $this->scan_text($contents, 'theme:' . $file->getFilename(), __('Active theme or CSS file', 'shrinkwell-image-optimizer'), time(), $items, $path_map, $basename_map);
            }
            $checked++;
        }
    }

    private function normalize_media_path(string $value): string
    {
        $uploads = wp_get_upload_dir();
        $path = wp_parse_url($value, PHP_URL_PATH);
        $path = false === $path || null === $path ? $value : $path;
        $path = rawurldecode(str_replace('\\', '/', (string) $path));
        $base_path = (string) wp_parse_url((string) $uploads['baseurl'], PHP_URL_PATH);

        if ($base_path && false !== strpos($path, $base_path . '/')) {
            $path = substr($path, strpos($path, $base_path . '/') + strlen($base_path) + 1);
        } elseif (false !== strpos($path, str_replace('\\', '/', (string) $uploads['basedir']) . '/')) {
            $path = substr($path, strpos($path, str_replace('\\', '/', (string) $uploads['basedir']) . '/') + strlen(str_replace('\\', '/', (string) $uploads['basedir'])) + 1);
        }

        return strtolower(ltrim(preg_replace('/\?.*$/', '', $path), '/'));
    }

    private function attachment_files(int $attachment_id): array
    {
        $file = get_attached_file($attachment_id);
        if (!$file) {
            return [];
        }

        $files = [$file];
        $meta = wp_get_attachment_metadata($attachment_id);
        $directory = trailingslashit(dirname($file));

        foreach ((array) ($meta['sizes'] ?? []) as $size) {
            if (!empty($size['file'])) {
                $files[] = $directory . wp_basename((string) $size['file']);
            }
        }

        if (!empty($meta['original_image'])) {
            $files[] = $directory . wp_basename((string) $meta['original_image']);
        }

        $core_files = array_values(array_unique($files));

        foreach ($core_files as $core_file) {
            $files[] = $core_file . '.shrinkwell-original';
            $files[] = (string) preg_replace('/\.(jpe?g|png|gif|webp)$/i', '.webp', $core_file);
            $files[] = (string) preg_replace('/\.(jpe?g|png|gif|webp)$/i', '.avif', $core_file);
        }

        return array_values(array_unique(array_filter($files, 'is_file')));
    }

    private function files_bytes(array $files): int
    {
        $bytes = 0;
        foreach ($files as $file) {
            if (is_file($file)) {
                $bytes += (int) filesize($file);
            }
        }

        return $bytes;
    }

    private function delete_file_family(string $file): void
    {
        $candidates = [$file, $file . '.shrinkwell-original'];
        $candidates[] = (string) preg_replace('/\.(jpe?g|png|gif|webp)$/i', '.webp', $file);
        $candidates[] = (string) preg_replace('/\.(jpe?g|png|gif|webp)$/i', '.avif', $file);

        foreach (array_unique($candidates) as $candidate) {
            if ($candidate && is_file($candidate)) {
                wp_delete_file($candidate);
            }
        }
    }

    private function source_label(string $source, string $label): string
    {
        $decoded = false !== strpos($source, ':decoded');
        $source = str_replace(':decoded', '', $source);

        if (0 === strpos($source, 'post:')) {
            /* translators: %s: Post, page, or product title. */
            $display = sprintf(__('Content: %s', 'shrinkwell-image-optimizer'), $label);
        } elseif (0 === strpos($source, 'meta:')) {
            $parts = explode(':', $source, 3);
            $meta_key = (string) ($parts[2] ?? '');
            if ('_thumbnail_id' === $meta_key) {
                /* translators: %s: Post, page, or product title. */
                $display = sprintf(__('Featured image: %s', 'shrinkwell-image-optimizer'), $label);
            } elseif ('_product_image_gallery' === $meta_key) {
                /* translators: %s: WooCommerce product title. */
                $display = sprintf(__('Product gallery: %s', 'shrinkwell-image-optimizer'), $label);
            } else {
                /* translators: 1: Custom field name. 2: Post, page, or product title. */
                $display = sprintf(__('Custom field %1$s in %2$s', 'shrinkwell-image-optimizer'), $meta_key, $label);
            }
        } elseif (0 === strpos($source, 'option:')) {
            /* translators: %s: WordPress option name. */
            $display = sprintf(__('Site setting: %s', 'shrinkwell-image-optimizer'), substr($source, 7));
        } elseif (0 === strpos($source, 'termmeta:')) {
            $parts = explode(':', $source, 3);
            $meta_key = (string) ($parts[2] ?? '');
            if ('thumbnail_id' === $meta_key) {
                /* translators: %s: WooCommerce product category name. */
                $display = sprintf(__('Product category thumbnail: %s', 'shrinkwell-image-optimizer'), $label);
            } else {
                /* translators: 1: Term metadata key. 2: Term name. */
                $display = sprintf(__('Term field %1$s in %2$s', 'shrinkwell-image-optimizer'), $meta_key, $label);
            }
        } elseif (0 === strpos($source, 'theme:')) {
            /* translators: %s: Active theme filename. */
            $display = sprintf(__('Active theme file: %s', 'shrinkwell-image-optimizer'), substr($source, 6));
        } else {
            $display = $label;
        }

        return $decoded
            ? sprintf(
                /* translators: %s: Human-readable media source label. */
                __('Decoded data in %s', 'shrinkwell-image-optimizer'),
                $display
            )
            : $display;
    }

    private function source_type(string $source): string
    {
        $source = str_replace(':decoded', '', $source);

        if (0 === strpos($source, 'post:')) {
            return 'post_content';
        }

        if (0 === strpos($source, 'meta:')) {
            return 'postmeta';
        }

        if (0 === strpos($source, 'termmeta:')) {
            return 'termmeta';
        }

        if (0 === strpos($source, 'option:')) {
            return 'option';
        }

        if (0 === strpos($source, 'theme:')) {
            return 'theme_file';
        }

        return 'database';
    }

    private function evidence_entry(string $source_type, string $source_key, string $match_type, string $confidence, string $source_label, string $reason): array
    {
        return [
            'source_type' => $source_type,
            'confidence' => $confidence,
            'confidence_label' => $this->confidence_label($confidence),
            'source_label' => $source_label,
            'source_key' => $source_key,
            'match_type' => $match_type,
            'reason' => $reason,
            'type' => $match_type,
            'detail' => $reason,
        ];
    }

    private function evidence_confidence(array $evidence): string
    {
        $score = ['low' => 1, 'medium' => 2, 'high' => 3];
        $highest = 'low';

        foreach ($evidence as $entry) {
            $confidence = is_array($entry) ? (string) ($entry['confidence'] ?? 'low') : 'low';
            if (($score[$confidence] ?? 0) > ($score[$highest] ?? 0)) {
                $highest = $confidence;
            }
        }

        return $highest;
    }

    private function match_detail(string $match_type): string
    {
        $details = [
            'featured_image' => __('Exact attachment ID stored as a featured image.', 'shrinkwell-image-optimizer'),
            'product_gallery' => __('Exact attachment ID stored in a WooCommerce product gallery.', 'shrinkwell-image-optimizer'),
            'term_thumbnail' => __('Exact attachment ID stored as a WooCommerce product category thumbnail.', 'shrinkwell-image-optimizer'),
            'keyed_media_id' => __('Exact attachment ID stored in a media-specific field.', 'shrinkwell-image-optimizer'),
            'explicit_id' => __('Attachment ID found in media-related content markup.', 'shrinkwell-image-optimizer'),
            'gutenberg_image_id' => __('Exact image ID found in a WordPress image block.', 'shrinkwell-image-optimizer'),
            'gutenberg_gallery_id' => __('Exact image ID found in a WordPress gallery block.', 'shrinkwell-image-optimizer'),
            'builder_id' => __('Attachment ID found in builder or shortcode media data.', 'shrinkwell-image-optimizer'),
            'builder_id_list' => __('Attachment ID found in a builder gallery or media list.', 'shrinkwell-image-optimizer'),
            'structured_media_id' => __('Attachment ID found in readable JSON or serialized media data.', 'shrinkwell-image-optimizer'),
            'exact_path' => __('Exact media URL or upload path matched this attachment or a generated size.', 'shrinkwell-image-optimizer'),
            'filename' => __('Filename matched, but the full upload path could not be confirmed.', 'shrinkwell-image-optimizer'),
            'protected_media_setting' => __('Attachment ID is configured as protected WordPress media.', 'shrinkwell-image-optimizer'),
            'unreadable_builder_data' => __('Builder data could not be decoded safely.', 'shrinkwell-image-optimizer'),
            'skipped_large_value' => __('Large database value was skipped to keep the scan safe.', 'shrinkwell-image-optimizer'),
            'no_reference_found' => __('Readable database and theme sources did not reference this attachment.', 'shrinkwell-image-optimizer'),
        ];

        return $details[$match_type] ?? __('Media-related database evidence matched this attachment.', 'shrinkwell-image-optimizer');
    }

    private function confidence_label(string $confidence): string
    {
        $labels = [
            'high' => __('High confidence', 'shrinkwell-image-optimizer'),
            'medium' => __('Medium confidence', 'shrinkwell-image-optimizer'),
            'low' => __('Low confidence', 'shrinkwell-image-optimizer'),
        ];

        return $labels[$confidence] ?? $labels['low'];
    }

    private function status_label(string $status): string
    {
        $labels = [
            'used' => __('Used', 'shrinkwell-image-optimizer'),
            'possibly_unused' => __('Possibly unused', 'shrinkwell-image-optimizer'),
            'old' => __('Not seen for 90+ days', 'shrinkwell-image-optimizer'),
            'needs_review' => __('Needs review', 'shrinkwell-image-optimizer'),
            'protected' => __('Protected', 'shrinkwell-image-optimizer'),
        ];

        return $labels[$status] ?? $labels['needs_review'];
    }
}
