<?php
/**
 * Main plugin class.
 *
 * @package Shrinkwell_Image_Optimizer
 */

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

require_once __DIR__ . '/class-shrinkwell-media-cleanup.php';

class Shrinkwell_Image_Optimizer
{
    public const VERSION = '1.16.4';
    private const OPTIMIZATION_ENGINE_VERSION = 'delivery-savings-v2';
    private const OPTION = 'shrinkwell_settings';
    private const STATS_OPTION = 'shrinkwell_stats';
    private const LOG_OPTION = 'shrinkwell_bulk_log';
    private const CHECKS_OPTION = 'shrinkwell_checks';
    private const REVIEW_OPTION = 'shrinkwell_review_notice';
    private const DEMO_OPTION = 'shrinkwell_demo_preview';
    private const META = '_shrinkwell_optimization';
    private const BACKUP_SUFFIX = '.shrinkwell-original';
    private const ZERO_SAVINGS_TRANSIENT = 'shrinkwell_zero_savings_report_v2';
    private const ZERO_SAVINGS_DISMISSED_META = '_shrinkwell_zero_savings_dismissed';
    private const SAVINGS_PERCENT_TRANSIENT = 'shrinkwell_savings_percent_summary';

    private static ?self $instance = null;
    private static bool $cwebp_checked = false;
    private static ?string $cwebp_path = null;

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

        return self::$instance;
    }

    private function __construct()
    {
        Shrinkwell_Media_Cleanup::instance();
        add_action('admin_menu', [$this, 'add_admin_menu']);
        add_action('admin_init', [$this, 'register_settings']);
        add_filter('option_page_capability_shrinkwell_settings_group', [$this, 'settings_capability']);
        add_filter('plugin_action_links_' . plugin_basename(SHRINKWELL_PLUGIN_FILE), [$this, 'plugin_action_links']);
        add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_assets']);
        add_action('admin_notices', [$this, 'admin_notices']);
        add_action('wp_dashboard_setup', [$this, 'shrinkwell_register_dashboard_widget']);

        add_filter('wp_generate_attachment_metadata', [$this, 'maybe_auto_optimize'], 20, 3);
        add_filter('wp_get_attachment_image', [$this, 'add_webp_picture_source'], 10, 5);
        add_filter('the_content', [$this, 'add_webp_sources_to_content'], 20);
        add_filter('widget_text', [$this, 'add_webp_sources_to_content'], 20);
        add_action('template_redirect', [$this, 'maybe_start_frontend_buffer'], 1);
        add_action('wp_enqueue_scripts', [$this, 'enqueue_frontend_media_assets']);
        add_filter('manage_media_columns', [$this, 'add_media_column']);
        add_action('manage_media_custom_column', [$this, 'render_media_column'], 10, 2);
        add_action('attachment_submitbox_misc_actions', [$this, 'render_attachment_submitbox_status']);
        add_filter('attachment_fields_to_edit', [$this, 'add_attachment_fields_status'], 10, 2);
        add_filter('media_row_actions', [$this, 'add_media_row_action'], 10, 2);
        add_action('restrict_manage_posts', [$this, 'render_media_library_filters'], 10, 2);
        add_action('pre_get_posts', [$this, 'filter_media_library_query']);

        add_action('admin_post_shrinkwell_optimize_attachment', [$this, 'handle_single_optimize']);
        add_action('admin_post_shrinkwell_test_attachment', [$this, 'handle_single_test']);
        add_action('admin_post_shrinkwell_restore_attachment', [$this, 'handle_single_restore']);
        add_action('admin_post_shrinkwell_restore_all_backups', [$this, 'handle_restore_all_backups']);
        add_action('admin_post_shrinkwell_review_notice', [$this, 'handle_review_notice_action']);
        add_action('wp_ajax_shrinkwell_test_attachment_compression', [$this, 'ajax_test_attachment_compression']);
        add_action('wp_ajax_shrinkwell_bulk_ids', [$this, 'ajax_bulk_ids']);
        add_action('wp_ajax_shrinkwell_bulk_optimize', [$this, 'ajax_bulk_optimize']);
        add_action('wp_ajax_shrinkwell_log_client_error', [$this, 'ajax_log_client_error']);
        add_action('wp_ajax_shrinkwell_delivery_test', [$this, 'ajax_delivery_test']);
        add_action('wp_ajax_shrinkwell_frontend_proof_scan', [$this, 'ajax_frontend_proof_scan']);
        add_action('wp_ajax_shrinkwell_recommendation_scan', [$this, 'ajax_recommendation_scan']);
        add_action('wp_ajax_shrinkwell_apply_recommendations', [$this, 'ajax_apply_recommendations']);
        add_action('wp_ajax_shrinkwell_seo_scan', [$this, 'ajax_seo_scan']);
        add_action('wp_ajax_shrinkwell_save_seo_fields', [$this, 'ajax_save_seo_fields']);
        add_action('wp_ajax_shrinkwell_zero_savings_report', [$this, 'shrinkwell_ajax_get_zero_savings_report']);
        add_action('wp_ajax_shrinkwell_force_reoptimize', [$this, 'shrinkwell_ajax_force_reoptimize']);
        add_action('wp_ajax_shrinkwell_dismiss_zero_savings', [$this, 'shrinkwell_ajax_dismiss_zero_savings']);
        add_action('wp_ajax_shrinkwell_backup_cleanup_preview', [$this, 'ajax_backup_cleanup_preview']);
        add_action('wp_ajax_shrinkwell_cleanup_old_backups', [$this, 'ajax_cleanup_old_backups']);
        add_action('wp_ajax_shrinkwell_activate_license', [$this, 'ajax_activate_license']);
    }

    public static function activate(): void
    {
        Shrinkwell_Media_Cleanup::activate();
        add_option(self::OPTION, self::default_settings());
        add_option(self::STATS_OPTION, [
            'optimized_images' => 0,
            'saved_bytes' => 0,
        ]);
        add_option(self::LOG_OPTION, []);
        add_option(self::CHECKS_OPTION, self::default_checks());
        add_option(self::REVIEW_OPTION, self::default_review_notice());
    }

    public static function uninstall(): void
    {
        delete_option(self::OPTION);
        delete_option(self::STATS_OPTION);
        delete_option(self::LOG_OPTION);
        delete_option(self::CHECKS_OPTION);
        delete_option(self::REVIEW_OPTION);
        delete_option(self::DEMO_OPTION);
        delete_option('shrinkwell_media_cleanup_scan');
        delete_option('shrinkwell_media_cleanup_db_version');

        global $wpdb;

        $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}shrinkwell_trash"); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared

        $cleanup_timestamp = wp_next_scheduled('shrinkwell_cleanup_expired_trash');
        if ($cleanup_timestamp) {
            wp_unschedule_event($cleanup_timestamp, 'shrinkwell_cleanup_expired_trash');
        }

        $wpdb->delete(
            $wpdb->postmeta,
            ['meta_key' => self::META],
            ['%s']
        );

        $wpdb->delete(
            $wpdb->postmeta,
            ['meta_key' => self::ZERO_SAVINGS_DISMISSED_META],
            ['%s']
        );

        delete_transient(self::ZERO_SAVINGS_TRANSIENT);
        delete_transient(self::SAVINGS_PERCENT_TRANSIENT);
    }

    public static function default_settings(): array
    {
        return [
            'auto_optimize' => 1,
            'compression_profile' => 'balanced',
            'quality' => 74,
            'max_width' => 2560,
            'max_height' => 0,
            'max_file_size_mb' => 10,
            'optimize_thumbnails' => 1,
            'strip_exif' => 1,
            'create_webp' => 1,
            'serve_webp' => 1,
            'create_avif' => 0,
            'serve_avif' => 0,
            'backup_originals' => 1,
        ];
    }

    public static function compression_profiles(): array
    {
        return [
            'balanced' => [
                'label' => __('Balanced', 'shrinkwell-image-optimizer'),
                'quality' => 74,
                'description' => __('Balanced makes files smaller while keeping photos natural. Best default for most websites.', 'shrinkwell-image-optimizer'),
                'recommended' => 1,
            ],
            'high' => [
                'label' => __('High compression', 'shrinkwell-image-optimizer'),
                'quality' => 70,
                'description' => __('High compression prioritizes smaller files and faster pages. Use it when speed matters more than perfect image detail.', 'shrinkwell-image-optimizer'),
                'recommended' => 0,
            ],
            'lossless' => [
                'label' => __('Near lossless', 'shrinkwell-image-optimizer'),
                'quality' => 95,
                'description' => __('Near lossless keeps images almost identical to the original, but saves less space than the other options.', 'shrinkwell-image-optimizer'),
                'recommended' => 0,
            ],
            'custom' => [
                'label' => __('Custom', 'shrinkwell-image-optimizer'),
                'quality' => 82,
                'description' => __('Custom uses your own compression strength. Lower strength keeps more quality; higher strength creates smaller files with more visible compression risk.', 'shrinkwell-image-optimizer'),
                'recommended' => 0,
            ],
        ];
    }

    private static function default_checks(): array
    {
        return [
            'delivery_tested_at' => '',
            'delivery_passed' => 0,
            'delivery_title' => '',
            'seo_scanned_at' => '',
            'seo_scanned' => 0,
            'seo_issues' => 0,
        ];
    }

    private static function default_review_notice(): array
    {
        return [
            'activated_at' => time(),
            'dismissed' => 0,
            'remind_after' => 0,
        ];
    }

    public function settings(): array
    {
        $settings = wp_parse_args((array) get_option(self::OPTION, []), self::default_settings());

        if (!shrinkwell_feature_available('avif')) {
            $settings['create_avif'] = 0;
            $settings['serve_avif'] = 0;
        }

        if (!shrinkwell_can_use_auto_optimize()) {
            $settings['auto_optimize'] = 0;
        }

        return $settings;
    }

    private function checks(): array
    {
        return wp_parse_args((array) get_option(self::CHECKS_OPTION, []), self::default_checks());
    }

    private function is_demo_preview(): bool
    {
        return (bool) get_option(self::DEMO_OPTION, false);
    }

    private function demo_stats(): array
    {
        return [
            'optimized_images' => 124,
            'saved_bytes' => (int) round(38.6 * MB_IN_BYTES),
        ];
    }

    private function demo_media_counts(): array
    {
        return [
            'total' => 141,
            'unoptimized' => 17,
            'processable' => 17,
            'too_large' => 0,
            'already_optimized' => 124,
            'scanned' => 141,
            'truncated' => false,
        ];
    }

    private function demo_checks(): array
    {
        return [
            'delivery_tested_at' => current_time('mysql'),
            'delivery_passed' => 1,
            'delivery_title' => __('Frontend delivery OK', 'shrinkwell-image-optimizer'),
            'seo_scanned_at' => current_time('mysql'),
            'seo_scanned' => 1,
            'seo_issues' => 3,
        ];
    }

    private function review_notice(): array
    {
        $notice = wp_parse_args((array) get_option(self::REVIEW_OPTION, []), self::default_review_notice());

        if (empty($notice['activated_at'])) {
            $notice['activated_at'] = time();
            update_option(self::REVIEW_OPTION, $notice, false);
        }

        return $notice;
    }

    private function quality_for_settings(array $settings): int
    {
        $profile = (string) ($settings['compression_profile'] ?? 'balanced');
        $profiles = self::compression_profiles();

        if ('custom' === $profile) {
            return min(95, max(40, (int) ($settings['quality'] ?? 82)));
        }

        return (int) ($profiles[$profile]['quality'] ?? $profiles['balanced']['quality']);
    }

    private static function custom_strength_for_quality(int $quality): int
    {
        $quality = min(95, max(40, $quality));

        return min(10, max(1, (int) round(((95 - $quality) / 55) * 9) + 1));
    }

    private static function custom_quality_for_strength(int $strength): int
    {
        $strength = min(10, max(1, $strength));

        return min(95, max(40, (int) round(95 - ((($strength - 1) / 9) * 55))));
    }

    private function media_counts(): array
    {
        static $cache = null;

        if (null !== $cache) {
            return $cache;
        }

        $cached = get_transient('shrinkwell_media_counts');

        if (is_array($cached)) {
            $cache = $cached;
            return $cache;
        }

        $total = new WP_Query([
            'post_type' => 'attachment',
            'post_mime_type' => ['image/jpeg', 'image/png', 'image/webp'],
            'post_status' => 'inherit',
            'fields' => 'ids',
            'posts_per_page' => 1,
        ]);

        $candidate_query = new WP_Query([
            'post_type' => 'attachment',
            'post_mime_type' => ['image/jpeg', 'image/png', 'image/webp'],
            'post_status' => 'inherit',
            'fields' => 'ids',
            'posts_per_page' => 5000,
            'no_found_rows' => true,
        ]);
        $unoptimized = 0;
        $processable = 0;
        $too_large = 0;
        $already_optimized = 0;

        foreach ($candidate_query->posts as $attachment_id) {
            if ($this->attachment_needs_optimization((int) $attachment_id)) {
                $unoptimized++;

                if ($this->attachment_processing_limit_error((int) $attachment_id)) {
                    $too_large++;
                } else {
                    $processable++;
                }

                continue;
            }

            $already_optimized++;
        }

        $cache = [
            'total' => (int) $total->found_posts,
            'unoptimized' => $unoptimized,
            'processable' => $processable,
            'too_large' => $too_large,
            'already_optimized' => $already_optimized,
            'scanned' => count($candidate_query->posts),
            'truncated' => 5000 === count($candidate_query->posts),
        ];

        set_transient('shrinkwell_media_counts', $cache, 5 * MINUTE_IN_SECONDS);

        return $cache;
    }

    private function missing_alt_text_count(): int
    {
        $query = new WP_Query([
            'post_type' => 'attachment',
            'post_mime_type' => ['image/jpeg', 'image/png', 'image/webp', 'image/avif'],
            'post_status' => 'inherit',
            'fields' => 'ids',
            'posts_per_page' => 1,
            'meta_query' => [
                'relation' => 'OR',
                [
                    'key' => '_wp_attachment_image_alt',
                    'compare' => 'NOT EXISTS',
                ],
                [
                    'key' => '_wp_attachment_image_alt',
                    'value' => '',
                    'compare' => '=',
                ],
            ],
        ]);

        return (int) $query->found_posts;
    }

    private function dashboard_summary(): array
    {
        $stats = wp_parse_args((array) get_option(self::STATS_OPTION, []), [
            'optimized_images' => 0,
            'saved_bytes' => 0,
        ]);
        $demo_preview = $this->is_demo_preview();

        if ($demo_preview) {
            $stats = $this->demo_stats();
        }
        $media_counts = $demo_preview ? $this->demo_media_counts() : $this->media_counts();
        $missing_alt = $demo_preview ? 3 : $this->missing_alt_text_count();
        $pending = (int) ($media_counts['processable'] ?? 0);
        $too_large = (int) ($media_counts['too_large'] ?? 0);
        $status = 'green';
        $status_label = __('Healthy', 'shrinkwell-image-optimizer');

        if ($too_large > 0 || $pending > 50 || $missing_alt > 50) {
            $status = 'red';
            $status_label = __('Needs attention', 'shrinkwell-image-optimizer');
        } elseif ($pending > 0 || $missing_alt > 0) {
            $status = 'orange';
            $status_label = __('Review recommended', 'shrinkwell-image-optimizer');
        }

        return [
            'optimized_images' => (int) $stats['optimized_images'],
            'saved_bytes' => (int) $stats['saved_bytes'],
            'pending' => $pending,
            'too_large' => $too_large,
            'missing_alt' => $missing_alt,
            'status' => $status,
            'status_label' => $status_label,
        ];
    }

    private function attachment_processing_limit_error(int $attachment_id)
    {
        $file = get_attached_file($attachment_id);

        if (!$file || !file_exists($file)) {
            return null;
        }

        $bytes = filesize($file);

        if (!$bytes) {
            return null;
        }

        $check = $this->check_local_processing_limits((string) $file, (int) $bytes);

        return is_wp_error($check) ? $check : null;
    }

    private function usage_tracker(): ?object
    {
        $tracker = $GLOBALS['shrinkwell_usage_tracker'] ?? null;

        return is_object($tracker) ? $tracker : null;
    }

    private function render_free_limit_notice(bool $demo_preview): void
    {
        $has_unlimited_optimization = function_exists('shrinkwell_can_use_unlimited_optimization') && shrinkwell_can_use_unlimited_optimization();
        $has_cleanup_actions = function_exists('shrinkwell_can_bulk_cleanup') && shrinkwell_can_bulk_cleanup();

        if ($demo_preview || ($has_unlimited_optimization && $has_cleanup_actions)) {
            return;
        }

        $usage_tracker = $this->usage_tracker();
        $usage_display = '';
        $limit_display = '60 MB';

        if ($usage_tracker && method_exists($usage_tracker, 'get_usage_display')) {
            $usage_display = (string) $usage_tracker->get_usage_display();
        }

        if ($usage_tracker && method_exists($usage_tracker, 'get_monthly_limit_bytes')) {
            $limit_display = size_format((int) $usage_tracker->get_monthly_limit_bytes(), 0);
        }
        ?>
        <section class="shrinkwell-panel shrinkwell-free-limit-card">
            <div class="shrinkwell-free-limit-card__icon" aria-hidden="true"></div>
            <div class="shrinkwell-free-limit-card__body">
                <span class="shrinkwell-free-limit-card__eyebrow"><?php esc_html_e('Free plan', 'shrinkwell-image-optimizer'); ?></span>
                <h2>
                    <?php
                    echo esc_html(
                        sprintf(
                            /* translators: %s: Monthly free optimization limit. */
                            __('Free processing limit: %s per month.', 'shrinkwell-image-optimizer'),
                            $limit_display
                        )
                    );
                    ?>
                </h2>
                <p>
                    <?php if ('' !== $usage_display) : ?>
                        <strong><?php echo esc_html($usage_display); ?></strong>
                    <?php endif; ?>
                    <?php esc_html_e('WebP and AVIF are included in Free within this monthly limit.', 'shrinkwell-image-optimizer'); ?>
                </p>
                <p class="shrinkwell-free-limit-card__note"><?php esc_html_e('This monthly limit counts source media processed, not storage saved. Total savings is shown separately above.', 'shrinkwell-image-optimizer'); ?></p>
            </div>
            <a class="button button-primary" href="<?php echo esc_url($this->tab_url('pro')); ?>"><?php esc_html_e('Upgrade to Pro', 'shrinkwell-image-optimizer'); ?></a>
        </section>
        <?php
    }

    private function usage_bytes_for_attachment_files(array $files): int
    {
        $bytes = 0;

        foreach (array_unique(array_map('strval', $files)) as $path) {
            if (file_exists($path) && $this->is_optimizable_path($path)) {
                $bytes += max(0, (int) filesize($path));
            }
        }

        return $bytes;
    }

    private function monthly_usage_limit_error(): WP_Error
    {
        return new WP_Error(
            'shrinkwell_monthly_usage_limit_reached',
            __('The next image would exceed this month’s free plan limit. The limit counts source media processed, not storage saved. Free includes WebP and AVIF conversion within the monthly cap. Upgrade to Pro to keep optimizing without limits and unlock safe Media Cleanup.', 'shrinkwell-image-optimizer')
        );
    }

    private function server_can_use_avif(): bool
    {
        /**
         * Filters whether Shrinkwell should expose AVIF creation and serving controls.
         *
         * This is an extension point, not a paid-feature lock.
         *
         * @param bool $can_use_avif Whether the current WordPress image editor supports AVIF.
         */
        return (bool) apply_filters('shrinkwell_can_use_avif', shrinkwell_server_supports_avif());
    }

    private function bulk_batch_limit(): int
    {
        /**
         * Filters the maximum number of media IDs returned to one bulk scan request.
         *
         * This keeps admin AJAX requests predictable while allowing site owners to tune larger installs.
         *
         * @param int $limit Maximum number of IDs returned per request.
         */
        return max(1, min(1000, (int) apply_filters('shrinkwell_bulk_batch_limit', 200)));
    }

    private function optimization_signature(?array $settings = null): string
    {
        $settings = $settings ?? $this->settings();
        $profile = (string) ($settings['compression_profile'] ?? 'balanced');
        $quality = (string) $this->quality_for_settings($settings);
        $width = (string) ($settings['max_width'] ?? 0);
        $height = (string) ($settings['max_height'] ?? 0);
        $thumbs = (string) ($settings['optimize_thumbnails'] ?? 1);
        $exif = (string) ($settings['strip_exif'] ?? 1);
        $webp = (string) ($settings['create_webp'] ?? 1);
        $avif = (string) ($settings['create_avif'] ?? 0);

        return md5(implode('|', [
            self::OPTIMIZATION_ENGINE_VERSION,
            $profile,
            $quality,
            $width,
            $height,
            $thumbs,
            $exif,
            $webp,
            $avif,
        ]));
    }

    private function attachment_needs_optimization(int $attachment_id): bool
    {
        if (!$this->is_optimizable_attachment($attachment_id)) {
            return false;
        }

        $file = get_attached_file($attachment_id);

        if (!$file || !file_exists($file)) {
            return false;
        }

        $meta = (array) get_post_meta($attachment_id, self::META, true);

        if (empty($meta['optimized_at'])) {
            return true;
        }

        return empty($meta['settings_signature']) || $meta['settings_signature'] !== $this->optimization_signature();
    }

    private function recent_logs(int $limit = 10): array
    {
        $logs = (array) get_option(self::LOG_OPTION, []);

        return array_slice($logs, 0, $limit);
    }

    private function add_bulk_log_entry(array $entry): void
    {
        $logs = (array) get_option(self::LOG_OPTION, []);
        array_unshift($logs, wp_parse_args($entry, [
            'time' => current_time('mysql'),
            'attachment_id' => 0,
            'title' => '',
            'status' => 'unknown',
            'saved_bytes' => 0,
            'message' => '',
        ]));

        update_option(self::LOG_OPTION, array_slice($logs, 0, 250), false);
    }

    private function recent_attachment_issue(int $attachment_id): string
    {
        foreach ($this->recent_logs(250) as $log) {
            if ((int) ($log['attachment_id'] ?? 0) !== $attachment_id) {
                continue;
            }

            if (!in_array((string) ($log['status'] ?? ''), ['error', 'skipped'], true)) {
                continue;
            }

            return sanitize_text_field((string) ($log['message'] ?? ''));
        }

        return '';
    }

    private function recent_attachment_issue_status(int $attachment_id): string
    {
        foreach ($this->recent_logs(250) as $log) {
            if ((int) ($log['attachment_id'] ?? 0) !== $attachment_id) {
                continue;
            }

            $status = (string) ($log['status'] ?? '');

            if (in_array($status, ['error', 'skipped'], true)) {
                return $status;
            }
        }

        return '';
    }

    private function backup_inventory(int $row_limit = 50): array
    {
        $inventory = [
            'images' => 0,
            'files' => 0,
            'bytes' => 0,
            'rows' => [],
        ];

        foreach ($this->backup_attachment_ids() as $attachment_id) {
            $summary = $this->attachment_backup_summary($attachment_id);

            if (empty($summary['files'])) {
                continue;
            }

            $inventory['images']++;
            $inventory['files'] += (int) $summary['files'];
            $inventory['bytes'] += (int) $summary['bytes'];

            if (count($inventory['rows']) < $row_limit) {
                $meta = (array) get_post_meta($attachment_id, self::META, true);
                $inventory['rows'][] = [
                    'attachment_id' => $attachment_id,
                    'title' => $this->attachment_title($attachment_id),
                    'files' => (int) $summary['files'],
                    'bytes' => (int) $summary['bytes'],
                    'saved_bytes' => (int) ($meta['saved_bytes'] ?? 0),
                ];
            }
        }

        return $inventory;
    }

    private function backup_attachment_ids(): array
    {
        $ids = [];
        $page = 1;

        do {
            $query = new WP_Query([
                'post_type' => 'attachment',
                'post_mime_type' => ['image/jpeg', 'image/png', 'image/webp'],
                'post_status' => 'inherit',
                'fields' => 'ids',
                'posts_per_page' => 200,
                'paged' => $page,
                'orderby' => 'ID',
                'order' => 'ASC',
            ]);

            $ids = array_merge($ids, array_map('absint', $query->posts));
            $page++;
        } while (!empty($query->posts) && $page <= (int) $query->max_num_pages);

        return array_values(array_unique($ids));
    }

    private function attachment_backup_summary(int $attachment_id): array
    {
        $metadata = (array) wp_get_attachment_metadata($attachment_id);
        $file = get_attached_file($attachment_id);
        $summary = [
            'files' => 0,
            'bytes' => 0,
        ];

        if (!$file) {
            return $summary;
        }

        foreach ($this->attachment_files($file, $metadata, true) as $path) {
            $backup_path = $path . self::BACKUP_SUFFIX;

            if (!file_exists($backup_path)) {
                continue;
            }

            $summary['files']++;
            $summary['bytes'] += (int) filesize($backup_path);
        }

        return $summary;
    }

    private function backup_cleanup_candidates(int $days = 30): array
    {
        $days = max(1, min(3650, $days));
        $cutoff = time() - ($days * DAY_IN_SECONDS);
        $items = [];
        $image_ids = [];
        $reclaim_bytes = 0;
        $savings_bytes = 0;

        foreach ($this->backup_attachment_ids() as $attachment_id) {
            $metadata = (array) wp_get_attachment_metadata($attachment_id);
            $file = get_attached_file($attachment_id);

            if (!$file) {
                continue;
            }

            foreach ($this->attachment_files((string) $file, $metadata, true) as $path) {
                $backup_path = $path . self::BACKUP_SUFFIX;

                if (!file_exists($backup_path) || !file_exists($path)) {
                    continue;
                }

                $modified = (int) filemtime($backup_path);

                if ($modified >= $cutoff) {
                    continue;
                }

                $backup_size = (int) filesize($backup_path);
                $current_size = (int) filesize($path);
                $saved_for_file = $backup_size - $current_size;

                if ($backup_size <= 0 || $saved_for_file <= 0) {
                    continue;
                }

                $items[] = [
                    'attachment_id' => (int) $attachment_id,
                    'title' => $this->attachment_title((int) $attachment_id),
                    'filename' => wp_basename($backup_path),
                    'path' => $path,
                    'backup_path' => $backup_path,
                    'backup_size' => $backup_size,
                    'current_size' => $current_size,
                    'savings_bytes' => $saved_for_file,
                    'modified' => $modified,
                ];

                $image_ids[(int) $attachment_id] = true;
                $reclaim_bytes += $backup_size;
                $savings_bytes += $saved_for_file;
            }
        }

        return [
            'days' => $days,
            'images' => count($image_ids),
            'files' => count($items),
            'reclaim_bytes' => $reclaim_bytes,
            'savings_bytes' => $savings_bytes,
            'items' => $items,
        ];
    }

    private function backup_cleanup_check_key(): string
    {
        return 'shrinkwell_backup_cleanup_check_' . get_current_user_id();
    }

    private function backup_cleanup_signature(array $items): string
    {
        $checked = [];
        foreach ($items as $item) {
            $checked[] = (string) ($item['backup_path'] ?? '') . ':' . (int) ($item['backup_size'] ?? 0);
        }
        sort($checked, SORT_STRING);

        return hash('sha256', implode('|', $checked));
    }

    public function shrinkwell_cleanup_old_backups(int $days = 30): array
    {
        $candidates = $this->backup_cleanup_candidates($days);
        $deleted_files = 0;
        $freed_bytes = 0;
        $errors = [];

        foreach ($candidates['items'] as $item) {
            $backup_path = (string) ($item['backup_path'] ?? '');
            $attachment_id = (int) ($item['attachment_id'] ?? 0);
            $filename = (string) ($item['filename'] ?? wp_basename($backup_path));

            if (!$backup_path || !file_exists($backup_path)) {
                continue;
            }

            if (!wp_delete_file($backup_path)) {
                $errors[] = [
                    'attachment_id' => $attachment_id,
                    'filename' => $filename,
                    'message' => __('Could not delete this backup file.', 'shrinkwell-image-optimizer'),
                ];
                continue;
            }

            $deleted_files++;
            $freed_bytes += (int) ($item['backup_size'] ?? 0);

            $this->add_bulk_log_entry([
                'attachment_id' => $attachment_id,
                'title' => (string) ($item['title'] ?? ''),
                'status' => 'backup_cleanup',
                'saved_bytes' => (int) ($item['backup_size'] ?? 0),
                'message' => sprintf(
                    /* translators: %s: Deleted backup filename. */
                    __('Deleted old backup file: %s', 'shrinkwell-image-optimizer'),
                    $filename
                ),
            ]);
        }

        delete_transient('shrinkwell_media_counts');

        return [
            'deleted_files' => $deleted_files,
            'freed_bytes' => $freed_bytes,
            'freedFormatted' => size_format($freed_bytes, 1),
            'errors' => $errors,
        ];
    }

    public function ajax_backup_cleanup_preview(): void
    {
        check_ajax_referer('shrinkwell_cleanup_backups_nonce', 'nonce');

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

        $post_data = wp_unslash($_POST);
        $days = max(1, absint($post_data['days'] ?? 30));
        $preview = $this->backup_cleanup_candidates($days);
        $check_token = wp_generate_password(32, false, false);
        set_transient($this->backup_cleanup_check_key(), [
            'token' => $check_token,
            'days' => $days,
            'files' => (int) $preview['files'],
            'reclaim_bytes' => (int) $preview['reclaim_bytes'],
            'signature' => $this->backup_cleanup_signature((array) $preview['items']),
        ], 15 * MINUTE_IN_SECONDS);

        wp_send_json_success([
            'days' => (int) $preview['days'],
            'images' => (int) $preview['images'],
            'files' => (int) $preview['files'],
            'reclaim_bytes' => (int) $preview['reclaim_bytes'],
            'reclaimFormatted' => size_format((int) $preview['reclaim_bytes'], 1),
            'savings_bytes' => (int) $preview['savings_bytes'],
            'savingsFormatted' => size_format((int) $preview['savings_bytes'], 1),
            'checkToken' => $check_token,
        ]);
    }

    public function ajax_cleanup_old_backups(): void
    {
        check_ajax_referer('shrinkwell_cleanup_backups_nonce', 'nonce');

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

        $post_data = wp_unslash($_POST);
        $days = max(1, absint($post_data['days'] ?? 30));
        $check_token = sanitize_text_field((string) ($post_data['check_token'] ?? ''));
        $checked = get_transient($this->backup_cleanup_check_key());

        if (!is_array($checked) || empty($checked['token']) || !hash_equals((string) $checked['token'], $check_token) || (int) $checked['days'] !== $days) {
            wp_send_json_error(['message' => __('Check safe backups again before deleting.', 'shrinkwell-image-optimizer')], 400);
        }

        $current_candidates = $this->backup_cleanup_candidates($days);
        if (!hash_equals((string) ($checked['signature'] ?? ''), $this->backup_cleanup_signature((array) $current_candidates['items']))) {
            delete_transient($this->backup_cleanup_check_key());
            wp_send_json_error(['message' => __('The safe backup list changed. Check safe backups again before deleting.', 'shrinkwell-image-optimizer')], 409);
        }

        delete_transient($this->backup_cleanup_check_key());

        wp_send_json_success($this->shrinkwell_cleanup_old_backups($days));
    }

    public function shrinkwell_analyze_zero_savings(): array
    {
        $cached = get_transient(self::ZERO_SAVINGS_TRANSIENT);

        if (is_array($cached)) {
            return $cached;
        }

        $report = [];

        foreach ($this->processed_attachment_ids_for_zero_savings() as $attachment_id) {
            $attachment_id = (int) $attachment_id;

            if (get_post_meta($attachment_id, self::ZERO_SAVINGS_DISMISSED_META, true)) {
                continue;
            }

            $sizes = $this->attachment_zero_savings_sizes($attachment_id);

            if (empty($sizes['current_size'])) {
                continue;
            }

            $original_size = (int) $sizes['original_size'];
            $current_size = (int) $sizes['current_size'];
            $savings_bytes = max(0, $original_size - $current_size);
            $savings_pct = $original_size > 0 ? round(($savings_bytes / $original_size) * 100, 1) : 0.0;

            if ($savings_bytes > 0 && $savings_pct >= 1.0) {
                continue;
            }

            $file = get_attached_file($attachment_id);
            $thumb = wp_get_attachment_image_src($attachment_id, 'thumbnail');

            $report[] = [
                'attachment_id' => $attachment_id,
                'filename' => $file ? wp_basename((string) $file) : $this->attachment_title($attachment_id, '#' . $attachment_id),
                'original_size' => $original_size,
                'current_size' => $current_size,
                'savings_bytes' => $savings_bytes,
                'savings_pct' => $savings_pct,
                'reason' => $this->zero_savings_reason($attachment_id, $original_size, (string) $file),
                'thumbnail_url' => is_array($thumb) ? (string) $thumb[0] : '',
            ];
        }

        set_transient(self::ZERO_SAVINGS_TRANSIENT, $report, HOUR_IN_SECONDS);

        return $report;
    }

    public function shrinkwell_ajax_get_zero_savings_report(): void
    {
        check_ajax_referer('shrinkwell_zero_savings_nonce', 'nonce');

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

        $report = $this->shrinkwell_analyze_zero_savings();

        wp_send_json_success([
            'items' => $report,
            'summary' => $this->zero_savings_summary($report),
        ]);
    }

    public function shrinkwell_ajax_force_reoptimize(): void
    {
        check_ajax_referer('shrinkwell_force_reoptimize_nonce', 'nonce');

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

        $post_data = wp_unslash($_POST);
        $raw_ids = isset($post_data['attachment_ids']) ? (array) $post_data['attachment_ids'] : [];

        if (isset($post_data['attachment_id'])) {
            $raw_ids[] = $post_data['attachment_id'];
        }

        $attachment_ids = array_slice(array_values(array_unique(array_filter(array_map('absint', $raw_ids)))), 0, 20);

        if (!$attachment_ids) {
            wp_send_json_error(['message' => __('No images were selected for re-optimization.', 'shrinkwell-image-optimizer')]);
        }

        $processed = 0;
        $processed_ids = [];
        $saved_bytes = 0;
        $errors = [];

        foreach ($attachment_ids as $attachment_id) {
            if ('attachment' !== get_post_type($attachment_id)) {
                $errors[] = [
                    'attachment_id' => $attachment_id,
                    'message' => __('Image not found.', 'shrinkwell-image-optimizer'),
                ];
                continue;
            }

            $previous_meta = (array) get_post_meta($attachment_id, self::META, true);
            $previous_saved = (int) ($previous_meta['saved_bytes'] ?? 0);
            delete_post_meta($attachment_id, self::META);
            delete_post_meta($attachment_id, '_shrinkwell_optimized');
            delete_post_meta($attachment_id, self::ZERO_SAVINGS_DISMISSED_META);

            $result = $this->optimize_attachment($attachment_id);

            if (is_wp_error($result)) {
                if ($previous_meta) {
                    update_post_meta($attachment_id, self::META, $previous_meta);
                }

                $errors[] = [
                    'attachment_id' => $attachment_id,
                    'message' => $result->get_error_message(),
                ];
                continue;
            }

            if (!empty($previous_meta['optimized_at'])) {
                $this->remove_from_global_stats($previous_saved);
            }

            $processed++;
            $processed_ids[] = $attachment_id;
            $saved_bytes += (int) ($result['saved_bytes'] ?? 0);

            $this->add_bulk_log_entry([
                'attachment_id' => $attachment_id,
                'title' => $this->attachment_title($attachment_id),
                'status' => 'optimized',
                'saved_bytes' => (int) ($result['saved_bytes'] ?? 0),
                'message' => __('Force re-optimized from the zero savings report.', 'shrinkwell-image-optimizer'),
            ]);
        }

        delete_transient(self::ZERO_SAVINGS_TRANSIENT);
        $stats = wp_parse_args((array) get_option(self::STATS_OPTION, []), [
            'optimized_images' => 0,
            'saved_bytes' => 0,
        ]);

        wp_send_json_success([
            'processed' => $processed,
            'processedIds' => $processed_ids,
            'saved_bytes' => $saved_bytes,
            'savedFormatted' => size_format($saved_bytes, 1),
            'total_saved_bytes' => (int) $stats['saved_bytes'],
            'totalSavedFormatted' => size_format((int) $stats['saved_bytes'], 1),
            'errors' => $errors,
        ]);
    }

    public function shrinkwell_ajax_dismiss_zero_savings(): void
    {
        check_ajax_referer('shrinkwell_force_reoptimize_nonce', 'nonce');

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

        $post_data = wp_unslash($_POST);
        $attachment_ids = array_values(array_unique(array_filter(array_map('absint', (array) ($post_data['attachment_ids'] ?? [])))));
        $dismissed = 0;

        foreach ($attachment_ids as $attachment_id) {
            if ('attachment' !== get_post_type($attachment_id)) {
                continue;
            }

            update_post_meta($attachment_id, self::ZERO_SAVINGS_DISMISSED_META, current_time('mysql'));
            $dismissed++;
        }

        delete_transient(self::ZERO_SAVINGS_TRANSIENT);

        wp_send_json_success([
            'dismissed' => $dismissed,
        ]);
    }

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

        $sql = $wpdb->prepare(
            "SELECT DISTINCT p.ID
            FROM {$wpdb->posts} p
            INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
            LEFT JOIN {$wpdb->postmeta} dismissed ON p.ID = dismissed.post_id AND dismissed.meta_key = %s
            WHERE p.post_type = %s
                AND p.post_mime_type IN (%s, %s, %s, %s)
                AND pm.meta_key IN (%s, %s)
                AND dismissed.post_id IS NULL
            ORDER BY p.ID DESC",
            self::ZERO_SAVINGS_DISMISSED_META,
            'attachment',
            'image/jpeg',
            'image/png',
            'image/webp',
            'image/avif',
            self::META,
            '_shrinkwell_optimized'
        );

        return array_values(array_map('absint', (array) $wpdb->get_col($sql))); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Prepared above with fixed placeholders.
    }

    private function attachment_zero_savings_sizes(int $attachment_id): array
    {
        $metadata = (array) wp_get_attachment_metadata($attachment_id);
        $file = get_attached_file($attachment_id);
        $meta = (array) get_post_meta($attachment_id, self::META, true);
        $backup_summary = $this->attachment_backup_summary($attachment_id);
        $current_size = 0;

        if ($file) {
            foreach ($this->attachment_files((string) $file, $metadata, true) as $path) {
                if (file_exists($path)) {
                    $current_size += (int) filesize($path);
                }
            }
        }

        $original_size = (int) ($backup_summary['bytes'] ?? 0);

        if ($original_size <= 0) {
            $original_size = $current_size + max(0, (int) ($meta['saved_bytes'] ?? 0));
        }

        return [
            'original_size' => $original_size,
            'current_size' => $current_size,
        ];
    }

    private function zero_savings_reason(int $attachment_id, int $original_size, string $file): string
    {
        $mime = (string) get_post_mime_type($attachment_id);
        $file_mime = $file ? $this->mime_for_path($file) : '';

        if ($original_size > 0 && $original_size < 30 * 1024) {
            return 'already_small';
        }

        if (in_array($mime, ['image/webp', 'image/avif'], true) || in_array($file_mime, ['image/webp', 'image/avif'], true)) {
            return 'already_webp';
        }

        if ($this->attachment_has_encoder_skip_log($attachment_id)) {
            return 'encoder_skipped';
        }

        if ($original_size >= 1024 * 1024 && ('image/png' === $mime || 'image/png' === $file_mime)) {
            return 'large_png';
        }

        if ($this->attachment_is_under_resize_threshold($attachment_id, $file)) {
            return 'resize_skipped';
        }

        return 'unknown';
    }

    private function attachment_has_encoder_skip_log(int $attachment_id): bool
    {
        foreach ($this->recent_logs(250) as $log) {
            if ((int) ($log['attachment_id'] ?? 0) !== $attachment_id) {
                continue;
            }

            $message = strtolower((string) ($log['message'] ?? ''));
            $status = (string) ($log['status'] ?? '');

            if (in_array($status, ['skipped', 'error', 'webp'], true) && preg_match('/(webp|avif|encoder|generated|not smaller|larger than original|worth keeping)/', $message)) {
                return true;
            }
        }

        return false;
    }

    private function attachment_is_under_resize_threshold(int $attachment_id, string $file): bool
    {
        if (!$file || !file_exists($file)) {
            return false;
        }

        $dimensions = wp_getimagesize($file);

        if (!is_array($dimensions) || empty($dimensions[0]) || empty($dimensions[1])) {
            return false;
        }

        $settings = $this->settings();
        $max_width = (int) ($settings['max_width'] ?? 0);
        $max_height = (int) ($settings['max_height'] ?? 0);

        if ($max_width > 0 && (int) $dimensions[0] > $max_width) {
            return false;
        }

        if ($max_height > 0 && (int) $dimensions[1] > $max_height) {
            return false;
        }

        return $this->is_optimizable_attachment($attachment_id);
    }

    private function zero_savings_summary(array $report): array
    {
        $summary = [
            'already_small' => 0,
            'already_webp' => 0,
            'encoder_skipped' => 0,
            'large_png' => 0,
            'resize_skipped' => 0,
            'unknown' => 0,
        ];

        foreach ($report as $item) {
            $reason = (string) ($item['reason'] ?? 'unknown');
            $summary[$reason] = isset($summary[$reason]) ? $summary[$reason] + 1 : 1;
        }

        return $summary;
    }

    private function savings_percent_summary(): array
    {
        $cached = get_transient(self::SAVINGS_PERCENT_TRANSIENT);

        if (is_array($cached)) {
            return $cached;
        }

        $summary = [
            'images' => 0,
            'total_original_bytes' => 0,
            'total_saved_bytes' => 0,
            'average_pct' => 0.0,
            'weighted_pct' => 0.0,
        ];
        $percent_total = 0.0;

        foreach ($this->processed_attachment_ids_for_savings_summary() as $attachment_id) {
            $sizes = $this->attachment_zero_savings_sizes((int) $attachment_id);
            $original_size = (int) ($sizes['original_size'] ?? 0);

            if ($original_size <= 0) {
                continue;
            }

            $current_size = (int) ($sizes['current_size'] ?? 0);
            $saved_bytes = max(0, $original_size - $current_size);
            $item_pct = ($saved_bytes / $original_size) * 100;

            $summary['images']++;
            $summary['total_original_bytes'] += $original_size;
            $summary['total_saved_bytes'] += $saved_bytes;
            $percent_total += $item_pct;
        }

        if ($summary['images'] > 0) {
            $summary['average_pct'] = round($percent_total / $summary['images'], 1);
        }

        if ($summary['total_original_bytes'] > 0) {
            $summary['weighted_pct'] = round(($summary['total_saved_bytes'] / $summary['total_original_bytes']) * 100, 1);
        }

        set_transient(self::SAVINGS_PERCENT_TRANSIENT, $summary, HOUR_IN_SECONDS);

        return $summary;
    }

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

        $sql = $wpdb->prepare(
            "SELECT DISTINCT p.ID
            FROM {$wpdb->posts} p
            INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
            WHERE p.post_type = %s
                AND p.post_mime_type IN (%s, %s, %s)
                AND pm.meta_key = %s
            ORDER BY p.ID DESC",
            'attachment',
            'image/jpeg',
            'image/png',
            'image/webp',
            self::META
        );

        return array_values(array_map('absint', (array) $wpdb->get_col($sql))); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Prepared above with fixed placeholders.
    }

    private function server_status(): array
    {
        $uploads = wp_upload_dir();
        $max_pixels = (int) apply_filters('shrinkwell_max_local_source_pixels', 20 * 1000 * 1000);

        return [
            'webp' => shrinkwell_server_supports_webp(),
            'webp_create' => $this->can_create_webp_locally(),
            'avif' => $this->server_can_use_avif(),
            'exec' => $this->can_use_exec(),
            'cwebp' => (bool) $this->locate_cwebp(),
            'cwebp_path' => (string) ($this->locate_cwebp() ?: ''),
            'memory_limit' => $this->format_memory_limit(function_exists('ini_get') ? (string) ini_get('memory_limit') : ''),
            'max_pixels' => $max_pixels,
            'uploads_writable' => !empty($uploads['basedir']) && $this->directory_is_writable((string) $uploads['basedir']),
        ];
    }

    private function filesystem()
    {
        global $wp_filesystem;

        if (!function_exists('WP_Filesystem')) {
            require_once ABSPATH . 'wp-admin/includes/file.php';
        }

        if (!WP_Filesystem()) {
            return null;
        }

        return $wp_filesystem;
    }

    private function directory_is_writable(string $path): bool
    {
        $filesystem = $this->filesystem();

        if (!$filesystem) {
            return false;
        }

        return $filesystem->is_dir($path) && $filesystem->is_writable($path);
    }

    private function write_file(string $path, string $contents): bool
    {
        $filesystem = $this->filesystem();

        if (!$filesystem) {
            return false;
        }

        return (bool) $filesystem->put_contents($path, $contents, FS_CHMOD_FILE);
    }

    private function format_memory_limit(string $limit): string
    {
        $limit = trim($limit);

        if ('' === $limit) {
            return '';
        }

        if ('-1' === $limit) {
            return __('Unlimited', 'shrinkwell-image-optimizer');
        }

        if (function_exists('wp_convert_hr_to_bytes')) {
            $bytes = (int) wp_convert_hr_to_bytes($limit);
            return $bytes > 0 ? size_format($bytes, 1) : $limit;
        }

        if (preg_match('/^\d+$/', $limit)) {
            return size_format((int) $limit, 1);
        }

        return $limit;
    }

    private function format_pixel_limit(int $pixels): string
    {
        if ($pixels >= 1000000) {
            return number_format_i18n($pixels / 1000000, 0) . ' MP';
        }

        return number_format_i18n($pixels) . ' px';
    }

    private function image_editor_supports_mime(string $mime): bool
    {
        return function_exists('wp_image_editor_supports') && wp_image_editor_supports(['mime_type' => $mime]);
    }

    private function can_create_webp_locally(): bool
    {
        return $this->image_editor_supports_mime('image/webp') || ($this->can_use_exec() && (bool) $this->locate_cwebp());
    }

    private function tab_url(string $tab): string
    {
        if ('summary' === $tab) {
            $tab = 'overview';
        } elseif (in_array($tab, ['optimize', 'formats'], true)) {
            $tab = 'settings';
        }

        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin navigation value; no state is changed.
        $page = sanitize_key((string) wp_unslash($_GET['page'] ?? 'shrinkwell-image-optimizer'));
        $base = 'shrinkwell-image-optimizer-settings' === $page ? 'options-general.php' : 'upload.php';

        return add_query_arg([
            'page' => $page,
            'tab' => $tab,
        ], admin_url($base));
    }

    private function account_url(): string
    {
        $url = admin_url('upload.php?page=shrinkwell-image-optimizer-account');

        if (function_exists('shrinkwell_fs')) {
            $freemius = shrinkwell_fs();
            if (is_object($freemius) && method_exists($freemius, 'get_account_url')) {
                $freemius_url = (string) $freemius->get_account_url();
                if ('' !== $freemius_url) {
                    $url = $freemius_url;
                }
            }
        }

        return $url;
    }

    private function status_card(string $title, bool $is_ok, string $ok_text, string $bad_text): string
    {
        $class = $is_ok ? 'shrinkwell-status-ok' : 'shrinkwell-status-warn';
        $label = $is_ok ? __('OK', 'shrinkwell-image-optimizer') : __('Needs attention', 'shrinkwell-image-optimizer');
        $body = $is_ok ? $ok_text : $bad_text;

        return '<div class="shrinkwell-status-card ' . esc_attr($class) . '"><strong>' . esc_html($title) . '</strong><span>' . esc_html($label) . '</span><p>' . esc_html($body) . '</p></div>';
    }

    private function help_tip(string $text): string
    {
        return '<span class="shrinkwell-help-tip" tabindex="0" role="img" aria-label="' . esc_attr($text) . '" data-help="' . esc_attr($text) . '">?</span>';
    }

    private function help_tip_allowed_html(): array
    {
        return [
            'span' => [
                'class' => true,
                'tabindex' => true,
                'role' => true,
                'aria-label' => true,
                'data-help' => true,
            ],
        ];
    }

    private function stat_icon(string $icon): string
    {
        $icons = [
            'image' => '<svg viewBox="0 0 32 32" aria-hidden="true" focusable="false"><rect x="6" y="7" width="20" height="18" rx="3"></rect><circle cx="20.5" cy="12.5" r="2.2"></circle><path d="M8.5 22l6.2-6.2 4.2 4.2 2.5-2.5 2.8 4.5"></path></svg>',
            'database' => '<svg viewBox="0 0 32 32" aria-hidden="true" focusable="false"><ellipse cx="16" cy="8.5" rx="9" ry="4.5"></ellipse><path d="M7 8.5v7c0 2.5 4 4.5 9 4.5s9-2 9-4.5v-7"></path><path d="M7 15.5v7c0 2.5 4 4.5 9 4.5s9-2 9-4.5v-7"></path></svg>',
            'clock' => '<svg viewBox="0 0 32 32" aria-hidden="true" focusable="false"><circle cx="16" cy="16" r="10"></circle><path d="M16 10.5V16l4 3"></path></svg>',
        ];

        $svg = $icons[$icon] ?? $icons['image'];

        return wp_kses($svg, [
            'svg' => [
                'viewbox' => true,
                'aria-hidden' => true,
                'focusable' => true,
            ],
            'rect' => [
                'x' => true,
                'y' => true,
                'width' => true,
                'height' => true,
                'rx' => true,
            ],
            'circle' => [
                'cx' => true,
                'cy' => true,
                'r' => true,
            ],
            'ellipse' => [
                'cx' => true,
                'cy' => true,
                'rx' => true,
                'ry' => true,
            ],
            'path' => [
                'd' => true,
            ],
        ]);
    }

    public function add_admin_menu(): void
    {
        add_media_page(
            __('Shrinkwell', 'shrinkwell-image-optimizer'),
            __('Shrinkwell', 'shrinkwell-image-optimizer'),
            'upload_files',
            'shrinkwell-image-optimizer',
            [$this, 'render_admin_page']
        );

        add_options_page(
            __('Shrinkwell Image Optimizer Settings', 'shrinkwell-image-optimizer'),
            __('Shrinkwell Image Optimizer', 'shrinkwell-image-optimizer'),
            'upload_files',
            'shrinkwell-image-optimizer-settings',
            [$this, 'render_admin_page']
        );
    }

    public function register_settings(): void
    {
        register_setting('shrinkwell_settings_group', self::OPTION, [
            'type' => 'array',
            'sanitize_callback' => [$this, 'sanitize_settings'],
            'default' => self::default_settings(),
        ]);
    }

    public function settings_capability(): string
    {
        return 'upload_files';
    }

    public function plugin_action_links(array $links): array
    {
        $settings_url = admin_url('options-general.php?page=shrinkwell-image-optimizer-settings');
        $settings_link = '<a href="' . esc_url($settings_url) . '">' . esc_html__('Settings', 'shrinkwell-image-optimizer') . '</a>';

        array_unshift($links, $settings_link);

        return $links;
    }

    public function shrinkwell_register_dashboard_widget(): void
    {
        if (!current_user_can('upload_files')) {
            return;
        }

        wp_add_dashboard_widget(
            'shrinkwell_dashboard_widget',
            __('Shrinkwell Image Optimizer', 'shrinkwell-image-optimizer'),
            [$this, 'render_dashboard_widget']
        );
    }

    public function render_dashboard_widget(): void
    {
        if (!current_user_can('upload_files')) {
            return;
        }

        $summary = $this->dashboard_summary();
        $status = sanitize_html_class((string) $summary['status']);
        ?>
        <div class="shrinkwell-dashboard-widget">
            <p class="shrinkwell-dashboard-health shrinkwell-dashboard-health-<?php echo esc_attr($status); ?>">
                <span aria-hidden="true"></span>
                <strong><?php echo esc_html((string) $summary['status_label']); ?></strong>
            </p>
            <div class="shrinkwell-dashboard-grid">
                <div>
                    <strong><?php echo esc_html(number_format_i18n((int) $summary['optimized_images'])); ?></strong>
                    <span><?php esc_html_e('optimized images', 'shrinkwell-image-optimizer'); ?></span>
                </div>
                <div>
                    <strong><?php echo esc_html(size_format((int) $summary['saved_bytes'], 1)); ?></strong>
                    <span><?php esc_html_e('saved', 'shrinkwell-image-optimizer'); ?></span>
                </div>
                <div>
                    <a href="<?php echo esc_url($this->tab_url('bulk')); ?>">
                        <strong><?php echo esc_html(number_format_i18n((int) $summary['pending'])); ?></strong>
                        <span><?php esc_html_e('pending optimization', 'shrinkwell-image-optimizer'); ?></span>
                    </a>
                </div>
                <div>
                    <a href="<?php echo esc_url($this->tab_url('seo')); ?>">
                        <strong><?php echo esc_html(number_format_i18n((int) $summary['missing_alt'])); ?></strong>
                        <span><?php esc_html_e('missing alt text', 'shrinkwell-image-optimizer'); ?></span>
                    </a>
                </div>
            </div>
            <?php if ((int) $summary['too_large'] > 0) : ?>
                <p class="description">
                    <?php
                    echo esc_html(
                        sprintf(
                            /* translators: %s: Number of images skipped because they exceed local processing limits. */
                            __('%s images exceed this site\'s safe local processing limits.', 'shrinkwell-image-optimizer'),
                            number_format_i18n((int) $summary['too_large'])
                        )
                    );
                    ?>
                </p>
            <?php endif; ?>
        </div>
        <?php
    }

    public function sanitize_settings($input): array
    {
        $input = (array) $input;
        $profile = sanitize_key((string) ($input['compression_profile'] ?? 'balanced'));

        if (!array_key_exists($profile, self::compression_profiles())) {
            $profile = 'balanced';
        }

        if (!shrinkwell_feature_available('avif') || (!empty($input['create_avif']) && !$this->server_can_use_avif())) {
            $input['create_avif'] = 0;
            $input['serve_avif'] = 0;
        }

        if (!empty($input['create_webp']) && !$this->can_create_webp_locally()) {
            $input['create_webp'] = 0;
            $input['serve_webp'] = 0;
        }

        $quality = isset($input['custom_strength'])
            ? self::custom_quality_for_strength(absint($input['custom_strength']))
            : min(95, max(40, absint($input['quality'] ?? 82)));

        return [
            'auto_optimize' => (!empty($input['auto_optimize']) && shrinkwell_can_use_auto_optimize()) ? 1 : 0,
            'compression_profile' => $profile,
            'quality' => $quality,
            'max_width' => max(0, absint($input['max_width'] ?? 0)),
            'max_height' => max(0, absint($input['max_height'] ?? 0)),
            'max_file_size_mb' => min(100, max(1, absint($input['max_file_size_mb'] ?? 10))),
            'optimize_thumbnails' => empty($input['optimize_thumbnails']) ? 0 : 1,
            'strip_exif' => empty($input['strip_exif']) ? 0 : 1,
            'create_webp' => empty($input['create_webp']) ? 0 : 1,
            'serve_webp' => empty($input['serve_webp']) ? 0 : 1,
            'create_avif' => (!empty($input['create_avif']) && shrinkwell_feature_available('avif')) ? 1 : 0,
            'serve_avif' => (!empty($input['serve_avif']) && shrinkwell_feature_available('avif')) ? 1 : 0,
            'backup_originals' => empty($input['backup_originals']) ? 0 : 1,
        ];
    }

    public function enqueue_admin_assets(string $hook): void
    {
        $allowed_hooks = [
            'index.php',
            'media_page_shrinkwell-image-optimizer',
            'post.php',
            'settings_page_shrinkwell-image-optimizer-settings',
            'upload.php',
        ];

        if (!in_array($hook, $allowed_hooks, true)) {
            return;
        }

        wp_enqueue_style(
            'dashicons'
        );

        $admin_css_path = SHRINKWELL_PLUGIN_DIR . 'assets/admin.css';
        $admin_js_path = SHRINKWELL_PLUGIN_DIR . 'assets/admin.js';
        $admin_css_version = self::VERSION . '-' . (file_exists($admin_css_path) ? (string) filemtime($admin_css_path) : '0');
        $admin_js_version = self::VERSION . '-' . (file_exists($admin_js_path) ? (string) filemtime($admin_js_path) : '0');

        wp_enqueue_style(
            'shrinkwell-admin',
            SHRINKWELL_PLUGIN_URL . 'assets/admin.css',
            [],
            $admin_css_version
        );

        wp_enqueue_script(
            'shrinkwell-admin',
            SHRINKWELL_PLUGIN_URL . 'assets/admin.js',
            ['jquery'],
            $admin_js_version,
            true
        );

        $freemius_license_action = '';
        $freemius_license_security = '';
        $freemius_module_id = '';
        if (function_exists('shrinkwell_fs')) {
            $freemius = shrinkwell_fs();
            if (is_object($freemius)) {
                $freemius_license_action = method_exists($freemius, 'get_ajax_action') ? (string) $freemius->get_ajax_action('activate_license') : '';
                $freemius_license_security = method_exists($freemius, 'get_ajax_security') ? (string) $freemius->get_ajax_security('activate_license') : '';
                $freemius_module_id = method_exists($freemius, 'get_id') ? (string) $freemius->get_id() : '';
            }
        }

        wp_localize_script('shrinkwell-admin', 'shrinkwellAdmin', [
            'ajaxUrl' => admin_url('admin-ajax.php'),
            'nonce' => wp_create_nonce('shrinkwell_bulk_optimize'),
            'zeroSavingsNonce' => wp_create_nonce('shrinkwell_zero_savings_nonce'),
            'forceReoptimizeNonce' => wp_create_nonce('shrinkwell_force_reoptimize_nonce'),
            'licenseNonce' => wp_create_nonce('shrinkwell_activate_license_nonce'),
            'freemiusLicenseAction' => $freemius_license_action,
            'freemiusLicenseSecurity' => $freemius_license_security,
            'freemiusModuleId' => $freemius_module_id,
            'doneText' => __('Optimization complete.', 'shrinkwell-image-optimizer'),
            'emptyText' => __('No unoptimized images found.', 'shrinkwell-image-optimizer'),
            'errorText' => __('Something went wrong. Please try again.', 'shrinkwell-image-optimizer'),
            'testingCompressionText' => __('Testing compression...', 'shrinkwell-image-optimizer'),
            'compressionTestDoneText' => __('Compression test complete.', 'shrinkwell-image-optimizer'),
            'compressionTestNoSavingsText' => __('Compression test complete. Shrinkwell would keep this image as-is because the tested output was not smaller.', 'shrinkwell-image-optimizer'),
            /* translators: 1: Human-readable saved size. 2: Number of tested files. */
            'compressionTestSavingsText' => __('Would save about %1$s across %2$s files. No files were replaced.', 'shrinkwell-image-optimizer'),
            'optimizingImageText' => __('Optimizing image...', 'shrinkwell-image-optimizer'),
            'imageOptimizedInlineText' => __('Image optimized. Refresh the Media Library to see the updated status.', 'shrinkwell-image-optimizer'),
            'imageActionFailedText' => __('Image action failed.', 'shrinkwell-image-optimizer'),
            'serverStoppedText' => __('The server stopped this image request. Shrinkwell skipped it and continued. This is often caused by a very large image or low PHP memory.', 'shrinkwell-image-optimizer'),
            'serverStoppedShortText' => __('Server stopped the request, usually because the image is too heavy for the current PHP memory.', 'shrinkwell-image-optimizer'),
            'serverStoppedWithDetailsText' => __('Server stopped the request. Image details:', 'shrinkwell-image-optimizer'),
            'configuredLimitText' => __('Configured file-size limit:', 'shrinkwell-image-optimizer'),
            'largeImageShortText' => __('Image is too large for safe local processing on this hosting setup.', 'shrinkwell-image-optimizer'),
            'skippedSummaryText' => __('images skipped. Shrinkwell continued with the rest.', 'shrinkwell-image-optimizer'),
            'moreSkippedText' => __('more skipped images are in the recent log below.', 'shrinkwell-image-optimizer'),
            'skippedImageText' => __('Skipped:', 'shrinkwell-image-optimizer'),
            'leaveWarningText' => __('Media optimization is still running. Keep this page open until Shrinkwell is finished.', 'shrinkwell-image-optimizer'),
            'readyText' => __('Ready', 'shrinkwell-image-optimizer'),
            'runningText' => __('Running', 'shrinkwell-image-optimizer'),
            'idleStepText' => __('Test 5 images first', 'shrinkwell-image-optimizer'),
            'testingStepText' => __('Testing 5 images', 'shrinkwell-image-optimizer'),
            'resultStepText' => __('Review test result', 'shrinkwell-image-optimizer'),
            'runningStepText' => __('Optimizing media library', 'shrinkwell-image-optimizer'),
            'pausedStepText' => __('Run paused', 'shrinkwell-image-optimizer'),
            'proofStepText' => __('Check Page Proof', 'shrinkwell-image-optimizer'),
            'testCompleteTitleText' => __('Test passed', 'shrinkwell-image-optimizer'),
            'testCompleteDetailText' => __('The 5-image test is complete. Check WebP/AVIF delivery with Page Proof, or optimize the full media library next.', 'shrinkwell-image-optimizer'),
            'testReviewTitleText' => __('Needs attention', 'shrinkwell-image-optimizer'),
            'testReviewDetailText' => __('Shrinkwell skipped or stopped something during the test. Review the short log and adjust settings if needed.', 'shrinkwell-image-optimizer'),
            'completionTitleText' => __('Optimization complete', 'shrinkwell-image-optimizer'),
            /* translators: 1: Number of processed images. 2: Saved size. 3: Number of skipped images. */
            'completionSummaryText' => __('%1$s images processed. %2$s saved. %3$s safely skipped.', 'shrinkwell-image-optimizer'),
            'currentFilePrefixText' => __('Current file:', 'shrinkwell-image-optimizer'),
            'settingsButtonText' => __('Adjust settings', 'shrinkwell-image-optimizer'),
            'optimizeAllButtonText' => __('Optimize all', 'shrinkwell-image-optimizer'),
            'skipAndContinueText' => __('Skip and continue anyway', 'shrinkwell-image-optimizer'),
            'monthlyLimitReachedTitleText' => __('This image would exceed the free limit', 'shrinkwell-image-optimizer'),
            'monthlyLimitReachedDetailText' => __('The free plan counts source media processed this month, not storage saved. Free includes WebP and AVIF conversion within the monthly cap.', 'shrinkwell-image-optimizer'),
            'monthlyLimitReachedUpgradeText' => __('Upgrade to Pro to keep optimizing without limits and unlock safe Media Cleanup.', 'shrinkwell-image-optimizer'),
            'maybeLaterText' => __('Maybe later', 'shrinkwell-image-optimizer'),
            'upgradeToProText' => __('Keep optimizing with Pro', 'shrinkwell-image-optimizer'),
            'licenseActivatingText' => __('Activating license code...', 'shrinkwell-image-optimizer'),
            'licenseActivatedText' => __('License activated. Refreshing Shrinkwell...', 'shrinkwell-image-optimizer'),
            'licenseEmptyText' => __('Enter a license key first.', 'shrinkwell-image-optimizer'),
            'licenseMaskedText' => __('This looks like a hidden or shortened license key. Copy the full key from the Freemius Licenses page or purchase email.', 'shrinkwell-image-optimizer'),
            'licenseFormatText' => __('Enter the full Freemius license key that starts with sk_.', 'shrinkwell-image-optimizer'),
            'licenseActivationFailedText' => __('Freemius could not activate this license. Check that the key is active, not blocked, and available for this site.', 'shrinkwell-image-optimizer'),
            'licenseIdUnavailableText' => __('This is a license ID, not a license key. Open Freemius Licenses and copy the full key that starts with sk_.', 'shrinkwell-image-optimizer'),
            'upgradeUrl' => shrinkwell_get_upgrade_url(),
            'settingsUrl' => $this->tab_url('settings'),
            'scanText' => __('Scanning media library...', 'shrinkwell-image-optimizer'),
            'pauseText' => __('Pause', 'shrinkwell-image-optimizer'),
            'resumeText' => __('Resume', 'shrinkwell-image-optimizer'),
            'webpOnlyRunningText' => __('Rebuilding missing WebP delivery files...', 'shrinkwell-image-optimizer'),
            'webpOnlyDoneText' => __('WebP delivery files checked.', 'shrinkwell-image-optimizer'),
            'webpOnlyNoFilesText' => __('WebP delivery files checked. No missing WebP files were found.', 'shrinkwell-image-optimizer'),
            /* translators: 1: Number of processed images. 2: Number of skipped images. */
            'webpOnlySummaryText' => __('WebP delivery files checked for %1$s images. %2$s skipped.', 'shrinkwell-image-optimizer'),
            'savedText' => __('saved', 'shrinkwell-image-optimizer'),
            'errorsText' => __('errors', 'shrinkwell-image-optimizer'),
            'skippedText' => __('skipped', 'shrinkwell-image-optimizer'),
            /* translators: %s: Selected compression profile label. */
            'selectedProfileText' => __('Selected: %s', 'shrinkwell-image-optimizer'),
            'testText' => __('Running delivery test...', 'shrinkwell-image-optimizer'),
            'largeFilesBlockedText' => __('The remaining images are above the safe local processing limits. Resize them first or raise the limits in Advanced settings.', 'shrinkwell-image-optimizer'),
            'testDoneText' => __('Delivery test complete.', 'shrinkwell-image-optimizer'),
            'testPassedText' => __('Frontend delivery is ready.', 'shrinkwell-image-optimizer'),
            'testNeedsWorkText' => __('Frontend delivery still needs attention.', 'shrinkwell-image-optimizer'),
            'deliveryReadinessTitleText' => __('Delivery readiness check', 'shrinkwell-image-optimizer'),
            'deliveryReadinessCheckingText' => __('Checking delivery readiness...', 'shrinkwell-image-optimizer'),
            'siteUrl' => home_url('/'),
            'pluginVersion' => self::VERSION,
            'proofScanText' => __('Checking this page for real frontend WebP delivery...', 'shrinkwell-image-optimizer'),
            'proofScanDoneText' => __('Page proof scan complete.', 'shrinkwell-image-optimizer'),
            'proofScanPassedText' => __('WebP delivery looks complete for this page sample.', 'shrinkwell-image-optimizer'),
            'proofScanPartialText' => __('WebP is detected on this page, but some local JPG/PNG URLs are still served directly.', 'shrinkwell-image-optimizer'),
            'proofScanNeedsWorkText' => __('No WebP delivery evidence was found on this page yet.', 'shrinkwell-image-optimizer'),
            'proofScanFilesReadyText' => __('WebP files are ready, but this page does not show full WebP delivery yet. This often happens with cached pages, theme assets, or CSS background images.', 'shrinkwell-image-optimizer'),
            'proofScanCacheHintText' => __('This is not a WebP file generation problem. WebP files are ready, but the live page HTML still prints JPG/PNG URLs. Clear page cache/CDN cache first; if the same files remain, review the page builder image fields.', 'shrinkwell-image-optimizer'),
            'proofScanNoSavingsText' => __('WebP files were found, but they are not smaller for this page sample.', 'shrinkwell-image-optimizer'),
            'proofScanUncoveredText' => __('local image URLs have WebP files ready, but this page still exposes the original JPG/PNG URLs.', 'shrinkwell-image-optimizer'),
            'proofScanLargerWebpText' => __('WebP copies were found but are larger than the original, so Shrinkwell will not count or serve them.', 'shrinkwell-image-optimizer'),
            'proofScanIgnoredIconsText' => __('site icon/meta image URLs were ignored because they do not affect visible page image delivery.', 'shrinkwell-image-optimizer'),
            'proofRewritePreviewText' => __('Shrinkwell can add WebP picture markup to this HTML when it reaches WordPress output. If the live page still shows JPG/PNG, clear or bypass page cache/CDN cache.', 'shrinkwell-image-optimizer'),
            'proofRewritePreviewTitleText' => __('Rewrite preview', 'shrinkwell-image-optimizer'),
            'proofCacheHeadersTitleText' => __('Cache headers seen by Page Proof', 'shrinkwell-image-optimizer'),
            'proofDiagnosisTitleText' => __('Technical reason', 'shrinkwell-image-optimizer'),
            'proofNextStepsTitleText' => __('What to do next', 'shrinkwell-image-optimizer'),
            'proofNextStepPassedText' => __('WebP delivery is visible for this page sample. Re-test after major cache, theme, or layout changes.', 'shrinkwell-image-optimizer'),
            'proofNextStepRewriteText' => __('Shrinkwell can rewrite this page sample, but the scanned HTML does not show those changes yet. Clear page cache/CDN cache or exclude the page from static HTML cache, then scan again.', 'shrinkwell-image-optimizer'),
            'proofNextStepCacheText' => __('Clear page cache and CDN cache, then run Page Proof again. Cached HTML can hide fresh Shrinkwell delivery changes.', 'shrinkwell-image-optimizer'),
            'proofNextStepBackgroundText' => __('Review background images and theme/plugin icons manually. Page builders and CSS files may output these outside normal WordPress image markup.', 'shrinkwell-image-optimizer'),
            'proofNextStepManualText' => __('If the same uncovered images remain after cache is cleared, edit those images through the WordPress media selector or page builder image control.', 'shrinkwell-image-optimizer'),
            'proofPlainGoodTitleText' => __('Looks good: WebP is visible on this page.', 'shrinkwell-image-optimizer'),
            'proofPlainGoodDetailText' => __('Shrinkwell can see WebP delivery in the page HTML. Re-test after changing cache, theme, or page builder settings.', 'shrinkwell-image-optimizer'),
            'proofPlainReadyTitleText' => __('WebP files are ready, but this page still shows old image URLs.', 'shrinkwell-image-optimizer'),
            'proofPlainReadyDetailText' => __('This usually means cache or the page builder is showing saved HTML. The images are not broken; the page just needs to refresh its output.', 'shrinkwell-image-optimizer'),
            'proofPlainMissingTitleText' => __('WebP files are not ready yet.', 'shrinkwell-image-optimizer'),
            'proofPlainMissingDetailText' => __('Run Media Optimization or Rebuild missing WebP delivery files first, then scan this page again.', 'shrinkwell-image-optimizer'),
            'proofPlainBackgroundTitleText' => __('Some images are printed by CSS or the page builder.', 'shrinkwell-image-optimizer'),
            'proofPlainBackgroundDetailText' => __('These may need a page builder cache clear or manual review because they are not normal WordPress image tags.', 'shrinkwell-image-optimizer'),
            'proofPlainUnknownTitleText' => __('Page Proof needs a manual check.', 'shrinkwell-image-optimizer'),
            'proofPlainUnknownDetailText' => __('Follow the checklist below first. If the same images remain, review those images in the page builder or media selector.', 'shrinkwell-image-optimizer'),
            'proofDoThisFirstText' => __('Do this first', 'shrinkwell-image-optimizer'),
            'proofThenText' => __('Then run Page Proof again.', 'shrinkwell-image-optimizer'),
            'proofImagesToReviewText' => __('Images still showing as JPG/PNG', 'shrinkwell-image-optimizer'),
            'proofTechnicalDetailsText' => __('Technical details', 'shrinkwell-image-optimizer'),
            'proofReportTitleText' => __('Shrinkwell Page Proof Report', 'shrinkwell-image-optimizer'),
            'proofReportSiteText' => __('Site:', 'shrinkwell-image-optimizer'),
            'proofReportPageScannedText' => __('Page scanned:', 'shrinkwell-image-optimizer'),
            'proofReportDateText' => __('Date:', 'shrinkwell-image-optimizer'),
            'proofReportPluginVersionText' => __('Plugin version:', 'shrinkwell-image-optimizer'),
            'proofReportSimpleResultText' => __('Simple result:', 'shrinkwell-image-optimizer'),
            'proofBackgroundImagesToReviewText' => __('Background images to review:', 'shrinkwell-image-optimizer'),
            'proofWebpEvidenceText' => __('WebP evidence:', 'shrinkwell-image-optimizer'),
            'proofEvidenceNotDetectedText' => __('Not detected', 'shrinkwell-image-optimizer'),
            'proofEvidenceFoundText' => __('Found', 'shrinkwell-image-optimizer'),
            'proofEvidencePartialText' => __('Partial', 'shrinkwell-image-optimizer'),
            'proofWebpDeliveryCoverageText' => __('WebP delivery coverage:', 'shrinkwell-image-optimizer'),
            'proofWebpFilesReadyText' => __('WebP files ready:', 'shrinkwell-image-optimizer'),
            'proofYesText' => __('Yes', 'shrinkwell-image-optimizer'),
            'proofNoText' => __('No', 'shrinkwell-image-optimizer'),
            /* translators: 1: Number of image URLs with usable WebP files. 2: Number of checked image URLs. */
            'proofUsableWebpUrlsText' => __('JPG/PNG URLs with usable WebP files: %1$s of %2$s', 'shrinkwell-image-optimizer'),
            'proofEstimatedSavedPayloadText' => __('Estimated saved image payload:', 'shrinkwell-image-optimizer'),
            'proofOriginalPayloadCheckedText' => __('Original JPG/PNG payload checked:', 'shrinkwell-image-optimizer'),
            'proofWebpPayloadAvailableText' => __('WebP payload available:', 'shrinkwell-image-optimizer'),
            'proofExistingWebpFilesFoundText' => __('Existing WebP files found:', 'shrinkwell-image-optimizer'),
            'proofWebpFilesLargerThanOriginalText' => __('WebP files larger than original:', 'shrinkwell-image-optimizer'),
            'proofIgnoredSiteIconUrlsText' => __('Ignored site icon/meta image URLs:', 'shrinkwell-image-optimizer'),
            'proofRewritePreviewCanAddText' => __('Shrinkwell can add WebP markup before cache/page builder output.', 'shrinkwell-image-optimizer'),
            'proofCoverageAfterPreviewText' => __('Coverage after preview:', 'shrinkwell-image-optimizer'),
            'proofUncoveredAfterPreviewText' => __('Uncovered after preview:', 'shrinkwell-image-optimizer'),
            'proofCacheHeadersSawText' => __('Cache headers Page Proof saw:', 'shrinkwell-image-optimizer'),
            'proofChecksText' => __('Checks:', 'shrinkwell-image-optimizer'),
            'proofOkText' => __('OK', 'shrinkwell-image-optimizer'),
            'proofExistingLargerWebpText' => __('Existing WebP files larger than original:', 'shrinkwell-image-optimizer'),
            /* translators: 1: Original image size. 2: WebP image size. */
            'proofOriginalWebpSizeText' => __('%1$s original, %2$s WebP', 'shrinkwell-image-optimizer'),
            'proofGeneratedByText' => __('Generated by Shrinkwell Image Optimizer', 'shrinkwell-image-optimizer'),
            'proofEstimatedReductionText' => __('estimated image payload reduction on this page', 'shrinkwell-image-optimizer'),
            /* translators: 1: Number of image URLs with usable WebP files. 2: Number of checked image URLs. */
            'proofUsableWebpSentenceText' => __('%1$s of %2$s JPG/PNG image URLs have usable WebP files.', 'shrinkwell-image-optimizer'),
            /* translators: 1: Original image payload. 2: WebP image payload. */
            'proofOriginalVsWebpText' => __('%1$s original vs %2$s WebP', 'shrinkwell-image-optimizer'),
            /* translators: %s: Delivery coverage percentage. */
            'proofDeliveryCoverageSentenceText' => __('%s%% delivery coverage', 'shrinkwell-image-optimizer'),
            /* translators: %s: Saved image payload percentage. */
            'proofSmallerSentenceText' => __('%s%% smaller', 'shrinkwell-image-optimizer'),
            /* translators: %s: Number of picture elements. */
            'proofPictureElementsAfterText' => __('%s picture elements after preview rewrite', 'shrinkwell-image-optimizer'),
            /* translators: %s: Number of WebP source tags. */
            'proofSourceTagsAfterText' => __('%s WebP source tags after preview rewrite', 'shrinkwell-image-optimizer'),
            /* translators: %s: Delivery coverage percentage. */
            'proofCoverageAfterRewriteText' => __('%s%% delivery coverage after preview rewrite', 'shrinkwell-image-optimizer'),
            /* translators: %s: Number of background images. */
            'proofBackgroundExamplesText' => __('%s background image examples', 'shrinkwell-image-optimizer'),
            'proofScanNoteText' => __('Scan note', 'shrinkwell-image-optimizer'),
            'copyProofReportText' => __('Copy proof report', 'shrinkwell-image-optimizer'),
            'proofReportCopiedText' => __('Proof report copied.', 'shrinkwell-image-optimizer'),
            'downloadProofReportText' => __('Download .txt', 'shrinkwell-image-optimizer'),
            'proofReportDownloadedText' => __('Proof report downloaded.', 'shrinkwell-image-optimizer'),
            'restoreAllConfirmText' => __('Restore all backups? This replaces optimized images with originals and removes generated WebP/AVIF delivery files. You can rebuild WebP delivery files afterwards.', 'shrinkwell-image-optimizer'),
            'cleanupBackupsNonce' => wp_create_nonce('shrinkwell_cleanup_backups_nonce'),
            'cleanupBackupsScanText' => __('Checking which backups can be safely deleted...', 'shrinkwell-image-optimizer'),
            /* translators: 1: Number of safe backup files. 2: Human-readable storage amount. */
            'cleanupBackupsReadyText' => __('%1$s backups can be safely removed · ~%2$s', 'shrinkwell-image-optimizer'),
            /* translators: 1: Human-readable storage amount. 2: Number of backup files. */
            'cleanupBackupsConfirmText' => __('Shrinkwell found %2$s old backup files that are safe to delete. This will free up %1$s. These originals cannot be restored afterwards. Continue?', 'shrinkwell-image-optimizer'),
            'cleanupBackupsRunningText' => __('Deleting safe old backups...', 'shrinkwell-image-optimizer'),
            /* translators: 1: Number of deleted backup files. 2: Human-readable storage amount. */
            'cleanupBackupsDoneText' => __('Deleted %1$s old backup files and freed %2$s.', 'shrinkwell-image-optimizer'),
            'cleanupBackupsEmptyText' => __('No old backups are safe to delete right now.', 'shrinkwell-image-optimizer'),
            'cleanupBackupsCanceledText' => __('Backup cleanup canceled.', 'shrinkwell-image-optimizer'),
            'mediaCleanupNonce' => wp_create_nonce('shrinkwell_media_cleanup_nonce'),
            'canBulkCleanup' => shrinkwell_can_bulk_cleanup(),
            /* translators: %s: Human-readable storage amount that could potentially be freed. */
            'mediaCleanupUpgradeButtonText' => __('Clean up %s with Pro', 'shrinkwell-image-optimizer'),
            'mediaCleanupUpgradeFallbackText' => __('Clean up with Pro', 'shrinkwell-image-optimizer'),
            'mediaCleanupScanningText' => __('Scanning posts, metadata, settings and theme references...', 'shrinkwell-image-optimizer'),
            'mediaCleanupDoneText' => __('Media scan complete.', 'shrinkwell-image-optimizer'),
            /* translators: %s: Number of selected images. */
            'mediaCleanupMoveConfirmText' => __('Move %1$s selected images to Shrinkwell Trash? They will be removed from the Media Library but kept on the server for 30 days.', 'shrinkwell-image-optimizer'),
            'mediaCleanupMoveTitleText' => __('Move to Shrinkwell Trash?', 'shrinkwell-image-optimizer'),
            'mediaCleanupMoveButtonText' => __('Move to Shrinkwell Trash', 'shrinkwell-image-optimizer'),
            'mediaCleanupMovingText' => __('Moving selected images to Shrinkwell Trash...', 'shrinkwell-image-optimizer'),
            /* translators: %s: Number of images moved to Shrinkwell Trash. */
            'mediaCleanupMovedText' => __('Moved %s images to Shrinkwell Trash.', 'shrinkwell-image-optimizer'),
            'mediaCleanupRestoreConfirmText' => __('Restore all images from Shrinkwell Trash to the Media Library?', 'shrinkwell-image-optimizer'),
            /* translators: %s: Number of restored images. */
            'mediaCleanupRestoredText' => __('Restored %s images.', 'shrinkwell-image-optimizer'),
            'mediaCleanupRestoringText' => __('Restoring images...', 'shrinkwell-image-optimizer'),
            /* translators: 1: Number of files. 2: Human-readable storage amount. */
            'mediaCleanupDeleteConfirmText' => __('This permanently removes %1$s files (%2$s) from your server. They cannot be restored by Shrinkwell afterwards.', 'shrinkwell-image-optimizer'),
            'mediaCleanupDeleteTitleText' => __('Delete permanently?', 'shrinkwell-image-optimizer'),
            'mediaCleanupDeleteButtonText' => __('Delete permanently', 'shrinkwell-image-optimizer'),
            /* translators: 1: Number of deleted files. 2: Human-readable storage amount. */
            'mediaCleanupDeletedText' => __('Permanently deleted %1$s files and freed %2$s.', 'shrinkwell-image-optimizer'),
            'mediaCleanupDeletingText' => __('Deleting permanently...', 'shrinkwell-image-optimizer'),
            'mediaCleanupFoundInText' => __('Found in:', 'shrinkwell-image-optimizer'),
            'mediaCleanupLocationsText' => __('locations', 'shrinkwell-image-optimizer'),
            'mediaCleanupLastAttachedText' => __('Last attached to:', 'shrinkwell-image-optimizer'),
            'mediaCleanupWhyStatusText' => __('Why this status?', 'shrinkwell-image-optimizer'),
            'mediaCleanupEvidenceFallbackText' => __('This result needs a new scan before detailed evidence is available.', 'shrinkwell-image-optimizer'),
            'mediaCleanupNotSelectableText' => __('This item is not selectable for cleanup from the scan results.', 'shrinkwell-image-optimizer'),
            'mediaCleanupEligibleEvidenceText' => __('High-confidence possibly unused result. Review it before moving it to Shrinkwell Trash.', 'shrinkwell-image-optimizer'),
            'mediaCleanupDatabaseSourceText' => __('Database', 'shrinkwell-image-optimizer'),
            'mediaCleanupNoImagesText' => __('No image attachments found.', 'shrinkwell-image-optimizer'),
            'scanRecommendationsText' => __('Scanning your media library...', 'shrinkwell-image-optimizer'),
            'recommendationsDoneText' => __('Recommended settings for this site', 'shrinkwell-image-optimizer'),
            'applyRecommendationsText' => __('Apply recommended settings', 'shrinkwell-image-optimizer'),
            'applyingRecommendationsText' => __('Applying recommended settings...', 'shrinkwell-image-optimizer'),
            'recommendationsAppliedText' => __('Recommended settings applied.', 'shrinkwell-image-optimizer'),
            'bulkNextText' => __('Go to media optimization', 'shrinkwell-image-optimizer'),
            'reviewSettingsText' => __('Review current settings', 'shrinkwell-image-optimizer'),
            'runBulkText' => __('Run media optimization', 'shrinkwell-image-optimizer'),
            'qualityStrengthText' => __('highest quality', 'shrinkwell-image-optimizer'),
            'balancedStrengthText' => __('balanced', 'shrinkwell-image-optimizer'),
            'smallFilesStrengthText' => __('smallest files', 'shrinkwell-image-optimizer'),
            'customStrongRiskText' => __('Images can start to show visible quality loss. Use this only when speed is more important than detail.', 'shrinkwell-image-optimizer'),
            'customMediumText' => __('Good middle ground: visible quality usually stays natural while files become smaller.', 'shrinkwell-image-optimizer'),
            'customQualityText' => __('Best for portfolios and product images: keeps images sharp, but saves less space.', 'shrinkwell-image-optimizer'),
            'currentImageText' => __('Optimizing:', 'shrinkwell-image-optimizer'),
            'reloadText' => __('Finished. Refreshing the page with updated numbers...', 'shrinkwell-image-optimizer'),
            'seoScanText' => __('Scanning image SEO...', 'shrinkwell-image-optimizer'),
            'seoDoneText' => __('SEO scan complete.', 'shrinkwell-image-optimizer'),
            'seoNoIssuesText' => __('No obvious image SEO issues found in the scanned images.', 'shrinkwell-image-optimizer'),
            'scannedText' => __('scanned', 'shrinkwell-image-optimizer'),
            'missingAltText' => __('missing alt text', 'shrinkwell-image-optimizer'),
            'weakFilenamesText' => __('weak filenames', 'shrinkwell-image-optimizer'),
            'seoAllText' => __('All', 'shrinkwell-image-optimizer'),
            'seoMissingAltText' => __('Missing alt', 'shrinkwell-image-optimizer'),
            'seoWeakFilenameText' => __('Weak filename', 'shrinkwell-image-optimizer'),
            'seoAlreadyFilledText' => __('Already filled', 'shrinkwell-image-optimizer'),
            'seoFilterImagesText' => __('Filter images', 'shrinkwell-image-optimizer'),
            'imageText' => __('Image', 'shrinkwell-image-optimizer'),
            'seoStatusText' => __('SEO status', 'shrinkwell-image-optimizer'),
            'currentAltText' => __('Current alt text', 'shrinkwell-image-optimizer'),
            'currentTitleText' => __('Current title', 'shrinkwell-image-optimizer'),
            'suggestionText' => __('Suggestion', 'shrinkwell-image-optimizer'),
            'needsAttentionText' => __('Needs attention', 'shrinkwell-image-optimizer'),
            'missingImportantText' => __('Missing important SEO data', 'shrinkwell-image-optimizer'),
            'filenameText' => __('Filename', 'shrinkwell-image-optimizer'),
            'issuesText' => __('Issues', 'shrinkwell-image-optimizer'),
            'actionText' => __('Action', 'shrinkwell-image-optimizer'),
            'reviewText' => __('Review', 'shrinkwell-image-optimizer'),
            'saveText' => __('Save', 'shrinkwell-image-optimizer'),
            'savingText' => __('Saving...', 'shrinkwell-image-optimizer'),
            'savedInlineText' => __('Saved.', 'shrinkwell-image-optimizer'),
            'saveFailedText' => __('Could not save this image SEO data.', 'shrinkwell-image-optimizer'),
            'altLabelText' => __('Alt text', 'shrinkwell-image-optimizer'),
            'titleLabelText' => __('Title', 'shrinkwell-image-optimizer'),
            'useSuggestionText' => __('Use suggestion', 'shrinkwell-image-optimizer'),
            'writeAltManuallyText' => __('Write a real description of what is visible in this image.', 'shrinkwell-image-optimizer'),
            'noSafeSuggestionText' => __('No safe suggestion yet', 'shrinkwell-image-optimizer'),
            'emptyAltWarningText' => __('Add a short description before saving.', 'shrinkwell-image-optimizer'),
            'bulkSaveSeoText' => __('Use all suggestions and save', 'shrinkwell-image-optimizer'),
            'bulkSavingSeoText' => __('Saving suggestions...', 'shrinkwell-image-optimizer'),
            'bulkSeoSavedText' => __('SEO changes saved.', 'shrinkwell-image-optimizer'),
            'readyToSaveText' => __('Ready to save.', 'shrinkwell-image-optimizer'),
            /* translators: %s: Number of optimized images to analyse. */
            'zeroSavingsScanText' => __('Analysing %s images...', 'shrinkwell-image-optimizer'),
            'zeroSavingsDoneText' => __('Zero savings analysis complete.', 'shrinkwell-image-optimizer'),
            /* translators: 1: Number of zero-savings images. 2: Reason summary. */
            'zeroSavingsSummaryText' => __('Found %1$s images to review: %2$s', 'shrinkwell-image-optimizer'),
            'zeroSavingsEmptyText' => __('No zero-savings images found in the current report.', 'shrinkwell-image-optimizer'),
            'zeroSavingsAllText' => __('All', 'shrinkwell-image-optimizer'),
            'zeroSavingsSkippedText' => __('Skipped', 'shrinkwell-image-optimizer'),
            'zeroSavingsUnknownText' => __('Unknown', 'shrinkwell-image-optimizer'),
            'zeroSavingsAlreadySmallText' => __('Already small', 'shrinkwell-image-optimizer'),
            'zeroSavingsAlreadyWebpText' => __('Already WebP/AVIF', 'shrinkwell-image-optimizer'),
            'zeroSavingsEncoderSkippedText' => __('Encoder skipped', 'shrinkwell-image-optimizer'),
            'zeroSavingsLargePngText' => __('Large PNG can save more', 'shrinkwell-image-optimizer'),
            'zeroSavingsResizeSkippedText' => __('No resize needed', 'shrinkwell-image-optimizer'),
            'zeroSavingsThumbnailText' => __('Thumbnail', 'shrinkwell-image-optimizer'),
            'zeroSavingsOriginalSizeText' => __('Original size', 'shrinkwell-image-optimizer'),
            'zeroSavingsSavedText' => __('Saved', 'shrinkwell-image-optimizer'),
            'zeroSavingsPercentText' => __('%', 'shrinkwell-image-optimizer'),
            'zeroSavingsReasonText' => __('Reason', 'shrinkwell-image-optimizer'),
            'zeroSavingsSelectText' => __('Select', 'shrinkwell-image-optimizer'),
            'zeroSavingsSelectVisibleText' => __('Select visible', 'shrinkwell-image-optimizer'),
            'forceReoptimizeRunningText' => __('Re-optimizing selected images...', 'shrinkwell-image-optimizer'),
            'forceReoptimizeText' => __('Re-optimize selected', 'shrinkwell-image-optimizer'),
            /* translators: 1: Number of processed images. 2: Human-readable saved size. */
            'forceReoptimizeDoneText' => __('Re-optimized %1$s images. %2$s saved.', 'shrinkwell-image-optimizer'),
            'forceReoptimizeNoneText' => __('Select images first.', 'shrinkwell-image-optimizer'),
            'zeroSavingsDismissRunningText' => __('Dismissing zero-savings images...', 'shrinkwell-image-optimizer'),
            'zeroSavingsDismissAllText' => __('Dismiss all', 'shrinkwell-image-optimizer'),
            /* translators: %s: Number of dismissed images. */
            'zeroSavingsDismissedText' => __('Dismissed %s images from future zero-savings scans.', 'shrinkwell-image-optimizer'),
            'cacheChecklistTitleText' => __('Cache checklist', 'shrinkwell-image-optimizer'),
            'cacheChecklistWordPressText' => __('Clear the WordPress cache plugin, if the site uses one.', 'shrinkwell-image-optimizer'),
            'cacheChecklistDiviText' => __('Clear Divi static CSS or page builder cache, if used.', 'shrinkwell-image-optimizer'),
            'cacheChecklistHostText' => __('Clear hosting cache, Varnish, CDN, or edge cache.', 'shrinkwell-image-optimizer'),
            'cacheChecklistBrowserText' => __('Test again in a private window or hard refresh the browser.', 'shrinkwell-image-optimizer'),
            'optimizationUrl' => $this->tab_url('settings'),
            'summaryUrl' => $this->tab_url('overview'),
            'formatsUrl' => $this->tab_url('settings'),
            'bulkUrl' => $this->tab_url('bulk'),
        ]);
    }

    public function enqueue_frontend_media_assets(): void
    {
        if (!is_user_logged_in() || !current_user_can('upload_files')) {
            return;
        }

        wp_enqueue_style(
            'shrinkwell-admin',
            SHRINKWELL_PLUGIN_URL . 'assets/admin.css',
            [],
            self::VERSION
        );
    }

    public function admin_notices(): void
    {
        $raw_result = '';
        $notice_nonce = isset($_GET['shrinkwell_notice_nonce']) ? sanitize_text_field(wp_unslash($_GET['shrinkwell_notice_nonce'])) : '';

        if (!$notice_nonce || !wp_verify_nonce($notice_nonce, 'shrinkwell_admin_notice')) {
            return;
        }

        if (isset($_GET['shrinkwell_result'])) {
            $raw_result = sanitize_key((string) wp_unslash($_GET['shrinkwell_result']));
        }

        if ('' === $raw_result) {
            return;
        }

        $restored_count = absint(wp_unslash($_GET['shrinkwell_count'] ?? 0));
        $error_count = absint(wp_unslash($_GET['shrinkwell_errors'] ?? 0));
        $messages = [
            'ok' => __('Image optimized.', 'shrinkwell-image-optimizer'),
            'restored' => __('Original image restored.', 'shrinkwell-image-optimizer'),
            'restored_all' => sprintf(
                /* translators: %s: Number of images restored from backup files. */
                __('Restored backups for %s images.', 'shrinkwell-image-optimizer'),
                number_format_i18n($restored_count)
            ),
            'restored_partial' => sprintf(
                /* translators: 1: Number of images restored from backup files. 2: Number of images that could not be restored. */
                __('Restored backups for %1$s images. %2$s images could not be restored; check the log below for details.', 'shrinkwell-image-optimizer'),
                number_format_i18n($restored_count),
                number_format_i18n($error_count)
            ),
            'error' => __('Image action failed.', 'shrinkwell-image-optimizer'),
        ];
        $result = $raw_result;

        if ('tested' === $result) {
            $notice = get_transient($this->test_notice_transient_key());
            delete_transient($this->test_notice_transient_key());

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

            $class = !empty($notice['success']) ? 'notice notice-success is-dismissible' : 'notice notice-warning is-dismissible';
            echo '<div class="' . esc_attr($class) . '"><p>' . esc_html((string) ($notice['message'] ?? '')) . '</p></div>';
            return;
        }

        if (empty($messages[$result])) {
            return;
        }

        $class = in_array($result, ['error', 'restored_partial'], true) ? 'notice notice-warning is-dismissible' : 'notice notice-success is-dismissible';

        echo '<div class="' . esc_attr($class) . '"><p>' . esc_html($messages[$result]) . '</p>';

        if (in_array($result, ['restored', 'restored_all', 'restored_partial'], true)) {
            $rebuild_url = add_query_arg([
                'shrinkwell_after_restore' => '1',
                'shrinkwell_notice_nonce' => wp_create_nonce('shrinkwell_admin_notice'),
            ], $this->tab_url('bulk'));
            echo '<p><strong>' . esc_html__('Restore done. Next: rebuild WebP delivery files.', 'shrinkwell-image-optimizer') . '</strong> ';
            echo '<a class="button button-primary" href="' . esc_url($rebuild_url) . '">' . esc_html__('Rebuild WebP delivery files', 'shrinkwell-image-optimizer') . '</a></p>';
        }

        echo '</div>';
    }

    private function test_notice_transient_key(): string
    {
        return 'shrinkwell_test_notice_' . get_current_user_id();
    }

    public function handle_review_notice_action(): void
    {
        if (!current_user_can('upload_files') || !check_admin_referer('shrinkwell_review_notice')) {
            wp_die(esc_html__('This request is not allowed.', 'shrinkwell-image-optimizer'));
        }

        $review_action = sanitize_key((string) wp_unslash($_POST['review_action'] ?? ''));
        $notice = $this->review_notice();

        if ('remind' === $review_action) {
            $notice['remind_after'] = time() + (14 * DAY_IN_SECONDS);
        } elseif ('dismiss' === $review_action) {
            $notice['dismissed'] = 1;
        }

        update_option(self::REVIEW_OPTION, $notice, false);

        wp_safe_redirect(esc_url_raw(wp_get_referer() ?: admin_url('upload.php?page=shrinkwell-image-optimizer')));
        exit;
    }

    private function should_show_review_notice(array $stats): bool
    {
        if ((int) ($stats['optimized_images'] ?? 0) < 20 || (int) ($stats['saved_bytes'] ?? 0) <= 0) {
            return false;
        }

        $notice = $this->review_notice();

        if (!empty($notice['dismissed']) || time() < (int) ($notice['remind_after'] ?? 0)) {
            return false;
        }

        return true;
    }

    private function render_review_notice(): void
    {
        $stats = wp_parse_args((array) get_option(self::STATS_OPTION, []), [
            'optimized_images' => 0,
            'saved_bytes' => 0,
        ]);

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

        $review_url = (string) apply_filters(
            'shrinkwell_review_url',
            'https://wordpress.org/support/plugin/shrinkwell-image-optimizer/reviews/#new-post'
        );
        ?>
        <div class="shrinkwell-review-notice">
            <div class="shrinkwell-review-stars" aria-hidden="true">★★★★★</div>
            <p>
                <?php
                echo esc_html(
                    sprintf(
                        /* translators: %s: Number of optimized images. */
                        __('Shrinkwell has optimized %s images on this site. If the free version helped, a quick WordPress.org review would help others find it.', 'shrinkwell-image-optimizer'),
                        number_format_i18n((int) $stats['optimized_images'])
                    )
                );
                ?>
            </p>
            <p class="shrinkwell-review-note"><?php esc_html_e('Reviews are posted on WordPress.org, so the review button opens the official plugin page.', 'shrinkwell-image-optimizer'); ?></p>
            <div class="shrinkwell-review-actions">
                <a class="button button-primary" href="<?php echo esc_url($review_url); ?>" target="_blank" rel="noopener noreferrer"><?php esc_html_e('Review on WordPress.org', 'shrinkwell-image-optimizer'); ?></a>
                <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
                    <?php wp_nonce_field('shrinkwell_review_notice'); ?>
                    <input type="hidden" name="action" value="shrinkwell_review_notice">
                    <input type="hidden" name="review_action" value="remind">
                    <button type="submit" class="button"><?php esc_html_e('Remind me in 14 days', 'shrinkwell-image-optimizer'); ?></button>
                </form>
                <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
                    <?php wp_nonce_field('shrinkwell_review_notice'); ?>
                    <input type="hidden" name="action" value="shrinkwell_review_notice">
                    <input type="hidden" name="review_action" value="dismiss">
                    <button type="submit" class="button button-link"><?php esc_html_e("Don't ask again", 'shrinkwell-image-optimizer'); ?></button>
                </form>
            </div>
        </div>
        <?php
    }

    private function render_demo_optimization_examples(): void
    {
        $examples = [
            [
                'title' => 'image-hero.jpg',
                'detail' => __('1.8 MB to 420 KB', 'shrinkwell-image-optimizer'),
                'status' => __('76% saved', 'shrinkwell-image-optimizer'),
            ],
            [
                'title' => 'product-photo.png',
                'detail' => __('WebP created', 'shrinkwell-image-optimizer'),
                'status' => __('Backup available', 'shrinkwell-image-optimizer'),
            ],
            [
                'title' => 'banner-home.jpg',
                'detail' => __('SEO filename check passed', 'shrinkwell-image-optimizer'),
                'status' => __('Frontend OK', 'shrinkwell-image-optimizer'),
            ],
        ];
        ?>
        <div class="shrinkwell-demo-examples">
            <strong><?php esc_html_e('Preview examples', 'shrinkwell-image-optimizer'); ?></strong>
            <ul>
                <?php foreach ($examples as $example) : ?>
                    <li>
                        <span><?php echo esc_html($example['title']); ?></span>
                        <em><?php echo esc_html($example['detail']); ?></em>
                        <b><?php echo esc_html($example['status']); ?></b>
                    </li>
                <?php endforeach; ?>
            </ul>
        </div>
        <?php
    }

    public function render_admin_page(): void
    {
        if (!current_user_can('upload_files')) {
            wp_die(esc_html__('You do not have permission to access this page.', 'shrinkwell-image-optimizer'));
        }

        $settings = $this->settings();
        $stats = wp_parse_args((array) get_option(self::STATS_OPTION, []), [
            'optimized_images' => 0,
            'saved_bytes' => 0,
        ]);
        $demo_preview = $this->is_demo_preview();

        if ($demo_preview) {
            $stats = $this->demo_stats();
        }
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only tab navigation; no state is changed.
        $active_tab = sanitize_key((string) wp_unslash($_GET['tab'] ?? 'overview'));
        if ('summary' === $active_tab) {
            $active_tab = 'overview';
        } elseif (in_array($active_tab, ['optimize', 'formats'], true)) {
            $active_tab = 'settings';
        }

        $is_pro = shrinkwell_is_pro();
        $tabs = [
            'overview' => __('Overview', 'shrinkwell-image-optimizer'),
            'bulk' => __('Optimize', 'shrinkwell-image-optimizer'),
            'settings' => __('Settings', 'shrinkwell-image-optimizer'),
            'seo' => __('SEO', 'shrinkwell-image-optimizer'),
            'media-cleanup' => __('Media Cleanup', 'shrinkwell-image-optimizer'),
            'backups' => __('Backups', 'shrinkwell-image-optimizer'),
            'advanced' => __('Advanced', 'shrinkwell-image-optimizer'),
            'pro' => $is_pro ? __('Account', 'shrinkwell-image-optimizer') : __('Pro', 'shrinkwell-image-optimizer'),
        ];

        if (!isset($tabs[$active_tab])) {
            $active_tab = 'overview';
        }

        $media_counts = $demo_preview ? $this->demo_media_counts() : $this->media_counts();
        $ready_to_optimize = (int) ($media_counts['processable'] ?? $media_counts['unoptimized']);
        $too_large_to_optimize = (int) ($media_counts['too_large'] ?? 0);
        $already_optimized_count = (int) ($media_counts['already_optimized'] ?? 0);
        $server_status = $this->server_status();
        $avif_feature_available = shrinkwell_feature_available('avif');
        $backup_inventory = 'backups' === $active_tab ? $this->backup_inventory() : null;
        $checks = $demo_preview ? $this->demo_checks() : $this->checks();
        $seo_issue_count = $demo_preview ? 3 : (int) get_option('shrinkwell_seo_missing_alt_count', 0);
        $media_cleanup = Shrinkwell_Media_Cleanup::instance();
        $possibly_unused_count = $media_cleanup->tab_badge_count();
        if ($demo_preview) {
            $settings['compression_profile'] = 'balanced';
            $settings['create_webp'] = 1;
            $settings['serve_webp'] = 1;
            $settings['create_avif'] = $server_status['avif'] ? 1 : 0;
            $settings['serve_avif'] = $server_status['avif'] ? 1 : 0;
            $settings['backup_originals'] = 1;
        }
        $total_media = (int) ($media_counts['total'] ?? ((int) $stats['optimized_images'] + $ready_to_optimize));
        $optimized_pct = $total_media > 0 ? (int) round(((int) $stats['optimized_images'] / $total_media) * 100) : 0;
        $reoptimize_reason = '';

        if ($ready_to_optimize > 0 && (int) $stats['optimized_images'] > 0) {
            if (!empty($settings['create_avif']) && !empty($settings['serve_avif'])) {
                $reoptimize_reason = __('AVIF is now enabled - existing images can be improved', 'shrinkwell-image-optimizer');
            } else {
                $reoptimize_reason = __('Settings changed since last optimization', 'shrinkwell-image-optimizer');
            }
        }

        $next_steps = [];

        if ($seo_issue_count > 0) {
            $next_steps[] = [
                'severity' => 'high',
                'title' => sprintf(
                    /* translators: %d: Number of images missing alt text. */
                    __('Add alt text to %d images', 'shrinkwell-image-optimizer'),
                    $seo_issue_count
                ),
                'desc' => __('Missing alt text harms SEO rankings and accessibility. Start with images in headers and service pages.', 'shrinkwell-image-optimizer'),
                'link' => $this->tab_url('seo'),
                'cta' => __('Go to SEO tab', 'shrinkwell-image-optimizer'),
            ];
        }

        if ($ready_to_optimize > 0) {
            $next_steps[] = [
                'severity' => 'medium',
                'title' => sprintf(
                    /* translators: %d: Number of images ready for optimization. */
                    __('Re-optimize %d images with current settings', 'shrinkwell-image-optimizer'),
                    $ready_to_optimize
                ),
                'desc' => $reoptimize_reason ?: __('Your settings have changed. Run the optimizer to apply them.', 'shrinkwell-image-optimizer'),
                'link' => $this->tab_url('bulk'),
                'cta' => __('Start optimization', 'shrinkwell-image-optimizer'),
            ];
        }

        if (!empty($checks['delivery_passed'])) {
            $next_steps[] = [
                'severity' => 'done',
                'title' => __('WebP/AVIF delivery is working correctly', 'shrinkwell-image-optimizer'),
                'desc' => __('Visitors automatically receive the best image format for their browser.', 'shrinkwell-image-optimizer'),
                'link' => '',
                'cta' => '',
            ];
        }

        $quality = $this->quality_for_settings($settings);
        $profile_label = self::compression_profiles()[$settings['compression_profile']]['label'] ?? __('Balanced', 'shrinkwell-image-optimizer');
        $resize_value = ((int) $settings['max_width'] || (int) $settings['max_height'])
            ? sprintf(
                /* translators: 1: Maximum image width. 2: Maximum image height. */
                __('Max %1$s wide, max %2$s high', 'shrinkwell-image-optimizer'),
                (int) $settings['max_width'] ? number_format_i18n((int) $settings['max_width']) . 'px' : __('unlimited', 'shrinkwell-image-optimizer'),
                (int) $settings['max_height'] ? number_format_i18n((int) $settings['max_height']) . 'px' : __('unlimited', 'shrinkwell-image-optimizer')
            )
            : __('Off', 'shrinkwell-image-optimizer');
        $webp_summary_label = (!empty($settings['create_webp']) && !empty($settings['serve_webp'])) ? __('On', 'shrinkwell-image-optimizer') : __('Check settings', 'shrinkwell-image-optimizer');
        $avif_summary_label = $avif_feature_available && !empty($settings['create_avif']) && !empty($settings['serve_avif']) ? __('On', 'shrinkwell-image-optimizer') : __('Check settings', 'shrinkwell-image-optimizer');
        $delivery_summary_label = !empty($checks['delivery_passed']) ? __('Passed', 'shrinkwell-image-optimizer') : __('Not tested', 'shrinkwell-image-optimizer');

        if ($demo_preview) {
            $webp_summary_label = __('On', 'shrinkwell-image-optimizer');
            $avif_summary_label = $server_status['avif'] ? __('Available', 'shrinkwell-image-optimizer') : __('Needs attention', 'shrinkwell-image-optimizer');
            $delivery_summary_label = __('Passed', 'shrinkwell-image-optimizer');
        }
        ?>
        <div class="wrap shrinkwell-wrap">
            <div class="shrinkwell-brand">
                <div class="shrinkwell-brand-main">
                    <img src="<?php echo esc_url(SHRINKWELL_PLUGIN_URL . 'assets/logo.png'); ?>" alt="" width="70" height="50">
                    <div>
                        <h1><?php esc_html_e('Shrinkwell Image Optimizer', 'shrinkwell-image-optimizer'); ?></h1>
                        <p><?php echo esc_html(shrinkwell_is_pro() ? __('Optimize without limits and safely clean up possibly unused media to reduce storage and backup size.', 'shrinkwell-image-optimizer') : __('Optimize your images with WebP and AVIF. Pro unlocks unlimited optimization and safe Media Cleanup.', 'shrinkwell-image-optimizer')); ?></p>
                    </div>
                </div>
                <div class="shrinkwell-brand-actions">
                    <a class="button button-primary" href="<?php echo esc_url($this->tab_url('bulk')); ?>"><?php esc_html_e('Optimize media', 'shrinkwell-image-optimizer'); ?></a>
                    <a class="button" href="<?php echo esc_url($this->tab_url('settings')); ?>"><?php esc_html_e('Settings', 'shrinkwell-image-optimizer'); ?></a>
                </div>
            </div>

            <nav class="nav-tab-wrapper shrinkwell-tabs" aria-label="<?php esc_attr_e('Image optimizer settings sections', 'shrinkwell-image-optimizer'); ?>">
                <?php foreach ($tabs as $tab => $label) : ?>
                    <?php
                    $badge = '';
                    if ('seo' === $tab && $seo_issue_count > 0) {
                        $badge = '<span class="shrinkwell-tab-badge">' . esc_html(number_format_i18n($seo_issue_count)) . '</span>';
                    } elseif ('media-cleanup' === $tab && $possibly_unused_count > 0) {
                        $badge = '<span class="shrinkwell-tab-badge shrinkwell-tab-badge--warn">' . esc_html(number_format_i18n($possibly_unused_count)) . '</span>';
                    }
                    ?>
                    <a class="nav-tab <?php echo $active_tab === $tab ? 'nav-tab-active' : ''; ?>" data-tab="<?php echo esc_attr($tab); ?>" href="<?php echo esc_url($this->tab_url($tab)); ?>">
                        <?php echo esc_html($label); ?>
                        <?php echo wp_kses($badge, ['span' => ['class' => true]]); ?>
                    </a>
                <?php endforeach; ?>
            </nav>

            <?php if ('pro' !== $active_tab) : ?>
                <?php $this->render_review_notice(); ?>
            <?php endif; ?>

            <?php if ($demo_preview) : ?>
                <div class="notice notice-info inline shrinkwell-demo-preview-notice">
                    <p><?php echo esc_html__('Demo data shown for preview purposes.', 'shrinkwell-image-optimizer'); ?></p>
                </div>
            <?php endif; ?>

            <?php if (!empty($media_counts['truncated'])) : ?>
                <div class="notice notice-warning inline">
                    <p>
                        <?php echo esc_html__('Shrinkwell scanned the first 5000 images only. Counts may be incomplete on larger media libraries.', 'shrinkwell-image-optimizer'); ?>
                        <a href="<?php echo esc_url($this->tab_url('advanced')); ?>"><?php echo esc_html__('Open Advanced tab', 'shrinkwell-image-optimizer'); ?></a>
                    </p>
                </div>
            <?php endif; ?>

            <?php if ('pro' !== $active_tab) : ?>
            <div class="shrinkwell-stats" aria-label="<?php esc_attr_e('Optimization statistics', 'shrinkwell-image-optimizer'); ?>">
                <div class="shrinkwell-stat-card shrinkwell-stat-images">
                    <span class="shrinkwell-stat-icon shrinkwell-stat-icon-image" aria-hidden="true"></span>
                    <div>
                        <strong><?php echo esc_html(number_format_i18n((int) $stats['optimized_images'])); ?></strong>
                        <span><?php esc_html_e('Optimized images', 'shrinkwell-image-optimizer'); ?></span>
                        <em><?php esc_html_e('Across your site', 'shrinkwell-image-optimizer'); ?></em>
                        <div class="shrinkwell-stat-progress">
                            <div class="shrinkwell-stat-progress-fill" style="width: <?php echo esc_attr((string) $optimized_pct); ?>%"></div>
                        </div>
                        <em class="shrinkwell-stat-pct">
                            <?php
                            echo esc_html(
                                sprintf(
                                    /* translators: 1: Percentage optimized. 2: Total image count. */
                                    __('%1$d%% of %2$s images', 'shrinkwell-image-optimizer'),
                                    $optimized_pct,
                                    number_format_i18n($total_media)
                                )
                            );
                            ?>
                        </em>
                    </div>
                </div>
                <div class="shrinkwell-stat-card shrinkwell-stat-space">
                    <span class="shrinkwell-stat-icon shrinkwell-stat-icon-database" aria-hidden="true"></span>
                    <div>
                        <strong><?php echo esc_html(size_format((int) $stats['saved_bytes'], 1)); ?></strong>
                        <span><?php esc_html_e('Total savings', 'shrinkwell-image-optimizer'); ?></span>
                    </div>
                </div>
                <div class="shrinkwell-stat-card shrinkwell-stat-waiting">
                    <span class="shrinkwell-stat-icon shrinkwell-stat-icon-clock" aria-hidden="true"></span>
                    <div>
                        <strong><?php echo esc_html(number_format_i18n($ready_to_optimize)); ?></strong>
                        <?php if (0 === $ready_to_optimize && $too_large_to_optimize > 0) : ?>
                            <span><?php esc_html_e('Needs resizing first', 'shrinkwell-image-optimizer'); ?></span>
                            <em><?php esc_html_e('Too large for local processing', 'shrinkwell-image-optimizer'); ?></em>
                        <?php elseif ((int) $stats['optimized_images'] > 0) : ?>
                            <span><?php esc_html_e('Ready to re-optimize', 'shrinkwell-image-optimizer'); ?></span>
                            <em><?php echo $reoptimize_reason ? esc_html($reoptimize_reason) : esc_html__('With current settings', 'shrinkwell-image-optimizer'); ?></em>
                        <?php else : ?>
                            <span><?php esc_html_e('Ready to optimize', 'shrinkwell-image-optimizer'); ?></span>
                            <em><?php esc_html_e('Existing media items', 'shrinkwell-image-optimizer'); ?></em>
                        <?php endif; ?>
                    </div>
                </div>
            </div>
            <?php endif; ?>

            <?php if ('overview' === $active_tab) : ?>
                <div class="shrinkwell-overview-layout">
                    <main class="shrinkwell-overview-main">
                        <?php if (!empty($next_steps)) : ?>
                            <section class="shrinkwell-panel shrinkwell-next-steps-panel">
                                <h2><?php esc_html_e('Recommended next steps', 'shrinkwell-image-optimizer'); ?></h2>
                                <ol class="shrinkwell-next-steps-list">
                                    <?php foreach ($next_steps as $index => $step) : ?>
                                        <li class="shrinkwell-next-step shrinkwell-next-step--<?php echo esc_attr((string) $step['severity']); ?>">
                                            <span class="shrinkwell-next-step__number"><?php echo esc_html((string) ($index + 1)); ?></span>
                                            <div class="shrinkwell-next-step__body">
                                                <strong><?php echo esc_html((string) $step['title']); ?></strong>
                                                <p><?php echo esc_html((string) $step['desc']); ?></p>
                                                <?php if (!empty($step['link']) && !empty($step['cta'])) : ?>
                                                    <a href="<?php echo esc_url((string) $step['link']); ?>" class="shrinkwell-next-step__cta">
                                                        <?php echo esc_html((string) $step['cta']); ?> &rarr;
                                                    </a>
                                                <?php endif; ?>
                                            </div>
                                        </li>
                                    <?php endforeach; ?>
                                </ol>
                            </section>
                        <?php endif; ?>
                        <section class="shrinkwell-panel">
                            <div class="shrinkwell-panel-header">
                                <div>
                                    <h2><?php esc_html_e('Current settings', 'shrinkwell-image-optimizer'); ?></h2>
                                    <p><?php esc_html_e('The settings Shrinkwell will use for new uploads and media library optimization.', 'shrinkwell-image-optimizer'); ?></p>
                                </div>
                                <div class="shrinkwell-panel-actions">
                                    <a class="button" href="<?php echo esc_url($this->tab_url('settings')); ?>"><?php esc_html_e('Settings', 'shrinkwell-image-optimizer'); ?></a>
                                    <a class="button" href="<?php echo esc_url($this->tab_url('advanced')); ?>"><?php esc_html_e('Advanced', 'shrinkwell-image-optimizer'); ?></a>
                                </div>
                            </div>
                            <div class="shrinkwell-summary-grid shrinkwell-summary-grid-compact">
                                <article class="shrinkwell-summary-card">
                                    <h3><?php esc_html_e('Optimize', 'shrinkwell-image-optimizer'); ?></h3>
                                    <dl>
                                        <div><dt><?php esc_html_e('New uploads', 'shrinkwell-image-optimizer'); ?></dt><dd><strong><?php echo empty($settings['auto_optimize']) ? esc_html__('Off', 'shrinkwell-image-optimizer') : esc_html__('On', 'shrinkwell-image-optimizer'); ?></strong></dd></div>
                                        <div><dt><?php esc_html_e('Compression', 'shrinkwell-image-optimizer'); ?></dt><dd><strong><?php echo esc_html($profile_label); ?></strong></dd></div>
                                        <div><dt><?php esc_html_e('Thumbnails', 'shrinkwell-image-optimizer'); ?></dt><dd><strong><?php echo empty($settings['optimize_thumbnails']) ? esc_html__('Off', 'shrinkwell-image-optimizer') : esc_html__('On', 'shrinkwell-image-optimizer'); ?></strong></dd></div>
                                    </dl>
                                </article>
                                <article class="shrinkwell-summary-card">
                                    <h3><?php esc_html_e('Formats', 'shrinkwell-image-optimizer'); ?></h3>
                                    <dl>
                                        <div><dt><?php esc_html_e('WebP', 'shrinkwell-image-optimizer'); ?></dt><dd><strong><?php echo esc_html($webp_summary_label); ?></strong></dd></div>
                                        <div><dt><?php esc_html_e('AVIF', 'shrinkwell-image-optimizer'); ?></dt><dd><strong><?php echo esc_html($avif_summary_label); ?></strong></dd></div>
                                        <div><dt><?php esc_html_e('Delivery test', 'shrinkwell-image-optimizer'); ?></dt><dd><strong><?php echo esc_html($delivery_summary_label); ?></strong></dd></div>
                                    </dl>
                                </article>
                                <article class="shrinkwell-summary-card">
                                    <h3><?php esc_html_e('Safety', 'shrinkwell-image-optimizer'); ?></h3>
                                    <dl>
                                        <div><dt><?php esc_html_e('Backups', 'shrinkwell-image-optimizer'); ?></dt><dd><strong><?php echo empty($settings['backup_originals']) ? esc_html__('Off', 'shrinkwell-image-optimizer') : esc_html__('On', 'shrinkwell-image-optimizer'); ?></strong></dd></div>
                                        <div><dt><?php esc_html_e('Resize', 'shrinkwell-image-optimizer'); ?></dt><dd><strong><?php echo esc_html($resize_value); ?></strong></dd></div>
                                        <div><dt><?php esc_html_e('EXIF', 'shrinkwell-image-optimizer'); ?></dt><dd><strong><?php echo empty($settings['strip_exif']) ? esc_html__('Kept', 'shrinkwell-image-optimizer') : esc_html__('Removed when possible', 'shrinkwell-image-optimizer'); ?></strong></dd></div>
                                    </dl>
                                </article>
                            </div>
                        </section>

                        <section class="shrinkwell-panel">
                            <h2><?php esc_html_e('Recommended settings scan', 'shrinkwell-image-optimizer'); ?></h2>
                            <p><?php esc_html_e('Scan a sample of your media library and get practical settings advice for this site.', 'shrinkwell-image-optimizer'); ?></p>
                            <button type="button" class="button button-primary" id="shrinkwell-run-recommendation-scan">
                                <?php esc_html_e('Scan site and recommend settings', 'shrinkwell-image-optimizer'); ?>
                            </button>
                            <div id="shrinkwell-recommendation-results" class="shrinkwell-test-results shrinkwell-recommendation-results" hidden></div>
                        </section>

                        <section class="shrinkwell-panel">
                            <h2><?php esc_html_e('Quick actions', 'shrinkwell-image-optimizer'); ?></h2>
                            <div class="shrinkwell-action-grid">
                        <a class="shrinkwell-action-card" href="<?php echo esc_url($this->tab_url('settings')); ?>">
                            <strong><?php esc_html_e('Adjust settings', 'shrinkwell-image-optimizer'); ?></strong>
                            <span><?php esc_html_e('Choose compression, WebP/AVIF delivery, thumbnails, and metadata settings in one place.', 'shrinkwell-image-optimizer'); ?></span>
                        </a>
                        <a class="shrinkwell-action-card" href="<?php echo esc_url($this->tab_url('advanced')); ?>">
                            <strong><?php esc_html_e('Advanced limits', 'shrinkwell-image-optimizer'); ?></strong>
                            <span><?php esc_html_e('Review server safety, backup behavior, and large-image processing limits.', 'shrinkwell-image-optimizer'); ?></span>
                        </a>
                        <a class="shrinkwell-action-card" href="<?php echo esc_url($this->tab_url('bulk')); ?>">
                            <strong><?php esc_html_e('Optimize media library', 'shrinkwell-image-optimizer'); ?></strong>
                            <span><?php esc_html_e('Optimize existing images and review recent log entries.', 'shrinkwell-image-optimizer'); ?></span>
                        </a>
                        <a class="shrinkwell-action-card" href="<?php echo esc_url($this->tab_url('seo')); ?>">
                            <strong><?php esc_html_e('Scan image SEO', 'shrinkwell-image-optimizer'); ?></strong>
                            <span><?php esc_html_e('Find missing alt text and weak filenames without changing content automatically.', 'shrinkwell-image-optimizer'); ?></span>
                        </a>
                        <a class="shrinkwell-action-card" href="<?php echo esc_url($this->tab_url('backups')); ?>">
                            <strong><?php esc_html_e('Manage originals', 'shrinkwell-image-optimizer'); ?></strong>
                            <span><?php esc_html_e('Review stored original files and restore images when needed.', 'shrinkwell-image-optimizer'); ?></span>
                        </a>
                            </div>
                        </section>
                    </main>

                    <aside class="shrinkwell-overview-side">
                        <?php $this->render_free_limit_notice($demo_preview); ?>

                        <section class="shrinkwell-panel shrinkwell-compact-status">
                            <h2><?php esc_html_e('Status', 'shrinkwell-image-optimizer'); ?></h2>
                            <?php echo wp_kses_post($this->status_card(__('Uploads', 'shrinkwell-image-optimizer'), !empty($settings['auto_optimize']), __('On', 'shrinkwell-image-optimizer'), __('Off', 'shrinkwell-image-optimizer'))); ?>
                            <?php echo wp_kses_post($this->status_card(__('WebP', 'shrinkwell-image-optimizer'), !empty($settings['create_webp']) && $server_status['webp_create'], __('Ready', 'shrinkwell-image-optimizer'), __('Check', 'shrinkwell-image-optimizer'))); ?>
                            <?php echo wp_kses_post($this->status_card(__('AVIF', 'shrinkwell-image-optimizer'), !empty($settings['create_avif']) && $server_status['avif'], __('Ready', 'shrinkwell-image-optimizer'), __('WordPress cannot write AVIF on this server.', 'shrinkwell-image-optimizer'))); ?>
                            <?php echo wp_kses_post($this->status_card(__('Frontend', 'shrinkwell-image-optimizer'), !empty($settings['serve_webp']) || !empty($settings['serve_avif']), __('Serving modern formats', 'shrinkwell-image-optimizer'), __('Off', 'shrinkwell-image-optimizer'))); ?>
                            <p class="description"><?php esc_html_e('Clear cache after changing format delivery.', 'shrinkwell-image-optimizer'); ?></p>
                        </section>
                    </aside>
                </div>
            <?php endif; ?>

            <?php if (in_array($active_tab, ['settings', 'advanced'], true)) : ?>
            <form method="post" action="options.php" class="shrinkwell-panel" id="shrinkwell-settings-form">
                <?php settings_fields('shrinkwell_settings_group'); ?>
                <?php $this->render_hidden_settings($settings, $active_tab); ?>

                <?php if ('settings' === $active_tab) : ?>
                    <div class="shrinkwell-sticky-save" id="shrinkwell-sticky-save" hidden>
                        <span class="shrinkwell-sticky-save__label">
                            <?php esc_html_e('Unsaved changes', 'shrinkwell-image-optimizer'); ?>
                        </span>
                        <button type="submit" form="shrinkwell-settings-form" class="button button-primary">
                            <?php esc_html_e('Save settings', 'shrinkwell-image-optimizer'); ?>
                        </button>
                    </div>
                    <h2><?php esc_html_e('Settings', 'shrinkwell-image-optimizer'); ?></h2>
                    <h3><?php esc_html_e('Image optimization', 'shrinkwell-image-optimizer'); ?></h3>

                    <label class="shrinkwell-toggle">
                        <input type="hidden" name="<?php echo esc_attr(self::OPTION); ?>[auto_optimize]" value="0">
                        <input type="checkbox" name="<?php echo esc_attr(self::OPTION); ?>[auto_optimize]" value="1" <?php checked($settings['auto_optimize']); ?> <?php disabled(!shrinkwell_can_use_auto_optimize()); ?>>
                        <span>
                            <?php esc_html_e('Optimize new uploads automatically', 'shrinkwell-image-optimizer'); ?><?php if (!shrinkwell_can_use_auto_optimize()) : ?> <span class="shrinkwell-pro-label"><?php esc_html_e('Pro', 'shrinkwell-image-optimizer'); ?></span><?php endif; ?>
                            <?php echo wp_kses($this->help_tip(__('New JPEG/PNG uploads are optimized immediately using the active settings. Existing images are handled on the Media Optimization page.', 'shrinkwell-image-optimizer')), $this->help_tip_allowed_html()); ?>
                        </span>
                    </label>

                    <h3>
                        <?php esc_html_e('Compression type', 'shrinkwell-image-optimizer'); ?>
                        <?php echo wp_kses($this->help_tip(__('Balanced is the safest default. Use Near lossless when image detail matters more than savings. Use Custom only when you want manual control.', 'shrinkwell-image-optimizer')), $this->help_tip_allowed_html()); ?>
                    </h3>
                    <div class="shrinkwell-segmented" role="radiogroup">
                        <?php foreach (self::compression_profiles() as $profile => $profile_data) : ?>
                            <label>
                                <input
                                    type="radio"
                                    name="<?php echo esc_attr(self::OPTION); ?>[compression_profile]"
                                    value="<?php echo esc_attr($profile); ?>"
                                    data-label="<?php echo esc_attr($profile_data['label']); ?>"
                                    data-description="<?php echo esc_attr($profile_data['description']); ?>"
                                    data-quality="<?php echo esc_attr((string) $profile_data['quality']); ?>"
                                    <?php checked($settings['compression_profile'], $profile); ?>
                                >
                                <span>
                                    <?php echo esc_html($profile_data['label']); ?>
                                    <?php if (!empty($profile_data['recommended'])) : ?>
                                        <em><?php esc_html_e('Recommended', 'shrinkwell-image-optimizer'); ?></em>
                                    <?php endif; ?>
                                </span>
                            </label>
                        <?php endforeach; ?>
                    </div>
                    <div class="shrinkwell-profile-help">
                        <strong id="shrinkwell-profile-title"><?php echo esc_html(sprintf(
                            /* translators: %s: Selected compression profile label. */
                            __('Selected: %s', 'shrinkwell-image-optimizer'),
                            self::compression_profiles()[$settings['compression_profile']]['label'] ?? __('Balanced', 'shrinkwell-image-optimizer')
                        )); ?></strong>
                        <p id="shrinkwell-profile-description"><?php echo esc_html(self::compression_profiles()[$settings['compression_profile']]['description'] ?? ''); ?></p>
                    </div>

                    <div class="shrinkwell-grid">
                        <label class="shrinkwell-range-field shrinkwell-custom-controls">
                            <span><?php esc_html_e('Custom compression strength', 'shrinkwell-image-optimizer'); ?></span>
                            <input id="shrinkwell-custom-quality" type="hidden" name="<?php echo esc_attr(self::OPTION); ?>[quality]" value="<?php echo esc_attr((int) $settings['quality']); ?>">
                            <input id="shrinkwell-custom-strength" type="range" min="1" max="10" step="1" name="<?php echo esc_attr(self::OPTION); ?>[custom_strength]" value="<?php echo esc_attr((string) self::custom_strength_for_quality((int) $settings['quality'])); ?>">
                            <span class="shrinkwell-range-readout"><strong id="shrinkwell-custom-strength-value"><?php echo esc_html((string) self::custom_strength_for_quality((int) $settings['quality'])); ?>/10</strong> <span id="shrinkwell-custom-strength-meaning"></span></span>
                            <small id="shrinkwell-custom-strength-description"><?php esc_html_e('Only used for Custom. Lower strength keeps more detail; higher strength makes smaller files with more visible compression risk.', 'shrinkwell-image-optimizer'); ?></small>
                        </label>
                        <input id="shrinkwell-active-quality" type="hidden" value="<?php echo esc_attr((string) $quality); ?>" disabled>
                    </div>

                    <label class="shrinkwell-toggle">
                        <input type="hidden" name="<?php echo esc_attr(self::OPTION); ?>[optimize_thumbnails]" value="0">
                        <input type="checkbox" name="<?php echo esc_attr(self::OPTION); ?>[optimize_thumbnails]" value="1" <?php checked($settings['optimize_thumbnails']); ?>>
                        <span>
                            <?php esc_html_e('Optimize thumbnails too', 'shrinkwell-image-optimizer'); ?>
                            <?php echo wp_kses($this->help_tip(__('Recommended for most sites. WordPress themes often load thumbnail sizes in grids, archives, sliders, and product cards.', 'shrinkwell-image-optimizer')), $this->help_tip_allowed_html()); ?>
                        </span>
                    </label>

                    <label class="shrinkwell-toggle">
                        <input type="hidden" name="<?php echo esc_attr(self::OPTION); ?>[strip_exif]" value="0">
                        <input type="checkbox" name="<?php echo esc_attr(self::OPTION); ?>[strip_exif]" value="1" <?php checked($settings['strip_exif']); ?>>
                        <span>
                            <?php esc_html_e('Remove metadata/EXIF where possible', 'shrinkwell-image-optimizer'); ?>
                            <?php echo wp_kses($this->help_tip(__('Removes camera metadata where the image editor allows it. This can reduce file size and avoid exposing camera/location details.', 'shrinkwell-image-optimizer')), $this->help_tip_allowed_html()); ?>
                        </span>
                    </label>
                    <hr class="shrinkwell-section-divider">
                    <h3><?php esc_html_e('Formats and delivery', 'shrinkwell-image-optimizer'); ?></h3>
                    <div class="shrinkwell-profile-help">
                        <strong><?php esc_html_e('What these options do', 'shrinkwell-image-optimizer'); ?></strong>
                        <p><?php esc_html_e('WebP and AVIF are modern image formats. The plugin keeps your original JPEG/PNG files as a safe fallback, then creates smaller modern versions and serves them to browsers that support them.', 'shrinkwell-image-optimizer'); ?></p>
                    </div>

                    <label class="shrinkwell-toggle">
                        <input type="hidden" name="<?php echo esc_attr(self::OPTION); ?>[create_webp]" value="0">
                        <input type="checkbox" name="<?php echo esc_attr(self::OPTION); ?>[create_webp]" value="1" <?php checked($settings['create_webp']); ?>>
                        <span>
                            <?php esc_html_e('Generate WebP versions', 'shrinkwell-image-optimizer'); ?>
                            <?php echo wp_kses($this->help_tip(__('Creates .webp files next to the original images. The original JPEG/PNG files stay available as fallback.', 'shrinkwell-image-optimizer')), $this->help_tip_allowed_html()); ?>
                            <small><?php esc_html_e('Recommended. Creates smaller WebP files next to your originals, without deleting the original images.', 'shrinkwell-image-optimizer'); ?></small>
                        </span>
                    </label>

                    <label class="shrinkwell-toggle">
                        <input type="hidden" name="<?php echo esc_attr(self::OPTION); ?>[serve_webp]" value="0">
                        <input type="checkbox" name="<?php echo esc_attr(self::OPTION); ?>[serve_webp]" value="1" <?php checked($settings['serve_webp']); ?>>
                        <span>
                            <?php esc_html_e('Use WebP automatically on the website', 'shrinkwell-image-optimizer'); ?>
                            <?php echo wp_kses($this->help_tip(__('Adds frontend markup so modern browsers receive WebP while older browsers keep the original JPEG/PNG fallback.', 'shrinkwell-image-optimizer')), $this->help_tip_allowed_html()); ?>
                            <small><?php esc_html_e('Recommended. Visitors get WebP when available; older browsers still receive JPEG/PNG.', 'shrinkwell-image-optimizer'); ?></small>
                        </span>
                    </label>

                    <label class="shrinkwell-toggle">
                        <input type="hidden" name="<?php echo esc_attr(self::OPTION); ?>[create_avif]" value="0">
                        <input type="checkbox" name="<?php echo esc_attr(self::OPTION); ?>[create_avif]" value="1" <?php checked($settings['create_avif']); ?> <?php disabled(!$avif_feature_available || !$server_status['avif']); ?>>
                        <span>
                            <?php esc_html_e('Generate AVIF versions', 'shrinkwell-image-optimizer'); ?>
                            <?php echo wp_kses($this->help_tip(__('Creates AVIF copies when this server supports safe local AVIF encoding. AVIF can take longer than WebP, so Shrinkwell still skips files that exceed safe local processing limits.', 'shrinkwell-image-optimizer')), $this->help_tip_allowed_html()); ?>
                            <small>
                                <?php
                                echo esc_html(
                                    $server_status['avif']
                                        ? __('AVIF is included in Free within your monthly optimization limit.', 'shrinkwell-image-optimizer')
                                        : __('AVIF is not available on this server because the local encoder check did not pass.', 'shrinkwell-image-optimizer')
                                );
                                ?>
                            </small>
                        </span>
                    </label>

                    <label class="shrinkwell-toggle">
                        <input type="hidden" name="<?php echo esc_attr(self::OPTION); ?>[serve_avif]" value="0">
                        <input type="checkbox" name="<?php echo esc_attr(self::OPTION); ?>[serve_avif]" value="1" <?php checked($settings['serve_avif']); ?> <?php disabled(!$avif_feature_available || !$server_status['avif']); ?>>
                        <span>
                            <?php esc_html_e('Use AVIF before WebP when available', 'shrinkwell-image-optimizer'); ?>
                            <small>
                                <?php
                                echo esc_html(
                                    $server_status['avif']
                                        ? __('Browsers that support AVIF can receive AVIF first, then WebP, then the original JPEG/PNG fallback.', 'shrinkwell-image-optimizer')
                                        : __('AVIF delivery needs local AVIF encoder support. WebP delivery can still be used as the stable fallback.', 'shrinkwell-image-optimizer')
                                );
                                ?>
                            </small>
                        </span>
                    </label>

                    <div class="shrinkwell-status-grid shrinkwell-encoder-status">
                        <?php echo wp_kses_post($this->status_card(__('WebP encoder', 'shrinkwell-image-optimizer'), $server_status['webp_create'], __('Available', 'shrinkwell-image-optimizer'), __('Not available', 'shrinkwell-image-optimizer'))); ?>
                        <?php echo wp_kses_post($this->status_card(__('AVIF encoder', 'shrinkwell-image-optimizer'), $server_status['avif'], __('Available', 'shrinkwell-image-optimizer'), __('WordPress cannot write AVIF with the current GD/Imagick setup.', 'shrinkwell-image-optimizer'))); ?>
                    </div>

                    <div class="shrinkwell-delivery-test">
                        <h3><?php esc_html_e('Frontend delivery test', 'shrinkwell-image-optimizer'); ?></h3>
                        <p class="description">
                            <?php esc_html_e('Test whether an optimized media item has next-gen files and can produce frontend fallback markup.', 'shrinkwell-image-optimizer'); ?>
                            <?php echo wp_kses($this->help_tip(__('Run this after clearing page cache. It checks whether Shrinkwell can produce picture markup and whether WebP files exist for a real media item.', 'shrinkwell-image-optimizer')), $this->help_tip_allowed_html()); ?>
                        </p>
                        <button type="button" class="button" id="shrinkwell-run-delivery-test">
                            <?php esc_html_e('Run delivery test', 'shrinkwell-image-optimizer'); ?>
                        </button>
                        <button type="button" class="button" id="shrinkwell-run-frontend-proof-scan">
                            <?php esc_html_e('Run Page Proof scan', 'shrinkwell-image-optimizer'); ?>
                        </button>
                        <label class="screen-reader-text" for="shrinkwell-proof-url"><?php esc_html_e('Page URL to scan', 'shrinkwell-image-optimizer'); ?></label>
                        <input type="url" id="shrinkwell-proof-url" class="regular-text" value="<?php echo esc_url(home_url('/')); ?>" placeholder="<?php echo esc_attr(home_url('/codex/')); ?>">
                        <p class="description"><?php esc_html_e('Use Page Proof to check a real frontend page. It verifies whether WebP is visible in the page HTML, estimates possible payload savings, and flags cached or uncovered JPG/PNG URLs.', 'shrinkwell-image-optimizer'); ?></p>
                        <div id="shrinkwell-delivery-test-results" class="shrinkwell-test-results" hidden></div>
                        <div id="shrinkwell-frontend-proof-results" class="shrinkwell-test-results" hidden></div>
                    </div>
                <?php else : ?>
                    <h2><?php esc_html_e('Advanced', 'shrinkwell-image-optimizer'); ?></h2>
                    <div class="shrinkwell-profile-help">
                        <strong><?php esc_html_e('Keep this simple unless you know the site needs different limits.', 'shrinkwell-image-optimizer'); ?></strong>
                        <p><?php esc_html_e('These options protect the original files and prevent extremely large uploads from slowing down pages. For most websites, keeping backups on and using the recommended resize limit is the safest choice.', 'shrinkwell-image-optimizer'); ?></p>
                    </div>
                    <div class="shrinkwell-after-restore-guide">
                        <strong><?php esc_html_e('WebP delivery files rebuild', 'shrinkwell-image-optimizer'); ?></strong>
                        <span><?php esc_html_e('Use this after restoring backups. It recreates missing WebP delivery files without recompressing the original JPG/PNG images.', 'shrinkwell-image-optimizer'); ?></span>
                        <button type="button" class="button shrinkwell-trigger-webp-rebuild">
                            <?php esc_html_e('Rebuild missing WebP delivery files', 'shrinkwell-image-optimizer'); ?>
                        </button>
                        <span class="shrinkwell-webp-rebuild-status" hidden aria-live="polite"></span>
                    </div>

                    <h3><?php esc_html_e('Server safety status', 'shrinkwell-image-optimizer'); ?></h3>
                    <p class="description"><?php esc_html_e('Quick check before optimizing. Green means Shrinkwell can safely work with normal website images on this hosting setup.', 'shrinkwell-image-optimizer'); ?></p>
                    <div class="shrinkwell-safety-grid">
                        <div class="shrinkwell-safety-card <?php echo $server_status['webp_create'] ? 'is-ok' : 'is-warn'; ?>">
                            <span><?php esc_html_e('WebP creation', 'shrinkwell-image-optimizer'); ?></span>
                            <strong><?php echo $server_status['webp_create'] ? esc_html__('Ready', 'shrinkwell-image-optimizer') : esc_html__('Needs check', 'shrinkwell-image-optimizer'); ?></strong>
                            <small><?php echo $server_status['webp_create'] ? esc_html__('Shrinkwell can create WebP files.', 'shrinkwell-image-optimizer') : esc_html__('WebP creation is not available on this server.', 'shrinkwell-image-optimizer'); ?></small>
                        </div>
                        <div class="shrinkwell-safety-card is-ok">
                            <span><?php esc_html_e('Server memory', 'shrinkwell-image-optimizer'); ?></span>
                            <strong><?php echo esc_html($server_status['memory_limit'] ?: __('Unknown', 'shrinkwell-image-optimizer')); ?></strong>
                            <small><?php esc_html_e('Used to avoid processing images that are too heavy for this server.', 'shrinkwell-image-optimizer'); ?></small>
                        </div>
                        <div class="shrinkwell-safety-card is-ok">
                            <span><?php esc_html_e('Safe image size', 'shrinkwell-image-optimizer'); ?></span>
                            <strong><?php echo esc_html($this->format_pixel_limit((int) $server_status['max_pixels'])); ?></strong>
                            <small><?php esc_html_e('Bigger images are skipped safely instead of risking a timeout.', 'shrinkwell-image-optimizer'); ?></small>
                        </div>
                        <div class="shrinkwell-safety-card <?php echo $server_status['uploads_writable'] ? 'is-ok' : 'is-warn'; ?>">
                            <span><?php esc_html_e('Uploads folder', 'shrinkwell-image-optimizer'); ?></span>
                            <strong><?php echo $server_status['uploads_writable'] ? esc_html__('Writable', 'shrinkwell-image-optimizer') : esc_html__('Needs check', 'shrinkwell-image-optimizer'); ?></strong>
                            <small><?php echo $server_status['uploads_writable'] ? esc_html__('Shrinkwell can save optimized files.', 'shrinkwell-image-optimizer') : esc_html__('WordPress cannot write to the uploads folder.', 'shrinkwell-image-optimizer'); ?></small>
                        </div>
                    </div>
                    <details class="shrinkwell-proof-details">
                        <summary><?php esc_html_e('Technical engine details', 'shrinkwell-image-optimizer'); ?></summary>
                        <p class="description">
                            <?php
                            if (!empty($server_status['cwebp_path'])) {
                                echo wp_kses(
                                    sprintf(
                                        /* translators: %s: cwebp executable path. */
                                        __('Fast WebP engine found at %s. Shrinkwell can use it with the WordPress image editor as fallback.', 'shrinkwell-image-optimizer'),
                                        '<code>' . esc_html($server_status['cwebp_path']) . '</code>'
                                    ),
                                    ['code' => []]
                                );
                            } else {
                                esc_html_e('Optional fast WebP engine was not found. This is okay: Shrinkwell will use the WordPress image editor for WebP generation.', 'shrinkwell-image-optimizer');
                            }
                            ?>
                        </p>
                        <p class="description">
                            <?php
                            echo esc_html(
                                sprintf(
                                    /* translators: %s: Whether command execution is allowed. */
                                    __('Command-line image tools: %s.', 'shrinkwell-image-optimizer'),
                                    $server_status['exec'] ? __('allowed', 'shrinkwell-image-optimizer') : __('not available', 'shrinkwell-image-optimizer')
                                )
                            );
                            ?>
                        </p>
                    </details>

                    <label class="shrinkwell-toggle">
                        <input type="hidden" name="<?php echo esc_attr(self::OPTION); ?>[backup_originals]" value="0">
                        <input type="checkbox" name="<?php echo esc_attr(self::OPTION); ?>[backup_originals]" value="1" <?php checked($settings['backup_originals']); ?>>
                        <span>
                            <?php esc_html_e('Keep original backups for restore', 'shrinkwell-image-optimizer'); ?>
                            <?php echo wp_kses($this->help_tip(__('Stores a copy of each original file before optimization. This uses extra disk space, but makes testing and restore safer.', 'shrinkwell-image-optimizer')), $this->help_tip_allowed_html()); ?>
                            <small><?php esc_html_e('Recommended while testing. This lets you restore originals if a setting is too aggressive.', 'shrinkwell-image-optimizer'); ?></small>
                        </span>
                    </label>

                    <h3><?php esc_html_e('Resize large uploads', 'shrinkwell-image-optimizer'); ?></h3>
                    <div class="shrinkwell-resize-presets" role="radiogroup" aria-label="<?php esc_attr_e('Resize presets', 'shrinkwell-image-optimizer'); ?>">
                        <button type="button" class="button" data-width="0" data-height="0"><?php esc_html_e('Do not resize', 'shrinkwell-image-optimizer'); ?></button>
                        <button type="button" class="button button-primary" data-width="2560" data-height="0"><?php esc_html_e('Recommended', 'shrinkwell-image-optimizer'); ?></button>
                        <button type="button" class="button" data-width="3840" data-height="0"><?php esc_html_e('Very large screens', 'shrinkwell-image-optimizer'); ?></button>
                    </div>
                    <p class="description"><?php esc_html_e('Recommended keeps images large enough for modern layouts while avoiding oversized camera uploads. Height can stay unlimited so images keep their natural proportions.', 'shrinkwell-image-optimizer'); ?></p>

                    <div class="shrinkwell-grid">
                        <label>
                            <span>
                                <?php esc_html_e('Maximum width in pixels', 'shrinkwell-image-optimizer'); ?>
                                <?php echo wp_kses($this->help_tip(__('Images wider than this can be resized during optimization. 2560 px is a good default for most modern websites.', 'shrinkwell-image-optimizer')), $this->help_tip_allowed_html()); ?>
                            </span>
                            <input id="shrinkwell-max-width" type="number" min="0" name="<?php echo esc_attr(self::OPTION); ?>[max_width]" value="<?php echo esc_attr((int) $settings['max_width']); ?>">
                            <small><?php esc_html_e('Use 2560 for most sites. Use 0 only if you never want Shrinkwell to resize width.', 'shrinkwell-image-optimizer'); ?></small>
                        </label>
                        <label>
                            <span>
                                <?php esc_html_e('Maximum height in pixels', 'shrinkwell-image-optimizer'); ?>
                                <?php echo wp_kses($this->help_tip(__('Leave this at 0 unless the site has a specific height limit. A height limit can unexpectedly resize tall images.', 'shrinkwell-image-optimizer')), $this->help_tip_allowed_html()); ?>
                            </span>
                            <input id="shrinkwell-max-height" type="number" min="0" name="<?php echo esc_attr(self::OPTION); ?>[max_height]" value="<?php echo esc_attr((int) $settings['max_height']); ?>">
                            <small><?php esc_html_e('Usually leave this at 0. That keeps the image ratio natural and avoids unexpected cropping.', 'shrinkwell-image-optimizer'); ?></small>
                        </label>
                        <label>
                            <span>
                                <?php esc_html_e('Maximum file size to optimize', 'shrinkwell-image-optimizer'); ?>
                                <?php echo wp_kses($this->help_tip(__('Shrinkwell skips files above this size to protect shared hosting from timeouts. Raise it only when PHP memory is strong enough.', 'shrinkwell-image-optimizer')), $this->help_tip_allowed_html()); ?>
                            </span>
                            <input type="number" min="1" max="100" name="<?php echo esc_attr(self::OPTION); ?>[max_file_size_mb]" value="<?php echo esc_attr((int) $settings['max_file_size_mb']); ?>">
                            <small><?php esc_html_e('Recommended start for shared hosting: 10 MB. Shrinkwell also checks pixel dimensions, because very large images can use much more memory after PHP opens them.', 'shrinkwell-image-optimizer'); ?></small>
                        </label>
                    </div>
                <?php endif; ?>

                <?php submit_button(__('Save settings', 'shrinkwell-image-optimizer')); ?>
            </form>
            <?php endif; ?>

            <?php if ('bulk' === $active_tab) : ?>
                <section class="shrinkwell-panel shrinkwell-bulk-panel" id="shrinkwell-bulk-flow" data-state="idle">
                    <div class="shrinkwell-panel-header shrinkwell-bulk-header">
                        <div>
                            <h2>
                                <?php esc_html_e('Media Optimization', 'shrinkwell-image-optimizer'); ?>
                                <?php echo wp_kses($this->help_tip(__('This optimizes existing media library images in small browser-driven batches. Keep the page open while a run is active.', 'shrinkwell-image-optimizer')), $this->help_tip_allowed_html()); ?>
                            </h2>
                        </div>
                        <span class="shrinkwell-bulk-state" aria-live="polite">
                            <span aria-hidden="true"></span>
                            <?php esc_html_e('Ready', 'shrinkwell-image-optimizer'); ?>
                        </span>
                    </div>
                    <?php if ($this->after_restore_guide_requested()) : ?>
                        <div class="shrinkwell-after-restore-guide">
                            <strong><?php esc_html_e('Restore complete. Rebuild WebP delivery files next.', 'shrinkwell-image-optimizer'); ?></strong>
                            <span><?php esc_html_e('Backups restore the original images and remove generated WebP/AVIF delivery files. Rebuild WebP files before running Page Proof again.', 'shrinkwell-image-optimizer'); ?></span>
                            <button type="button" class="button button-primary shrinkwell-trigger-webp-rebuild">
                                <?php esc_html_e('Rebuild WebP delivery files', 'shrinkwell-image-optimizer'); ?>
                            </button>
                            <span class="shrinkwell-webp-rebuild-status" hidden aria-live="polite"></span>
                        </div>
                    <?php endif; ?>
                    <div class="shrinkwell-bulk-summary shrinkwell-bulk-summary-cards">
                        <span class="shrinkwell-bulk-card-total"><strong><?php echo esc_html(number_format_i18n((int) $media_counts['total'])); ?></strong> <?php esc_html_e('images total', 'shrinkwell-image-optimizer'); ?></span>
                        <span class="shrinkwell-bulk-card-ready"><strong><?php echo esc_html(number_format_i18n($ready_to_optimize)); ?></strong> <?php esc_html_e('ready for run', 'shrinkwell-image-optimizer'); ?></span>
                        <span class="shrinkwell-bulk-card-done"><strong><?php echo esc_html(number_format_i18n($already_optimized_count)); ?></strong> <?php esc_html_e('already optimized', 'shrinkwell-image-optimizer'); ?></span>
                        <span class="shrinkwell-bulk-card-saved" hidden><strong id="shrinkwell-run-saved"><?php esc_html_e('0 B', 'shrinkwell-image-optimizer'); ?></strong> <?php esc_html_e('saved this run', 'shrinkwell-image-optimizer'); ?></span>
                        <span class="shrinkwell-bulk-card-skipped" hidden><strong id="shrinkwell-run-error-count"><?php esc_html_e('0', 'shrinkwell-image-optimizer'); ?></strong> <?php esc_html_e('skipped this run', 'shrinkwell-image-optimizer'); ?></span>
                    </div>

                    <?php if ($demo_preview) : ?>
                        <?php $this->render_demo_optimization_examples(); ?>
                    <?php endif; ?>

                    <div class="shrinkwell-flow-nav" aria-label="<?php esc_attr_e('Media optimization progress', 'shrinkwell-image-optimizer'); ?>">
                        <span class="shrinkwell-flow-dot is-active" aria-hidden="true"></span>
                        <span class="shrinkwell-flow-dot" aria-hidden="true"></span>
                        <span class="shrinkwell-flow-dot" aria-hidden="true"></span>
                        <span class="shrinkwell-flow-dot" aria-hidden="true"></span>
                        <strong id="shrinkwell-flow-label"><?php esc_html_e('Test 5 images first', 'shrinkwell-image-optimizer'); ?></strong>
                    </div>

                    <p class="shrinkwell-running-note shrinkwell-start-warning shrinkwell-bulk-running-notice">
                        <?php esc_html_e('Shrinkwell is running - keep this tab open to track progress. You can still use other browser tabs.', 'shrinkwell-image-optimizer'); ?>
                    </p>
                    <p class="description shrinkwell-start-context">
                        <?php esc_html_e('Shrinkwell tests 5 images first so you can review quality, server response, and WebP/AVIF output before processing the full library.', 'shrinkwell-image-optimizer'); ?>
                    </p>

                    <div class="shrinkwell-bulk-actions">
                        <button type="button" class="button button-primary" id="shrinkwell-start-test-bulk">
                            <?php esc_html_e('Test 5 images', 'shrinkwell-image-optimizer'); ?>
                        </button>
                        <button type="button" class="button" id="shrinkwell-start-bulk">
                            <?php esc_html_e('Optimize all', 'shrinkwell-image-optimizer'); ?>
                        </button>
                        <button type="button" class="button" id="shrinkwell-pause-bulk">
                            <?php esc_html_e('Pause', 'shrinkwell-image-optimizer'); ?>
                        </button>
                    </div>

                    <div id="shrinkwell-bulk-next-step" class="shrinkwell-bulk-next-step" hidden></div>
                    <?php $this->render_bulk_inline_issue_log(); ?>

                    <div class="shrinkwell-flow-testing" aria-live="polite">
                        <div class="shrinkwell-flow-spinner" aria-hidden="true"></div>
                        <strong><?php esc_html_e('Testing 5 images...', 'shrinkwell-image-optimizer'); ?></strong>
                        <span><?php esc_html_e('Checking quality, server response, and modern image output', 'shrinkwell-image-optimizer'); ?></span>
                        <div class="shrinkwell-progress-bar"><span></span></div>
                    </div>

                    <div class="shrinkwell-completion-banner" id="shrinkwell-completion-banner" hidden>
                        <strong><?php esc_html_e('Optimization complete', 'shrinkwell-image-optimizer'); ?></strong>
                        <span id="shrinkwell-completion-summary"></span>
                    </div>

                    <div class="shrinkwell-bulk-proof">
                        <div>
                            <strong><?php esc_html_e('Check WebP/AVIF delivery too? Recommended.', 'shrinkwell-image-optimizer'); ?></strong>
                            <span><?php esc_html_e('Scan a real frontend page to confirm modern image delivery is visible in the page HTML.', 'shrinkwell-image-optimizer'); ?></span>
                        </div>
                        <div class="shrinkwell-bulk-proof-actions">
                            <label class="screen-reader-text" for="shrinkwell-proof-url"><?php esc_html_e('Page URL to scan', 'shrinkwell-image-optimizer'); ?></label>
                            <input type="url" id="shrinkwell-proof-url" class="regular-text" value="<?php echo esc_url(home_url('/')); ?>" placeholder="<?php echo esc_attr(home_url('/codex/')); ?>">
                            <button type="button" class="button" id="shrinkwell-run-frontend-proof-scan">
                                <?php esc_html_e('Check page', 'shrinkwell-image-optimizer'); ?>
                            </button>
                        </div>
                    </div>
                    <div id="shrinkwell-frontend-proof-results" class="shrinkwell-test-results" hidden></div>
                    <button type="button" class="button-link" id="shrinkwell-new-run">
                        <?php esc_html_e('Start a new run', 'shrinkwell-image-optimizer'); ?>
                    </button>

                    <p class="shrinkwell-bulk-queue-note"><?php esc_html_e('Shrinkwell only queues files that fit the current local processing limits. Large files are skipped with details in the log instead of risking a server timeout.', 'shrinkwell-image-optimizer'); ?></p>
                    <p class="description shrinkwell-bulk-webp-note">
                        <?php esc_html_e('Use WebP rebuild after restoring backups. It recreates missing WebP delivery files without recompressing the original JPG/PNG images. Existing WebP images do not need another WebP copy.', 'shrinkwell-image-optimizer'); ?>
                    </p>

                    <?php if ($too_large_to_optimize > 0) : ?>
                        <div class="shrinkwell-bulk-limit-note">
                            <strong>
                                <?php
                                echo esc_html(
                                    sprintf(
                                        /* translators: %s: Number of images. */
                                        _n('%s image needs resizing first.', '%s images need resizing first.', $too_large_to_optimize, 'shrinkwell-image-optimizer'),
                                        number_format_i18n($too_large_to_optimize)
                                    )
                                );
                                ?>
                            </strong>
                            <span><?php esc_html_e('These files are above the safe local processing limit for this hosting setup, so Shrinkwell will skip them instead of risking a timeout.', 'shrinkwell-image-optimizer'); ?></span>
                            <a href="<?php echo esc_url($this->tab_url('advanced')); ?>"><?php esc_html_e('Review limits', 'shrinkwell-image-optimizer'); ?></a>
                        </div>
                    <?php endif; ?>

                    <div class="shrinkwell-progress" hidden>
                        <div class="shrinkwell-bulk-meter" id="shrinkwell-bulk-meter">
                            <strong id="shrinkwell-progress-percent"><?php esc_html_e('0%', 'shrinkwell-image-optimizer'); ?></strong>
                            <span id="shrinkwell-progress-label"><?php esc_html_e('Ready', 'shrinkwell-image-optimizer'); ?></span>
                        </div>
                        <div class="shrinkwell-progress-main">
                            <p id="shrinkwell-progress-text"></p>
                            <p class="shrinkwell-current-image" id="shrinkwell-current-image" hidden></p>
                            <p class="description" id="shrinkwell-progress-detail"></p>
                            <div class="shrinkwell-progress-bar"><span></span></div>
                            <div class="shrinkwell-run-stats">
                                <span><strong id="shrinkwell-run-done-count"><?php esc_html_e('0', 'shrinkwell-image-optimizer'); ?></strong><?php esc_html_e('Done', 'shrinkwell-image-optimizer'); ?></span>
                                <span><strong class="shrinkwell-run-skipped-value"><?php esc_html_e('0', 'shrinkwell-image-optimizer'); ?></strong><?php esc_html_e('Skipped', 'shrinkwell-image-optimizer'); ?></span>
                                <span class="shrinkwell-run-saved-stat" hidden><strong class="shrinkwell-run-saved-value"><?php esc_html_e('0 B', 'shrinkwell-image-optimizer'); ?></strong><?php esc_html_e('Saved', 'shrinkwell-image-optimizer'); ?></span>
                            </div>
                        </div>
                        <div class="shrinkwell-run-errors" id="shrinkwell-run-errors" hidden>
                            <strong><?php esc_html_e('Skipped images in this run', 'shrinkwell-image-optimizer'); ?></strong>
                            <ul></ul>
                        </div>
                    </div>
                </section>

                <?php if (current_user_can('manage_options')) : ?>
                    <details class="shrinkwell-panel shrinkwell-zero-savings-panel" data-total-images="<?php echo esc_attr((string) $already_optimized_count); ?>">
                        <summary>
                            <span><?php esc_html_e('Zero savings analysis', 'shrinkwell-image-optimizer'); ?></span>
                        </summary>
                        <p><?php esc_html_e('Find processed images that saved nothing or almost nothing, then decide whether to retry or dismiss them from this report.', 'shrinkwell-image-optimizer'); ?></p>
                        <p class="description"><?php esc_html_e('Large PNG files are flagged separately because they can still save a lot through WebP/AVIF delivery or a stronger re-optimization, even when no resize is needed.', 'shrinkwell-image-optimizer'); ?></p>
                        <div class="shrinkwell-zero-savings-actions">
                            <button type="button" class="button button-primary" id="shrinkwell-scan-zero-savings">
                                <?php esc_html_e('Scan for zero savings', 'shrinkwell-image-optimizer'); ?>
                            </button>
                            <span class="spinner" id="shrinkwell-zero-savings-spinner"></span>
                            <span id="shrinkwell-zero-savings-status" aria-live="polite"></span>
                        </div>
                        <div id="shrinkwell-zero-savings-results" class="shrinkwell-zero-savings-results" hidden></div>
                    </details>
                <?php endif; ?>

                <section class="shrinkwell-panel">
                    <h2><?php esc_html_e('Log', 'shrinkwell-image-optimizer'); ?></h2>
                    <?php $this->render_bulk_log_table(); ?>
                </section>
            <?php endif; ?>

            <?php if ('seo' === $active_tab) : ?>
                <section class="shrinkwell-panel">
                    <h2><?php esc_html_e('Image SEO check', 'shrinkwell-image-optimizer'); ?></h2>
                    <p><?php esc_html_e('Shrinkwell checks image SEO signals such as missing alt text and weak filenames. It reports issues only and does not rewrite content automatically.', 'shrinkwell-image-optimizer'); ?></p>
                    <div class="shrinkwell-profile-help">
                        <strong><?php esc_html_e('What to improve first', 'shrinkwell-image-optimizer'); ?></strong>
                        <p><?php esc_html_e('Start with visible images in headers, service pages, product pages, and portfolio pages. Shrinkwell only shows a one-click suggestion when it has a safe source. If a filename is random, write a real description instead of reusing the filename.', 'shrinkwell-image-optimizer'); ?></p>
                    </div>
                    <button type="button" class="button button-primary" id="shrinkwell-run-seo-scan">
                        <?php esc_html_e('Scan image SEO', 'shrinkwell-image-optimizer'); ?>
                    </button>
                    <div id="shrinkwell-seo-results" class="shrinkwell-test-results shrinkwell-seo-results" hidden></div>
                </section>
            <?php endif; ?>

            <?php if ('media-cleanup' === $active_tab) : ?>
                <?php $media_cleanup->render_tab(); ?>
            <?php endif; ?>

            <?php if ('backups' === $active_tab && is_array($backup_inventory)) : ?>
                <section class="shrinkwell-panel">
                    <h2><?php esc_html_e('Backup storage', 'shrinkwell-image-optimizer'); ?></h2>
                    <p><?php esc_html_e('Restore originals or free up storage from old Shrinkwell backup files.', 'shrinkwell-image-optimizer'); ?></p>
                    <div class="shrinkwell-bulk-summary">
                        <span><strong><?php echo esc_html(number_format_i18n((int) $backup_inventory['images'])); ?></strong> <?php esc_html_e('images with backups', 'shrinkwell-image-optimizer'); ?></span>
                        <span><strong><?php echo esc_html(number_format_i18n((int) $backup_inventory['files'])); ?></strong> <?php esc_html_e('original files', 'shrinkwell-image-optimizer'); ?></span>
                        <span><strong><?php echo esc_html(size_format((int) $backup_inventory['bytes'], 1)); ?></strong> <?php esc_html_e('original storage', 'shrinkwell-image-optimizer'); ?></span>
                    </div>

                    <div class="shrinkwell-backup-actions-grid">
                        <div class="shrinkwell-backup-action-card">
                            <h3><?php esc_html_e('Restore originals', 'shrinkwell-image-optimizer'); ?></h3>
                            <strong class="shrinkwell-backup-action-number"><?php echo esc_html(number_format_i18n((int) $backup_inventory['images'])); ?></strong>
                            <span><?php esc_html_e('restorable images', 'shrinkwell-image-optimizer'); ?></span>
                            <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
                                <?php wp_nonce_field('shrinkwell_restore_all_backups'); ?>
                                <input type="hidden" name="action" value="shrinkwell_restore_all_backups">
                                <button type="submit" class="button button-secondary shrinkwell-confirm-restore-all" <?php disabled(empty($backup_inventory['images'])); ?>><?php esc_html_e('Restore all originals', 'shrinkwell-image-optimizer'); ?></button>
                            </form>
                            <p class="description"><?php esc_html_e('After restoring, run WebP rebuild before checking Page Proof again.', 'shrinkwell-image-optimizer'); ?></p>
                        </div>
                        <div class="shrinkwell-backup-action-card">
                            <h3><?php esc_html_e('Free up backup storage', 'shrinkwell-image-optimizer'); ?></h3>
                            <p><?php esc_html_e('Shrinkwell checks every backup first. Only backups older than 30 days are eligible when the optimized image still exists and is smaller.', 'shrinkwell-image-optimizer'); ?></p>
                            <button type="button" class="button button-secondary" id="shrinkwell-check-old-backups" data-days="30" <?php disabled(empty($backup_inventory['files'])); ?>><?php esc_html_e('Check safe backups', 'shrinkwell-image-optimizer'); ?></button>
                            <div class="shrinkwell-backup-check-result" id="shrinkwell-backup-check-result" hidden>
                                <strong id="shrinkwell-backup-check-summary"></strong>
                                <button type="button" class="button shrinkwell-button-danger" id="shrinkwell-delete-checked-backups"><?php esc_html_e('Delete these backups', 'shrinkwell-image-optimizer'); ?></button>
                            </div>
                            <span id="shrinkwell-backup-cleanup-status" aria-live="polite"></span>
                            <p class="description"><?php esc_html_e('After deletion, those originals can no longer be restored.', 'shrinkwell-image-optimizer'); ?></p>
                        </div>
                    </div>
                </section>

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

                <section class="shrinkwell-panel">
                    <h2><?php esc_html_e('Available backups', 'shrinkwell-image-optimizer'); ?></h2>
                    <p class="shrinkwell-backup-note">
                        <?php esc_html_e('Savings shown here are for the main file only. WebP and AVIF versions stored separately add additional size reduction.', 'shrinkwell-image-optimizer'); ?>
                    </p>
                    <?php $this->render_backup_table($backup_inventory['rows']); ?>
                </section>
            <?php endif; ?>

            <?php if ('pro' === $active_tab) : ?>
                <?php $this->render_pro_tab($stats, $server_status); ?>
                <?php $this->render_review_notice(); ?>
            <?php endif; ?>
        </div>
        <?php
    }

    private function render_pro_tab(array $stats, array $server_status): void
    {
        $usage_tracker = $this->usage_tracker();
        $limit_bytes = $usage_tracker && method_exists($usage_tracker, 'get_monthly_limit_bytes') ? (int) $usage_tracker->get_monthly_limit_bytes() : 60 * 1024 * 1024;
        $used_bytes = $usage_tracker && method_exists($usage_tracker, 'get_current_usage_bytes') ? (int) $usage_tracker->get_current_usage_bytes() : 0;
        $remaining_bytes = max(0, $limit_bytes - $used_bytes);
        $usage_percent = $limit_bytes > 0 ? min(100, (int) round(($used_bytes / $limit_bytes) * 100)) : 0;
        $limit_label = size_format($limit_bytes, 0);
        $used_label = size_format($used_bytes, 1);
        $remaining_label = size_format($remaining_bytes, 1);
        $optimized_images = (int) ($stats['optimized_images'] ?? 0);
        $saved_bytes = (int) ($stats['saved_bytes'] ?? 0);
        $saved_label = $saved_bytes > 0 ? size_format($saved_bytes, 1) : __('Available after first optimization', 'shrinkwell-image-optimizer');
        $optimized_label = $optimized_images > 0 ? number_format_i18n($optimized_images) : __('Available after first optimization', 'shrinkwell-image-optimizer');
        $is_pro = shrinkwell_is_pro();
        $account_url = $this->account_url();
        $license_url = $this->tab_url('pro') . '#shrinkwell-license-form';
        $media_cleanup_url = $this->tab_url('media-cleanup');

        $benefits = [
            [
                'icon' => 'infinity',
                'title' => __('Unlimited optimization', 'shrinkwell-image-optimizer'),
                'copy' => __('Optimize large media libraries without waiting for next month’s free limit.', 'shrinkwell-image-optimizer'),
            ],
            [
                'icon' => 'image',
                'title' => __('AVIF when supported', 'shrinkwell-image-optimizer'),
                'copy' => __('Serve smaller next-gen images when your server passes safe AVIF encoder checks.', 'shrinkwell-image-optimizer'),
            ],
            [
                'icon' => 'upload',
                'title' => __('Automatic upload optimization', 'shrinkwell-image-optimizer'),
                'copy' => __('New uploads can be optimized automatically, so client sites stay lean.', 'shrinkwell-image-optimizer'),
            ],
            [
                'icon' => 'shield',
                'title' => __('Safe Media Cleanup', 'shrinkwell-image-optimizer'),
                'copy' => __('Review possibly unused media and reduce WordPress backup size without blind deletion.', 'shrinkwell-image-optimizer'),
            ],
        ];

        $comparison_rows = [
            [
                'feature' => __('Monthly optimization', 'shrinkwell-image-optimizer'),
                'free' => ['label' => __('60 MB per month', 'shrinkwell-image-optimizer'), 'type' => 'limited'],
                'pro' => ['label' => __('Unlimited', 'shrinkwell-image-optimizer'), 'type' => 'included'],
            ],
            [
                'feature' => __('WebP generation and delivery', 'shrinkwell-image-optimizer'),
                'free' => ['label' => __('Included', 'shrinkwell-image-optimizer'), 'type' => 'included'],
                'pro' => ['label' => __('Included', 'shrinkwell-image-optimizer'), 'type' => 'included'],
            ],
            [
                'feature' => __('AVIF conversion and delivery', 'shrinkwell-image-optimizer'),
                'free' => ['label' => __('Server check only', 'shrinkwell-image-optimizer'), 'type' => 'partial'],
                'pro' => ['label' => __('Included when your server supports safe AVIF encoding', 'shrinkwell-image-optimizer'), 'type' => 'included'],
            ],
            [
                'feature' => __('Background optimization', 'shrinkwell-image-optimizer'),
                'free' => ['label' => __('Locked', 'shrinkwell-image-optimizer'), 'type' => 'locked'],
                'pro' => ['label' => __('Included', 'shrinkwell-image-optimizer'), 'type' => 'included'],
            ],
            [
                'feature' => __('Re-optimize after settings change', 'shrinkwell-image-optimizer'),
                'free' => ['label' => __('Locked', 'shrinkwell-image-optimizer'), 'type' => 'locked'],
                'pro' => ['label' => __('Included', 'shrinkwell-image-optimizer'), 'type' => 'included'],
            ],
            [
                'feature' => __('Auto-optimize new uploads', 'shrinkwell-image-optimizer'),
                'free' => ['label' => __('Locked', 'shrinkwell-image-optimizer'), 'type' => 'locked'],
                'pro' => ['label' => __('Included', 'shrinkwell-image-optimizer'), 'type' => 'included'],
            ],
            [
                'feature' => __('Media Cleanup', 'shrinkwell-image-optimizer'),
                'free' => ['label' => __('Preview only', 'shrinkwell-image-optimizer'), 'type' => 'partial'],
                'pro' => ['label' => __('Safe cleanup actions included', 'shrinkwell-image-optimizer'), 'type' => 'included'],
            ],
            [
                'feature' => __('Backups and restore', 'shrinkwell-image-optimizer'),
                'free' => ['label' => __('Included', 'shrinkwell-image-optimizer'), 'type' => 'included'],
                'pro' => ['label' => __('Included', 'shrinkwell-image-optimizer'), 'type' => 'included'],
            ],
            [
                'feature' => __('Local processing', 'shrinkwell-image-optimizer'),
                'free' => ['label' => __('Included', 'shrinkwell-image-optimizer'), 'type' => 'included'],
                'pro' => ['label' => __('Included', 'shrinkwell-image-optimizer'), 'type' => 'included'],
            ],
        ];
        ?>
        <div class="shrinkwell-pro-page <?php echo esc_attr($is_pro ? 'is-active-account' : 'is-upgrade'); ?>">
            <section class="shrinkwell-pro-hero">
                <div class="shrinkwell-pro-hero__icon" aria-hidden="true"></div>
                <div class="shrinkwell-pro-hero__copy">
                    <h2><?php echo esc_html($is_pro ? __('Shrinkwell Pro is active', 'shrinkwell-image-optimizer') : __('Unlock unlimited optimization with Shrinkwell Pro', 'shrinkwell-image-optimizer')); ?></h2>
                    <p><?php echo esc_html($is_pro ? __('Unlimited optimization and safe Media Cleanup are enabled on this site.', 'shrinkwell-image-optimizer') : __('Remove the 60 MB monthly limit, enable AVIF, automate new uploads, and clean up media safely.', 'shrinkwell-image-optimizer')); ?></p>
                    <p class="shrinkwell-pro-trust"><span aria-hidden="true">✓</span> <?php esc_html_e('Images are optimized locally. No external compression API.', 'shrinkwell-image-optimizer'); ?></p>
                </div>
                <div class="shrinkwell-pro-hero__action">
                    <p>
                        <strong><?php echo esc_html($is_pro ? __('Pro plan active', 'shrinkwell-image-optimizer') : __('Free plan', 'shrinkwell-image-optimizer')); ?></strong>
                        <?php if ($is_pro) : ?>
                            <?php esc_html_e('Your license is active. Manage account details and billing through Freemius.', 'shrinkwell-image-optimizer'); ?>
                        <?php else : ?>
                            <?php
                            echo esc_html(
                                sprintf(
                                    /* translators: 1: Current monthly processed media amount. 2: Monthly usage limit. */
                                    __('%1$s of %2$s processed this month', 'shrinkwell-image-optimizer'),
                                    $used_label,
                                    $limit_label
                                )
                            );
                            ?>
                        <?php endif; ?>
                    </p>
                    <?php if (!$is_pro) : ?>
                        <div class="shrinkwell-pro-meter" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="<?php echo esc_attr((string) $usage_percent); ?>">
                            <span style="width: <?php echo esc_attr((string) $usage_percent); ?>%"></span>
                        </div>
                    <?php endif; ?>
                    <div class="shrinkwell-pro-hero__buttons">
                        <?php if ($is_pro) : ?>
                            <a class="button button-primary" href="<?php echo esc_url($account_url); ?>"><?php esc_html_e('Manage account', 'shrinkwell-image-optimizer'); ?></a>
                            <a class="button button-secondary" href="<?php echo esc_url($media_cleanup_url); ?>"><?php esc_html_e('Open Media Cleanup', 'shrinkwell-image-optimizer'); ?></a>
                        <?php else : ?>
                            <a class="button button-primary" href="<?php echo esc_url(shrinkwell_get_upgrade_url()); ?>"><?php esc_html_e('Get Shrinkwell Pro', 'shrinkwell-image-optimizer'); ?></a>
                            <a class="button button-secondary" href="<?php echo esc_url($license_url); ?>"><?php esc_html_e('Enter license code', 'shrinkwell-image-optimizer'); ?></a>
                        <?php endif; ?>
                    </div>
                </div>
            </section>

            <section class="shrinkwell-pro-stat-grid" aria-label="<?php esc_attr_e('Current plan status', 'shrinkwell-image-optimizer'); ?>">
                <article class="shrinkwell-pro-stat-card">
                    <span class="shrinkwell-pro-mini-icon is-limit" aria-hidden="true"></span>
                    <div><small><?php esc_html_e('Monthly limit', 'shrinkwell-image-optimizer'); ?></small><strong><?php echo esc_html($is_pro ? __('Unlimited', 'shrinkwell-image-optimizer') : $limit_label); ?></strong><em><?php echo esc_html($is_pro ? __('Pro plan', 'shrinkwell-image-optimizer') : __('per month', 'shrinkwell-image-optimizer')); ?></em></div>
                </article>
                <article class="shrinkwell-pro-stat-card">
                    <span class="shrinkwell-pro-mini-icon is-used" aria-hidden="true"></span>
                    <div><small><?php esc_html_e('Processed this month', 'shrinkwell-image-optimizer'); ?></small><strong><?php echo esc_html($is_pro ? __('Unlimited', 'shrinkwell-image-optimizer') : $used_label); ?></strong><em><?php echo esc_html($is_pro ? __('No monthly cap', 'shrinkwell-image-optimizer') : sprintf(
                        /* translators: %s: Remaining monthly usage. */
                        __('%s remaining', 'shrinkwell-image-optimizer'),
                        $remaining_label
                    )); ?></em></div>
                </article>
                <article class="shrinkwell-pro-stat-card">
                    <span class="shrinkwell-pro-mini-icon is-images" aria-hidden="true"></span>
                    <div><small><?php esc_html_e('All-time optimized images', 'shrinkwell-image-optimizer'); ?></small><strong><?php echo esc_html($optimized_label); ?></strong><em><?php esc_html_e('images', 'shrinkwell-image-optimizer'); ?></em></div>
                </article>
                <article class="shrinkwell-pro-stat-card">
                    <span class="shrinkwell-pro-mini-icon is-savings" aria-hidden="true"></span>
                    <div><small><?php esc_html_e('Total saved space', 'shrinkwell-image-optimizer'); ?></small><strong><?php echo esc_html($saved_label); ?></strong><em><?php esc_html_e('across your site', 'shrinkwell-image-optimizer'); ?></em></div>
                </article>
            </section>

            <div class="shrinkwell-pro-main-grid">
                <section class="shrinkwell-panel shrinkwell-pro-comparison shrinkwell-pro-comparison-card">
                    <h2><?php echo esc_html($is_pro ? __('Plan features', 'shrinkwell-image-optimizer') : __('Free vs Pro', 'shrinkwell-image-optimizer')); ?></h2>
                    <table>
                        <colgroup>
                            <col>
                            <col>
                            <col>
                        </colgroup>
                        <thead>
                            <tr>
                                <th scope="col"><?php esc_html_e('Feature', 'shrinkwell-image-optimizer'); ?></th>
                                <th scope="col"><?php esc_html_e('Free', 'shrinkwell-image-optimizer'); ?></th>
                                <th scope="col"><span class="shrinkwell-plan-title"><?php esc_html_e('Pro', 'shrinkwell-image-optimizer'); ?></span><span class="shrinkwell-plan-badge"><?php esc_html_e('Best value', 'shrinkwell-image-optimizer'); ?></span></th>
                            </tr>
                        </thead>
                        <tbody>
                            <?php foreach ($comparison_rows as $row) : ?>
                                <tr>
                                    <th scope="row"><?php echo esc_html((string) $row['feature']); ?></th>
                                    <td>
                                        <span class="shrinkwell-pro-feature is-<?php echo esc_attr((string) $row['free']['type']); ?>">
                                            <span aria-hidden="true"></span>
                                            <?php echo esc_html((string) $row['free']['label']); ?>
                                        </span>
                                    </td>
                                    <td>
                                        <span class="shrinkwell-pro-feature is-<?php echo esc_attr((string) $row['pro']['type']); ?>">
                                            <span aria-hidden="true"></span>
                                            <?php echo esc_html((string) $row['pro']['label']); ?>
                                        </span>
                                    </td>
                                </tr>
                            <?php endforeach; ?>
                        </tbody>
                    </table>
                </section>

                <section class="shrinkwell-panel shrinkwell-pro-benefits">
                    <h2><?php echo esc_html($is_pro ? __('Pro benefits active', 'shrinkwell-image-optimizer') : __('Why upgrade to Pro?', 'shrinkwell-image-optimizer')); ?></h2>
                    <div class="shrinkwell-benefit-grid">
                    <?php foreach ($benefits as $benefit) : ?>
                        <article class="shrinkwell-pro-benefit shrinkwell-benefit-card">
                            <span class="shrinkwell-pro-benefit__icon shrinkwell-benefit-icon is-<?php echo esc_attr((string) $benefit['icon']); ?>" aria-hidden="true"></span>
                            <div>
                                <h3><?php echo esc_html((string) $benefit['title']); ?></h3>
                                <p><?php echo esc_html((string) $benefit['copy']); ?></p>
                            </div>
                        </article>
                    <?php endforeach; ?>
                    </div>
                </section>
            </div>

            <div class="shrinkwell-pro-bottom-grid">
                <section class="shrinkwell-panel shrinkwell-pro-cleanup shrinkwell-cleanup-upsell">
                    <span class="shrinkwell-pro-cleanup__icon" aria-hidden="true"></span>
                    <div>
                        <h2><?php echo esc_html($is_pro ? __('Media Cleanup is unlocked', 'shrinkwell-image-optimizer') : __('Clean up media before it bloats your backups', 'shrinkwell-image-optimizer')); ?></h2>
                        <p><?php echo esc_html($is_pro ? __('Review possibly unused media, move items to Shrinkwell Trash, restore safely, and permanently delete only after confirmation.', 'shrinkwell-image-optimizer') : __('Shrinkwell can help identify possibly unused media and safely guide cleanup, so backups stay smaller and easier to manage.', 'shrinkwell-image-optimizer')); ?></p>
                        <ul>
                            <li><?php esc_html_e('Review before deleting', 'shrinkwell-image-optimizer'); ?></li>
                            <li><?php echo esc_html($is_pro ? __('Move to Shrinkwell Trash first', 'shrinkwell-image-optimizer') : __('Reduce storage and backup size', 'shrinkwell-image-optimizer')); ?></li>
                            <li><?php echo esc_html($is_pro ? __('Restore before permanent deletion', 'shrinkwell-image-optimizer') : __('Keep restore safety in mind', 'shrinkwell-image-optimizer')); ?></li>
                        </ul>
                        <a class="button button-primary" href="<?php echo esc_url($is_pro ? $media_cleanup_url : shrinkwell_get_upgrade_url()); ?>"><?php echo esc_html($is_pro ? __('Open Media Cleanup', 'shrinkwell-image-optimizer') : __('Unlock Media Cleanup with Pro', 'shrinkwell-image-optimizer')); ?></a>
                    </div>
                </section>

                <section class="shrinkwell-panel shrinkwell-pro-license-card">
                    <?php if ($is_pro) : ?>
                        <h2><?php esc_html_e('Account and license', 'shrinkwell-image-optimizer'); ?></h2>
                        <p><?php esc_html_e('Your Pro license is active on this site. Manage billing, license status and account details through Freemius.', 'shrinkwell-image-optimizer'); ?></p>
                        <a class="button button-secondary" href="<?php echo esc_url($account_url); ?>"><?php esc_html_e('Manage account', 'shrinkwell-image-optimizer'); ?></a>
                    <?php else : ?>
                        <h2><?php esc_html_e('Already purchased Pro?', 'shrinkwell-image-optimizer'); ?></h2>
                        <p><?php esc_html_e('Enter your license key to activate Pro on this site. Licensing and checkout are handled securely by Freemius.', 'shrinkwell-image-optimizer'); ?></p>
                        <form class="shrinkwell-license-form" id="shrinkwell-license-form">
                            <label for="shrinkwell-license-key"><?php esc_html_e('License key', 'shrinkwell-image-optimizer'); ?></label>
                            <input type="text" id="shrinkwell-license-key" autocomplete="off" placeholder="<?php esc_attr_e('Paste your Freemius license key', 'shrinkwell-image-optimizer'); ?>">
                            <p class="description"><?php esc_html_e('Use the full key that starts with sk_ from your Freemius purchase email or Licenses page. Do not use the numeric license ID from the Sites table.', 'shrinkwell-image-optimizer'); ?></p>
                            <button type="submit" class="button button-secondary"><?php esc_html_e('Activate Pro', 'shrinkwell-image-optimizer'); ?></button>
                            <span id="shrinkwell-license-status" class="shrinkwell-license-status" aria-live="polite"></span>
                        </form>
                    <?php endif; ?>
                </section>
            </div>
        </div>
        <?php
    }

    private function render_hidden_settings(array $settings, string $active_tab): void
    {
        $visible_keys = [
            'settings' => ['auto_optimize', 'compression_profile', 'quality', 'custom_strength', 'optimize_thumbnails', 'strip_exif', 'create_webp', 'serve_webp', 'create_avif', 'serve_avif'],
            'advanced' => ['backup_originals', 'max_width', 'max_height', 'max_file_size_mb'],
        ];
        $skip = $visible_keys[$active_tab] ?? [];

        foreach (self::default_settings() as $key => $default) {
            if (in_array($key, $skip, true)) {
                continue;
            }

            $value = $settings[$key] ?? $default;

            echo '<input type="hidden" name="' . esc_attr(self::OPTION . '[' . $key . ']') . '" value="' . esc_attr((string) $value) . '">';
        }
    }

    private function render_recent_error_summary(): void
    {
        $groups = $this->recent_error_groups();

        if (!$groups) {
            return;
        }

        echo '<div class="shrinkwell-error-summary">';
        echo '<div class="shrinkwell-error-summary-head">';
        echo '<h3>' . esc_html__('Recent safe skips and issues', 'shrinkwell-image-optimizer') . '</h3>';
        echo '<p>' . esc_html__('Shrinkwell continued the run. These notes explain the latest skipped or failed images in plain language.', 'shrinkwell-image-optimizer') . '</p>';
        echo '</div>';
        echo '<div class="shrinkwell-error-summary-grid">';

        foreach ($groups as $group) {
            echo '<div class="shrinkwell-error-summary-card shrinkwell-error-summary-' . esc_attr(sanitize_html_class((string) $group['key'])) . '">';
            echo '<strong class="shrinkwell-error-summary-count">';
            echo esc_html(
                sprintf(
                    /* translators: %s: Number of skipped images. */
                    _n('%s image', '%s images', (int) $group['count'], 'shrinkwell-image-optimizer'),
                    number_format_i18n((int) $group['count'])
                )
            );
            echo '</strong>';
            echo '<h4>' . esc_html((string) $group['label']) . '</h4>';
            echo '<p>' . esc_html((string) $group['detail']) . '</p>';
            echo '<p class="shrinkwell-error-summary-action">' . esc_html((string) $group['action']) . '</p>';

            if (!empty($group['samples'])) {
                echo '<ul>';
                foreach ((array) $group['samples'] as $sample) {
                    echo '<li>' . esc_html((string) $sample) . '</li>';
                }
                echo '</ul>';
            }

            if (!empty($group['url']) && !empty($group['button'])) {
                echo '<a class="button" href="' . esc_url((string) $group['url']) . '">' . esc_html((string) $group['button']) . '</a>';
            }

            echo '</div>';
        }

        echo '</div>';
        echo '</div>';
    }

    private function render_media_issue_center(): void
    {
        $logs = $this->recent_logs(250);

        if (!$logs) {
            return;
        }

        $buckets = [
            'action' => [
                'label' => __('Needs action', 'shrinkwell-image-optimizer'),
                'detail' => __('Fix these first. Something blocked restore, write access, or image processing.', 'shrinkwell-image-optimizer'),
                'items' => [],
            ],
            'safe' => [
                'label' => __('Skipped', 'shrinkwell-image-optimizer'),
                'detail' => __('Check with Page Proof if you are unsure.', 'shrinkwell-image-optimizer'),
                'items' => [],
            ],
            'done' => [
                'label' => __('Completed', 'shrinkwell-image-optimizer'),
                'detail' => __('Recent images that completed successfully.', 'shrinkwell-image-optimizer'),
                'items' => [],
            ],
        ];

        foreach ($logs as $log) {
            $status = (string) ($log['status'] ?? 'unknown');
            $bucket = 'done';

            if ('error' === $status) {
                $bucket = 'action';
            } elseif ('skipped' === $status) {
                $bucket = 'safe';
            }

            $buckets[$bucket]['items'][] = $log;
        }

        echo '<div class="shrinkwell-issue-center">';
        echo '<div class="shrinkwell-issue-center-head">';
        echo '<h3>' . esc_html__('Log summary', 'shrinkwell-image-optimizer') . '</h3>';
        echo '<p>' . esc_html__('Open the details only when action is needed or Page Proof reports something suspicious.', 'shrinkwell-image-optimizer') . '</p>';
        echo '</div>';
        echo '<div class="shrinkwell-issue-grid">';

        foreach ($buckets as $key => $bucket) {
            $items = (array) $bucket['items'];
            echo '<section class="shrinkwell-issue-card shrinkwell-issue-' . esc_attr($key) . '">';
            echo '<strong>' . esc_html(number_format_i18n(count($items))) . '</strong>';
            echo '<h4>' . esc_html((string) $bucket['label']) . '</h4>';
            echo '<p>' . esc_html((string) $bucket['detail']) . '</p>';

            if ($items) {
                echo '<ul>';
                foreach (array_slice($items, 0, 4) as $item) {
                    $title = $this->truncate_log_title(trim((string) ($item['title'] ?? '')) ?: __('Unknown image', 'shrinkwell-image-optimizer'));
                    $message = trim((string) ($item['message'] ?? ''));
                    $classification = in_array((string) ($item['status'] ?? ''), ['error', 'skipped'], true)
                        ? $this->classify_error_message($message)
                        : [];
                    echo '<li><span>' . esc_html($title) . '</span>';
                    if ('' !== $message) {
                        echo '<small>' . esc_html($message) . '</small>';
                    }
                    if (!empty($classification['action'])) {
                        echo '<small><strong>' . esc_html__('What to do:', 'shrinkwell-image-optimizer') . '</strong> ' . esc_html((string) $classification['action']) . '</small>';
                    }
                    echo '</li>';
                }
                echo '</ul>';
            } else {
                echo '<em>' . esc_html__('Nothing here right now.', 'shrinkwell-image-optimizer') . '</em>';
            }

            echo '</section>';
        }

        echo '</div>';
        echo '</div>';
    }

    private function recent_error_groups(): array
    {
        $logs = $this->recent_logs(250);
        $groups = [];

        foreach ($logs as $log) {
            $status = (string) ($log['status'] ?? '');

            if (!in_array($status, ['error', 'skipped'], true)) {
                continue;
            }

            $classification = $this->classify_error_message((string) ($log['message'] ?? ''));
            $key = (string) $classification['key'];

            if (!isset($groups[$key])) {
                $groups[$key] = array_merge($classification, [
                    'count' => 0,
                    'samples' => [],
                ]);
            }

            $groups[$key]['count']++;

            if (count($groups[$key]['samples']) < 3) {
                $title = trim((string) ($log['title'] ?? ''));
                $groups[$key]['samples'][] = $title ?: __('Unknown image', 'shrinkwell-image-optimizer');
            }
        }

        return $groups;
    }

    private function truncate_log_title(string $title): string
    {
        if (function_exists('mb_strlen') && function_exists('mb_substr')) {
            return mb_strlen($title) > 40 ? mb_substr($title, 0, 37) . '...' : $title;
        }

        return strlen($title) > 40 ? substr($title, 0, 37) . '...' : $title;
    }

    private function classify_error_message(string $message): array
    {
        $lower = strtolower($message);

        if (
            false !== strpos($lower, 'too large')
            || false !== strpos($lower, 'file-size')
            || false !== strpos($lower, 'file size')
            || false !== strpos($lower, 'dimensions')
            || false !== strpos($lower, 'memory')
            || false !== strpos($lower, 'server stopped')
            || false !== strpos($lower, 'timeout')
        ) {
            return [
                'key' => 'limits',
                'label' => __('Too large for local processing', 'shrinkwell-image-optimizer'),
                'detail' => __('These files exceed the current safe file-size, pixel, timeout, or PHP memory limits.', 'shrinkwell-image-optimizer'),
                'action' => __('Reduce the image size first, raise Advanced limits carefully, or ask the host for more PHP memory.', 'shrinkwell-image-optimizer'),
                'button' => __('Review limits', 'shrinkwell-image-optimizer'),
                'url' => $this->tab_url('advanced'),
            ];
        }

        if (
            false !== strpos($lower, 'missing')
            || false !== strpos($lower, 'not found')
            || false !== strpos($lower, 'could not be restored')
        ) {
            return [
                'key' => 'missing',
                'label' => __('Missing file or backup path', 'shrinkwell-image-optimizer'),
                'detail' => __('WordPress still has this media item, but one of the files on disk is missing or no longer matches the backup path.', 'shrinkwell-image-optimizer'),
                'action' => __('Open the media item, check the File URL and filename, then re-upload or remove the broken media item if the original file is gone.', 'shrinkwell-image-optimizer'),
                'button' => '',
                'url' => '',
            ];
        }

        if (
            false !== strpos($lower, 'permission')
            || false !== strpos($lower, 'writable')
            || false !== strpos($lower, 'write')
            || false !== strpos($lower, 'upload directory')
        ) {
            return [
                'key' => 'permissions',
                'label' => __('File permission issue', 'shrinkwell-image-optimizer'),
                'detail' => __('Shrinkwell could not write optimized files, backups, or next-gen files for these images.', 'shrinkwell-image-optimizer'),
                'action' => __('Check uploads folder permissions or ask the host to confirm WordPress can write to the uploads directory.', 'shrinkwell-image-optimizer'),
                'button' => '',
                'url' => '',
            ];
        }

        if (
            false !== strpos($lower, 'webp')
            || false !== strpos($lower, 'avif')
            || false !== strpos($lower, 'encoder')
            || false !== strpos($lower, 'gd')
            || false !== strpos($lower, 'imagick')
        ) {
            return [
                'key' => 'encoder',
                'label' => __('Modern format copy not needed or not worth keeping', 'shrinkwell-image-optimizer'),
                'detail' => __('Shrinkwell only keeps WebP/AVIF copies when they are useful. Existing modern files, unsupported encoders, or larger generated copies can be skipped safely.', 'shrinkwell-image-optimizer'),
                'action' => __('No action is needed unless Page Proof still reports important visible JPG/PNG images without modern delivery files.', 'shrinkwell-image-optimizer'),
                'button' => __('Review formats', 'shrinkwell-image-optimizer'),
                'url' => $this->tab_url('settings'),
            ];
        }

        if (
            false !== strpos($lower, 'unsupported')
            || false !== strpos($lower, 'mime')
            || false !== strpos($lower, 'not an image')
        ) {
            return [
                'key' => 'unsupported',
                'label' => __('Unsupported file type', 'shrinkwell-image-optimizer'),
                'detail' => __('These files are not supported by the local optimizer and were skipped safely.', 'shrinkwell-image-optimizer'),
                'action' => __('Use JPEG, PNG, WebP, or supported AVIF images for local optimization.', 'shrinkwell-image-optimizer'),
                'button' => '',
                'url' => '',
            ];
        }

        return [
            'key' => 'other',
            'label' => __('Other skipped images', 'shrinkwell-image-optimizer'),
            'detail' => __('Shrinkwell skipped these images and continued with the rest of the run.', 'shrinkwell-image-optimizer'),
            'action' => __('Open the full log below for the exact message and retry after checking the file.', 'shrinkwell-image-optimizer'),
            'button' => '',
            'url' => '',
        ];
    }

    private function render_bulk_log_table(): void
    {
        $logs = $this->recent_logs(250);

        if (!$logs) {
            echo '<p class="description">' . esc_html__('No media optimization log entries yet.', 'shrinkwell-image-optimizer') . '</p>';
            return;
        }

        $attention_logs = [];
        $action_logs = [];
        $skipped_logs = [];
        $completed_logs = [];

        foreach ($logs as $log) {
            $status = (string) ($log['status'] ?? 'unknown');

            if ('error' === $status) {
                $action_logs[] = $log;
                $attention_logs[] = $log;
            } elseif ('skipped' === $status) {
                $skipped_logs[] = $log;
                $attention_logs[] = $log;
            } else {
                $completed_logs[] = $log;
            }
        }

        echo '<details class="shrinkwell-log-drawer"' . ($action_logs ? ' open' : '') . '>';
        echo '<summary><span>' . esc_html__('View log', 'shrinkwell-image-optimizer') . '</span><strong>' . esc_html(number_format_i18n(count($logs))) . '</strong></summary>';
        $this->render_media_issue_center();

        if ($attention_logs) {
            $this->render_log_section(
                __('Needs action and skipped', 'shrinkwell-image-optimizer'),
                __('Skipped usually means Shrinkwell protected the server or the file did not need work.', 'shrinkwell-image-optimizer'),
                $attention_logs,
                20,
                false
            );
        }

        if ($completed_logs) {
            $this->render_log_section(
                __('Completed', 'shrinkwell-image-optimizer'),
                __('These images have been processed. You usually do not need to open this list.', 'shrinkwell-image-optimizer'),
                $completed_logs,
                24,
                false
            );
        }

        echo '</details>';
    }

    private function render_bulk_inline_issue_log(): void
    {
        $logs = $this->recent_logs(20);
        $attention_logs = [];

        foreach ($logs as $log) {
            if (in_array((string) ($log['status'] ?? ''), ['error', 'skipped'], true)) {
                $attention_logs[] = $log;
            }
        }

        if (!$attention_logs) {
            return;
        }

        echo '<div class="shrinkwell-bulk-mini-log">';
        echo '<strong>' . esc_html__('Recent issues from the last run', 'shrinkwell-image-optimizer') . '</strong>';
        echo '<ul>';

        foreach (array_slice($attention_logs, 0, 3) as $log) {
            $title = trim((string) ($log['title'] ?? '')) ?: __('Unknown image', 'shrinkwell-image-optimizer');
            $message = trim((string) ($log['message'] ?? ''));
            echo '<li>';
            echo '<span>' . esc_html($title) . '</span>';
            if ('' !== $message) {
                echo '<small>' . esc_html($message) . '</small>';
            }
            echo '</li>';
        }

        echo '</ul>';
        echo '</div>';
    }

    private function render_log_section(string $title, string $description, array $logs, int $limit, bool $open = false): void
    {
        echo '<details class="shrinkwell-log-section"' . ($open ? ' open' : '') . '>';
        echo '<summary>';
        echo '<span>' . esc_html($title) . '</span>';
        echo '<strong>' . esc_html(number_format_i18n(count($logs))) . '</strong>';
        echo '</summary>';
        echo '<p class="description">' . esc_html($description) . '</p>';
        $this->render_log_rows_table($logs, $limit);
        echo '</details>';
    }

    private function render_log_rows_table(array $logs, int $limit): void
    {
        $logs = array_slice($logs, 0, $limit);

        echo '<table class="widefat striped shrinkwell-log-table"><thead><tr>';
        echo '<th>' . esc_html__('Time', 'shrinkwell-image-optimizer') . '</th>';
        echo '<th>' . esc_html__('Image', 'shrinkwell-image-optimizer') . '</th>';
        echo '<th>' . esc_html__('Status', 'shrinkwell-image-optimizer') . '</th>';
        echo '<th>' . esc_html__('Saved', 'shrinkwell-image-optimizer') . '</th>';
        echo '<th>' . esc_html__('Message', 'shrinkwell-image-optimizer') . '</th>';
        echo '</tr></thead><tbody>';

        foreach ($logs as $log) {
            $attachment_id = absint($log['attachment_id'] ?? 0);
            $title = (string) ($log['title'] ?? '');
            $display_title = $this->truncate_log_title($title ?: '#' . $attachment_id);
            $edit_link = $attachment_id ? get_edit_post_link($attachment_id) : '';
            $status = (string) ($log['status'] ?? 'unknown');

            echo '<tr>';
            echo '<td>' . esc_html((string) ($log['time'] ?? '')) . '</td>';
            echo '<td>';
            if ($edit_link) {
                echo '<a href="' . esc_url($edit_link) . '" title="' . esc_attr($title ?: '#' . $attachment_id) . '">' . esc_html($display_title) . '</a>';
            } else {
                echo esc_html($title ? $display_title : __('Unknown image', 'shrinkwell-image-optimizer'));
            }
            echo '</td>';
            echo '<td><span class="shrinkwell-log-status shrinkwell-log-' . esc_attr(sanitize_html_class($status)) . '">' . esc_html($this->log_status_label($status)) . '</span></td>';
            echo '<td>' . esc_html(size_format((int) ($log['saved_bytes'] ?? 0), 1)) . '</td>';
            echo '<td>' . esc_html((string) ($log['message'] ?? '')) . '</td>';
            echo '</tr>';
        }

        echo '</tbody></table>';
    }

    private function log_status_label(string $status): string
    {
        $labels = [
            'optimized' => __('Optimized', 'shrinkwell-image-optimizer'),
            'webp' => __('WebP generated', 'shrinkwell-image-optimizer'),
            'restored' => __('Restored', 'shrinkwell-image-optimizer'),
            'backup_cleanup' => __('Backup cleaned', 'shrinkwell-image-optimizer'),
            'skipped' => __('Skipped safely', 'shrinkwell-image-optimizer'),
            'error' => __('Needs attention', 'shrinkwell-image-optimizer'),
        ];

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

    private function render_backup_table(array $rows): void
    {
        if (!$rows) {
            echo '<p class="description">' . esc_html__('No backup files were found.', 'shrinkwell-image-optimizer') . '</p>';
            return;
        }

        echo '<table class="widefat striped shrinkwell-log-table"><thead><tr>';
        echo '<th>' . esc_html__('Image', 'shrinkwell-image-optimizer') . '</th>';
        echo '<th>' . esc_html__('Original files', 'shrinkwell-image-optimizer') . '</th>';
        echo '<th>' . esc_html__('Original size', 'shrinkwell-image-optimizer') . '</th>';
        echo '<th>' . esc_html__('Saved by optimization', 'shrinkwell-image-optimizer') . '</th>';
        echo '<th>' . esc_html__('Savings %', 'shrinkwell-image-optimizer') . '</th>';
        echo '<th>' . esc_html__('Action', 'shrinkwell-image-optimizer') . '</th>';
        echo '</tr></thead><tbody>';

        foreach ($rows as $row) {
            $attachment_id = absint($row['attachment_id'] ?? 0);
            $title = (string) ($row['title'] ?? '');
            $edit_link = $attachment_id ? get_edit_post_link($attachment_id) : '';
            $original_bytes = (int) ($row['bytes'] ?? 0);
            $saved_bytes = (int) ($row['saved_bytes'] ?? 0);
            $savings_pct = $original_bytes > 0 && $saved_bytes > 0 ? round(($saved_bytes / $original_bytes) * 100, 1) : 0.0;

            echo '<tr>';
            echo '<td>';
            if ($edit_link) {
                echo '<a href="' . esc_url($edit_link) . '">' . esc_html($title ?: '#' . $attachment_id) . '</a>';
            } else {
                echo esc_html($title ?: __('Unknown image', 'shrinkwell-image-optimizer'));
            }
            echo '</td>';
            echo '<td>' . esc_html(number_format_i18n((int) ($row['files'] ?? 0))) . '</td>';
            echo '<td>' . esc_html(size_format($original_bytes, 1)) . '</td>';
            echo '<td>';
            if ($saved_bytes <= 0) {
                echo '<span class="shrinkwell-backup-zero" title="' . esc_attr__('This image may already have been small, or savings are stored separately in WebP/AVIF files.', 'shrinkwell-image-optimizer') . '">&mdash;</span>';
            } else {
                echo '<span class="shrinkwell-savings-good">' . esc_html(size_format($saved_bytes, 1)) . '</span>';
            }
            echo '</td>';
            echo '<td>';
            if ($savings_pct > 0) {
                echo '<span class="' . esc_attr($this->savings_percentage_class($savings_pct)) . '">' . esc_html(number_format_i18n($savings_pct, 1) . '%') . '</span>';
            } else {
                echo '&mdash;';
            }
            echo '</td>';
            echo '<td>' . wp_kses_post($this->restore_link($attachment_id)) . '</td>';
            echo '</tr>';
        }

        echo '</tbody></table>';
    }

    private function savings_percentage_class(float $savings_pct): string
    {
        if ($savings_pct < 5.0) {
            return 'sw-savings-low';
        }

        if ($savings_pct <= 15.0) {
            return 'sw-savings-medium';
        }

        return 'sw-savings-high';
    }

    public function maybe_auto_optimize(array $metadata, int $attachment_id, string $context): array
    {
        $settings = $this->settings();

        if (!shrinkwell_can_use_auto_optimize() || empty($settings['auto_optimize']) || 'create' !== $context || !$this->is_optimizable_attachment($attachment_id)) {
            return $metadata;
        }

        $result = $this->optimize_attachment($attachment_id, $metadata);

        return is_wp_error($result) ? $metadata : $result['metadata'];
    }

    public function add_webp_picture_source(string $html, int $attachment_id, $size, bool $icon, array $attr): string
    {
        if (is_admin() || $icon) {
            return $html;
        }

        return $this->picture_html_for_image($html, $attachment_id, $size);
    }

    public function maybe_start_frontend_buffer(): void
    {
        if (
            is_admin() ||
            wp_doing_ajax() ||
            wp_doing_cron() ||
            is_feed() ||
            is_robots() ||
            is_trackback() ||
            headers_sent()
        ) {
            return;
        }

        $settings = $this->settings();

        if (empty($settings['serve_webp']) || empty($settings['create_webp'])) {
            return;
        }

        if (!apply_filters('shrinkwell_enable_frontend_html_rewrite', true)) {
            return;
        }

        ob_start([$this, 'rewrite_frontend_html']);
    }

    public function rewrite_frontend_html(string $html): string
    {
        if ('' === $html || false === stripos($html, '<html')) {
            return $html;
        }

        $rewritten = $this->add_webp_sources_to_content($html, true);
        $rewritten = $this->rewrite_inline_background_images($rewritten);
        $rewritten = $this->rewrite_preload_image_links($rewritten);

        return $rewritten;
    }

    private function rewrite_inline_background_images(string $html): string
    {
        if (false === stripos($html, 'url(') || !apply_filters('shrinkwell_rewrite_background_images', true)) {
            return $html;
        }

        return preg_replace_callback('/url\(\s*([\'"]?)([^)\'"]+\.(?:jpe?g|png)(?:\?[^)\'"]*)?)\1\s*\)/i', function (array $matches): string {
            $quote = (string) ($matches[1] ?? '');
            $url = html_entity_decode((string) ($matches[2] ?? ''), ENT_QUOTES);
            $webp_url = $this->next_gen_url_for_url($url, 'webp');

            if (!$webp_url) {
                return $matches[0];
            }

            return 'url(' . $quote . esc_url_raw($webp_url) . $quote . ')';
        }, $html) ?? $html;
    }

    private function rewrite_preload_image_links(string $html): string
    {
        if (false === stripos($html, '<link') || false === stripos($html, 'as=') || !apply_filters('shrinkwell_rewrite_image_preloads', true)) {
            return $html;
        }

        return preg_replace_callback('/<link\b[^>]*>/i', function (array $matches): string {
            $link = $matches[0];
            $as = strtolower($this->attribute_from_html($link, 'as'));

            if ('image' !== $as) {
                return $link;
            }

            $href = $this->attribute_from_html($link, 'href');

            if ('' === $href) {
                return $link;
            }

            $webp_url = $this->next_gen_url_for_url($href, 'webp');

            if (!$webp_url) {
                return $link;
            }

            $rewritten = $this->replace_html_attribute($link, 'href', $webp_url);

            if ('' === $this->attribute_from_html($rewritten, 'type')) {
                $rewritten = $this->add_html_attribute($rewritten, 'type', 'image/webp');
            }

            return $rewritten;
        }, $html) ?? $html;
    }

    private function picture_html_for_image(string $html, int $attachment_id, $size): string
    {
        $settings = $this->settings();

        if (false !== stripos($html, '<picture') || !$this->is_supported_attachment($attachment_id) || 'image/webp' === get_post_mime_type($attachment_id)) {
            return $html;
        }

        $sources = [];
        $sizes = $this->attribute_from_html($html, 'sizes');

        if (!empty($settings['serve_avif']) && !empty($settings['create_avif'])) {
            $avif_srcset = $this->next_gen_srcset_for_image_html($html, $attachment_id, $size, 'avif');

            if ('' !== $avif_srcset) {
                $sources[] = $this->source_html('image/avif', $avif_srcset, $sizes);
            }
        }

        if (!empty($settings['serve_webp']) && !empty($settings['create_webp'])) {
            $webp_srcset = $this->next_gen_srcset_for_image_html($html, $attachment_id, $size, 'webp');

            if ('' !== $webp_srcset) {
                $sources[] = $this->source_html('image/webp', $webp_srcset, $sizes);
            }
        }

        if (!$sources) {
            return $html;
        }

        return '<picture class="shrinkwell-picture">' . implode('', $sources) . $html . '</picture>';
    }

    public function add_webp_sources_to_content(string $content, bool $force = false): string
    {
        $settings = $this->settings();

        if ((!$force && is_admin()) || false === stripos($content, '<img') || (empty($settings['serve_webp']) && empty($settings['serve_avif']))) {
            return $content;
        }

        return preg_replace_callback('/<img\b[^>]*>/i', function (array $matches) use ($content): string {
            $img = $matches[0][0];
            $offset = (int) $matches[0][1];

            if ($this->is_offset_inside_picture($content, $offset)) {
                return $img;
            }

            $src = $this->attribute_from_html($img, 'src');

            if ('' === $src) {
                return $img;
            }

            $srcset = $this->attribute_from_html($img, 'srcset');
            $sizes = $this->attribute_from_html($img, 'sizes');
            $sources = [];

            if (!empty($settings['serve_avif']) && !empty($settings['create_avif'])) {
                $avif_srcset = $this->is_primary_product_image_html($img) ? '' : ($srcset ? $this->next_gen_srcset_from_srcset($srcset, 'avif') : '');

                if ('' === $avif_srcset) {
                    $avif_srcset = $this->next_gen_srcset_from_html_fallback($img, $src, 'avif');
                }

                if ('' !== $avif_srcset) {
                    $sources[] = $this->source_html('image/avif', $avif_srcset, $sizes);
                }
            }

            if (!empty($settings['serve_webp']) && !empty($settings['create_webp'])) {
                $webp_srcset = $this->is_primary_product_image_html($img) ? '' : ($srcset ? $this->next_gen_srcset_from_srcset($srcset, 'webp') : '');

                if ('' === $webp_srcset) {
                    $webp_srcset = $this->next_gen_srcset_from_html_fallback($img, $src, 'webp');
                }

                if ('' !== $webp_srcset) {
                    $sources[] = $this->source_html('image/webp', $webp_srcset, $sizes);
                }
            }

            if (!$sources) {
                return $img;
            }

            return '<picture class="shrinkwell-picture">' . implode('', $sources) . $img . '</picture>';
        }, $content, -1, $count, PREG_OFFSET_CAPTURE) ?? $content;
    }

    public function add_media_column(array $columns): array
    {
        $columns['shrinkwell_status'] = __('Optimization', 'shrinkwell-image-optimizer');

        return $columns;
    }

    public function render_media_library_filters(string $post_type, string $which = ''): void
    {
        global $pagenow;

        static $rendered = false;

        if ($rendered || 'upload.php' !== $pagenow || ('attachment' !== $post_type && '' !== $post_type) || 'bottom' === $which) {
            return;
        }

        $rendered = true;

        $selected = $this->current_media_library_filter();
        $filters = $this->media_library_filter_labels();

        echo '<label class="screen-reader-text" for="shrinkwell-media-filter">' . esc_html__('Filter by Shrinkwell status', 'shrinkwell-image-optimizer') . '</label>';
        echo '<select id="shrinkwell-media-filter" name="shrinkwell_media_filter">';
        echo '<option value="">' . esc_html__('All Shrinkwell statuses', 'shrinkwell-image-optimizer') . '</option>';

        foreach ($filters as $value => $label) {
            echo '<option value="' . esc_attr($value) . '"' . selected($selected, $value, false) . '>' . esc_html($label) . '</option>';
        }

        echo '</select>';
    }

    public function filter_media_library_query(WP_Query $query): void
    {
        global $pagenow;

        if (!is_admin() || !$query->is_main_query() || 'upload.php' !== $pagenow) {
            return;
        }

        $filter = $this->current_media_library_filter();

        if (!$filter || !isset($this->media_library_filter_labels()[$filter])) {
            return;
        }

        $ids = $this->media_library_filter_attachment_ids($filter);

        $query->set('post__in', $ids ?: [0]);
        $query->set('orderby', 'post__in');
    }

    private function current_media_library_filter(): string
    {
        $filter = filter_input(INPUT_GET, 'shrinkwell_media_filter', FILTER_UNSAFE_RAW);

        return sanitize_key((string) $filter);
    }

    private function media_library_filter_labels(): array
    {
        return [
            'needs_action' => __('Needs action', 'shrinkwell-image-optimizer'),
            'webp_missing' => __('WebP missing', 'shrinkwell-image-optimizer'),
            'avif_missing' => __('AVIF missing', 'shrinkwell-image-optimizer'),
            'reoptimize' => __('Ready to re-optimize', 'shrinkwell-image-optimizer'),
            'skipped' => __('Skipped safely', 'shrinkwell-image-optimizer'),
        ];
    }

    private function media_library_filter_attachment_ids(string $filter): array
    {
        $query = new WP_Query([
            'post_type' => 'attachment',
            'post_mime_type' => ['image/jpeg', 'image/png', 'image/webp'],
            'post_status' => 'inherit',
            'fields' => 'ids',
            'posts_per_page' => max(50, min(2000, (int) apply_filters('shrinkwell_media_filter_scan_limit', 1000))),
            'orderby' => 'ID',
            'order' => 'DESC',
            'no_found_rows' => true,
        ]);
        $ids = [];

        foreach ($query->posts as $attachment_id) {
            $attachment_id = (int) $attachment_id;

            if ($this->attachment_matches_media_filter($attachment_id, $filter)) {
                $ids[] = $attachment_id;
            }
        }

        return $ids;
    }

    private function attachment_matches_media_filter(int $attachment_id, string $filter): bool
    {
        $meta = (array) get_post_meta($attachment_id, self::META, true);
        $signature_changed = !empty($meta['optimized_at']) && (empty($meta['settings_signature']) || $meta['settings_signature'] !== $this->optimization_signature());
        $limit_error = $this->attachment_processing_limit_error($attachment_id);
        $recent_status = $this->recent_attachment_issue_status($attachment_id);

        if ('reoptimize' === $filter) {
            return $signature_changed;
        }

        if ('skipped' === $filter) {
            return (bool) $limit_error || 'skipped' === $recent_status;
        }

        if ('webp_missing' === $filter) {
            return $this->attachment_next_gen_is_missing($attachment_id, 'webp', true);
        }

        if ('avif_missing' === $filter) {
            $settings = $this->settings();
            $avif_enabled = !empty($settings['create_avif']) || !empty($settings['serve_avif']) || $this->server_can_use_avif();

            return $this->attachment_next_gen_is_missing($attachment_id, 'avif', $avif_enabled);
        }

        if ('needs_action' === $filter) {
            $settings = $this->settings();
            $avif_enabled = !empty($settings['create_avif']) || !empty($settings['serve_avif']) || $this->server_can_use_avif();

            return 'error' === $recent_status
                || $signature_changed
                || $this->attachment_next_gen_is_missing($attachment_id, 'webp', true)
                || $this->attachment_next_gen_is_missing($attachment_id, 'avif', $avif_enabled);
        }

        return false;
    }

    private function after_restore_guide_requested(): bool
    {
        $after_restore = sanitize_key((string) wp_unslash($_GET['shrinkwell_after_restore'] ?? ''));
        $nonce = sanitize_text_field((string) wp_unslash($_GET['shrinkwell_notice_nonce'] ?? ''));

        return '1' === $after_restore && wp_verify_nonce($nonce, 'shrinkwell_admin_notice');
    }

    public function render_attachment_submitbox_status(): void
    {
        $post = get_post();

        if (!$post instanceof WP_Post || 'attachment' !== $post->post_type || !$this->is_supported_attachment((int) $post->ID)) {
            return;
        }

        echo '<div class="misc-pub-section shrinkwell-attachment-submitbox-status">';
        echo wp_kses_post($this->attachment_status_panel((int) $post->ID, true));
        echo '</div>';
    }

    public function add_attachment_fields_status(array $form_fields, WP_Post $post): array
    {
        if (!$this->is_supported_attachment((int) $post->ID)) {
            return $form_fields;
        }

        $form_fields['shrinkwell_status'] = [
            'label' => __('Shrinkwell', 'shrinkwell-image-optimizer'),
            'input' => 'html',
            'html' => $this->attachment_status_panel((int) $post->ID, false),
        ];

        return $form_fields;
    }

    private function attachment_status_panel(int $attachment_id, bool $show_heading): string
    {
        $meta = (array) get_post_meta($attachment_id, self::META, true);
        $signature_changed = !empty($meta['optimized_at']) && (empty($meta['settings_signature']) || $meta['settings_signature'] !== $this->optimization_signature());
        $limit_error = $this->attachment_processing_limit_error($attachment_id);
        $savings_percent = $this->attachment_main_savings_percent($attachment_id, $meta);
        $status_label = __('Not optimized', 'shrinkwell-image-optimizer');
        $status_class = '';

        if (!empty($meta['optimized_at']) && !$signature_changed) {
            $status_label = __('Optimized', 'shrinkwell-image-optimizer');
            $status_class = ' shrinkwell-pill-good';
        } elseif ($signature_changed) {
            $status_label = __('Re-optimize', 'shrinkwell-image-optimizer');
            $status_class = ' shrinkwell-pill-warn';
        } elseif ($limit_error) {
            $status_label = __('Skipped safely', 'shrinkwell-image-optimizer');
            $status_class = ' shrinkwell-pill-warn';
        }

        ob_start();
        ?>
        <div class="shrinkwell-attachment-status-card">
            <div class="shrinkwell-attachment-status-card__head">
                <?php if ($show_heading) : ?>
                    <strong><?php esc_html_e('Shrinkwell', 'shrinkwell-image-optimizer'); ?></strong>
                <?php endif; ?>
                <span class="shrinkwell-pill<?php echo esc_attr($status_class); ?>"><?php echo esc_html($status_label); ?></span>
            </div>
            <div class="shrinkwell-attachment-status-card__percent">
                <strong><?php echo esc_html(number_format_i18n($savings_percent)); ?>%</strong>
                <span><?php esc_html_e('saved', 'shrinkwell-image-optimizer'); ?></span>
            </div>
        </div>
        <?php
        return (string) ob_get_clean();
    }

    private function attachment_main_savings_percent(int $attachment_id, array $meta): int
    {
        $saved = isset($meta['main_saved_bytes']) ? (int) $meta['main_saved_bytes'] : (int) ($meta['saved_bytes'] ?? 0);

        if ($saved <= 0) {
            return 0;
        }

        $file = get_attached_file($attachment_id);

        if (!$file || !file_exists($file)) {
            return 0;
        }

        $current = (int) filesize($file);
        $original = $current + $saved;

        if ($original <= 0) {
            return 0;
        }

        return max(0, min(100, (int) round(($saved / $original) * 100)));
    }

    public function render_media_column(string $column_name, int $attachment_id): void
    {
        if ('shrinkwell_status' !== $column_name) {
            return;
        }

        if (!$this->is_supported_attachment($attachment_id)) {
            echo '&mdash;';
            return;
        }

        $meta = (array) get_post_meta($attachment_id, self::META, true);
        $signature_changed = !empty($meta['optimized_at']) && (empty($meta['settings_signature']) || $meta['settings_signature'] !== $this->optimization_signature());
        $limit_error = $this->attachment_processing_limit_error($attachment_id);
        $issue = $limit_error ? $limit_error->get_error_message() : '';

        if ('' === $issue && empty($meta['optimized_at'])) {
            $issue = $this->recent_attachment_issue($attachment_id);
        }
        $settings = $this->settings();
        $webp_status = $this->attachment_next_gen_status($attachment_id, 'webp', true);
        $show_avif_status = !empty($settings['create_avif']) || !empty($settings['serve_avif']) || $this->server_can_use_avif();
        $avif_status = $show_avif_status ? $this->attachment_next_gen_status($attachment_id, 'avif', !empty($settings['create_avif']) || !empty($settings['serve_avif'])) : null;

        echo '<div class="shrinkwell-media-proof">';

        if (!empty($meta['optimized_at']) && !$signature_changed) {
            echo '<span class="shrinkwell-pill shrinkwell-pill-good">' . esc_html__('Optimized', 'shrinkwell-image-optimizer') . '</span>';
        } elseif ($signature_changed) {
            echo '<span class="shrinkwell-pill shrinkwell-pill-warn">' . esc_html__('Re-optimize', 'shrinkwell-image-optimizer') . '</span>';
        } elseif ($limit_error) {
            echo '<span class="shrinkwell-pill shrinkwell-pill-warn">' . esc_html__('Skipped safely', 'shrinkwell-image-optimizer') . '</span>';
        } else {
            echo '<span class="shrinkwell-pill">' . esc_html__('Not optimized', 'shrinkwell-image-optimizer') . '</span>';
        }

        if (!empty($meta['optimized_at'])) {
            $saved = isset($meta['main_saved_bytes']) ? (int) $meta['main_saved_bytes'] : (int) ($meta['saved_bytes'] ?? 0);
            echo '<small>' . esc_html(sprintf(
                /* translators: %s: Human-readable amount of disk space saved. */
                __('Saved %s', 'shrinkwell-image-optimizer'),
                size_format($saved, 1)
            )) . '</small>';
        }

        echo wp_kses_post($this->media_proof_status_line((string) $webp_status['label'], (string) $webp_status['class']));

        if (is_array($avif_status)) {
            echo wp_kses_post($this->media_proof_status_line((string) $avif_status['label'], (string) $avif_status['class']));
        }

        if ($signature_changed) {
            echo '<small class="shrinkwell-media-proof-warn">' . esc_html__('Settings changed since this image was optimized.', 'shrinkwell-image-optimizer') . '</small>';
        }

        if ('' !== $issue) {
            echo '<small class="shrinkwell-media-proof-note" title="' . esc_attr($issue) . '">' . esc_html($this->shorten_text($issue, 80)) . '</small>';
        }

        echo '<div class="shrinkwell-media-proof-actions">';
        if ($this->is_optimizable_attachment($attachment_id)) {
            echo wp_kses_post($this->test_link($attachment_id));

            if (empty($meta['optimized_at']) || $signature_changed) {
                echo wp_kses_post($this->optimize_link($attachment_id, $signature_changed));
            }
        }

        if (!empty($meta['optimized_at'])) {
            echo wp_kses_post($this->restore_link($attachment_id));
        }

        echo '</div>';
        echo '<span class="shrinkwell-media-action-status" aria-live="polite"></span>';
        echo '</div>';
    }

    private function media_proof_status_line(string $label, string $class): string
    {
        $short_label = $this->shorten_text($label, 64);

        return '<small class="' . esc_attr($class) . '" title="' . esc_attr($label) . '">' . esc_html($short_label) . '</small>';
    }

    private function attachment_next_gen_status(int $attachment_id, string $extension, bool $enabled): array
    {
        $extension = 'avif' === $extension ? 'avif' : 'webp';
        $label = 'avif' === $extension ? 'AVIF' : 'WebP';

        if (!$enabled) {
            return [
                'label' => sprintf(
                    /* translators: %s: Image format, such as WebP or AVIF. */
                    __('%s off', 'shrinkwell-image-optimizer'),
                    $label
                ),
                'class' => 'shrinkwell-media-proof-muted',
            ];
        }

        $counts = $this->attachment_next_gen_counts($attachment_id, $extension);

        if (!empty($counts['file_missing'])) {
            return [
                'label' => __('File missing', 'shrinkwell-image-optimizer'),
                'class' => 'shrinkwell-media-proof-warn',
            ];
        }

        $checked = (int) $counts['checked'];
        $ready = (int) $counts['ready'];
        $missing = (int) $counts['missing'];

        if (0 === $checked) {
            return [
                'label' => sprintf(
                    /* translators: %s: Image format, such as WebP or AVIF. */
                    __('%s source image', 'shrinkwell-image-optimizer'),
                    $label
                ),
                'class' => 'shrinkwell-media-proof-ok',
            ];
        }

        if ($ready > 0 && 0 === $missing) {
            return [
                'label' => sprintf(
                    /* translators: 1: Image format, such as WebP or AVIF. 2: Number of generated files found. */
                    __('%1$s ready: %2$s', 'shrinkwell-image-optimizer'),
                    $label,
                    number_format_i18n($ready)
                ),
                'class' => 'shrinkwell-media-proof-ok',
            ];
        }

        if ($ready > 0) {
            return [
                'label' => sprintf(
                    /* translators: 1: Image format, such as WebP or AVIF. 2: Number of generated files found. 3: Number of missing files. */
                    __('%1$s partial: %2$s ready, %3$s missing', 'shrinkwell-image-optimizer'),
                    $label,
                    number_format_i18n($ready),
                    number_format_i18n($missing)
                ),
                'class' => 'shrinkwell-media-proof-warn',
            ];
        }

        return [
            'label' => sprintf(
                /* translators: 1: Image format, such as WebP or AVIF. 2: Number of missing files. */
                __('%1$s missing: %2$s', 'shrinkwell-image-optimizer'),
                $label,
                number_format_i18n($missing)
            ),
            'class' => 'shrinkwell-media-proof-warn',
        ];
    }

    private function attachment_next_gen_is_missing(int $attachment_id, string $extension, bool $enabled): bool
    {
        if (!$enabled) {
            return false;
        }

        $counts = $this->attachment_next_gen_counts($attachment_id, $extension);

        return empty($counts['file_missing']) && (int) $counts['checked'] > 0 && (int) $counts['missing'] > 0;
    }

    private function attachment_next_gen_counts(int $attachment_id, string $extension): array
    {
        $extension = 'avif' === $extension ? 'avif' : 'webp';
        $metadata = (array) wp_get_attachment_metadata($attachment_id);
        $file = get_attached_file($attachment_id);
        $counts = [
            'checked' => 0,
            'ready' => 0,
            'missing' => 0,
            'file_missing' => false,
        ];

        if (!$file || !file_exists($file)) {
            $counts['file_missing'] = true;

            return $counts;
        }

        foreach ($this->attachment_files((string) $file, $metadata) as $path) {
            if (!file_exists($path) || !$this->is_supported_path($path) || 'image/' . $extension === $this->mime_for_path($path)) {
                continue;
            }

            $counts['checked']++;
            $next_gen_path = preg_replace('/\.(jpe?g|png|webp)$/i', '.' . $extension, $path);

            if ($next_gen_path && file_exists($next_gen_path) && $this->next_gen_copy_is_usable($path, $next_gen_path, $extension)) {
                $counts['ready']++;
            } else {
                $counts['missing']++;
            }
        }

        return $counts;
    }

    private function shorten_text(string $text, int $limit): string
    {
        if (function_exists('mb_strlen') && function_exists('mb_substr')) {
            return mb_strlen($text) > $limit ? mb_substr($text, 0, $limit - 3) . '...' : $text;
        }

        return strlen($text) > $limit ? substr($text, 0, $limit - 1) . '...' : $text;
    }

    private function optimize_link(int $attachment_id, bool $is_reoptimize = false): string
    {
        $url = wp_nonce_url(
            admin_url('admin-post.php?action=shrinkwell_optimize_attachment&attachment_id=' . $attachment_id),
            'shrinkwell_optimize_attachment_' . $attachment_id
        );

        $label = $is_reoptimize ? __('Re-optimize image', 'shrinkwell-image-optimizer') : __('Optimize image', 'shrinkwell-image-optimizer');

        return '<a class="button button-small shrinkwell-optimize-attachment" href="' . esc_url($url) . '" data-attachment-id="' . esc_attr((string) $attachment_id) . '">' . esc_html($label) . '</a>';
    }

    private function test_link(int $attachment_id): string
    {
        $url = wp_nonce_url(
            admin_url('admin-post.php?action=shrinkwell_test_attachment&attachment_id=' . $attachment_id),
            'shrinkwell_test_attachment_' . $attachment_id
        );

        return '<a class="button button-small shrinkwell-test-compression" href="' . esc_url($url) . '" data-attachment-id="' . esc_attr((string) $attachment_id) . '">' . esc_html__('Test compression', 'shrinkwell-image-optimizer') . '</a>';
    }

    public function add_media_row_action(array $actions, WP_Post $post): array
    {
        if (!$this->is_supported_attachment((int) $post->ID)) {
            return $actions;
        }

        $meta = (array) get_post_meta((int) $post->ID, self::META, true);

        $signature_changed = !empty($meta['optimized_at']) && (empty($meta['settings_signature']) || $meta['settings_signature'] !== $this->optimization_signature());

        if ($this->is_optimizable_attachment((int) $post->ID)) {
            $optimize_url = wp_nonce_url(
                admin_url('admin-post.php?action=shrinkwell_optimize_attachment&attachment_id=' . (int) $post->ID),
                'shrinkwell_optimize_attachment_' . (int) $post->ID
            );

            $test_url = wp_nonce_url(
                admin_url('admin-post.php?action=shrinkwell_test_attachment&attachment_id=' . (int) $post->ID),
                'shrinkwell_test_attachment_' . (int) $post->ID
            );

            $actions['shrinkwell_test'] = '<a class="shrinkwell-test-compression" href="' . esc_url($test_url) . '" data-attachment-id="' . esc_attr((string) $post->ID) . '">' . esc_html__('Test compression', 'shrinkwell-image-optimizer') . '</a>';

            if (empty($meta['optimized_at']) || $signature_changed) {
                $actions['shrinkwell_optimize'] = '<a class="shrinkwell-optimize-attachment" href="' . esc_url($optimize_url) . '" data-attachment-id="' . esc_attr((string) $post->ID) . '">' . esc_html($signature_changed ? __('Re-optimize image', 'shrinkwell-image-optimizer') : __('Optimize image', 'shrinkwell-image-optimizer')) . '</a>';
            }
        }

        if (!empty($meta['optimized_at'])) {
            $actions['shrinkwell_restore'] = $this->restore_link((int) $post->ID);
        }

        return $actions;
    }

    public function handle_single_optimize(): void
    {
        $attachment_id = absint(wp_unslash($_GET['attachment_id'] ?? 0));

        if (!$attachment_id || !current_user_can('upload_files') || !check_admin_referer('shrinkwell_optimize_attachment_' . $attachment_id)) {
            wp_die(esc_html__('This optimization request is not allowed.', 'shrinkwell-image-optimizer'));
        }

        $result = $this->optimize_attachment($attachment_id);
        $redirect = wp_get_referer() ?: admin_url('upload.php');

        wp_safe_redirect(esc_url_raw(add_query_arg([
            'shrinkwell_result' => is_wp_error($result) ? 'error' : 'ok',
            'shrinkwell_notice_nonce' => wp_create_nonce('shrinkwell_admin_notice'),
        ], $redirect)));
        exit;
    }

    public function handle_single_test(): void
    {
        $attachment_id = absint(wp_unslash($_GET['attachment_id'] ?? 0));

        if (!$attachment_id || !current_user_can('upload_files') || !check_admin_referer('shrinkwell_test_attachment_' . $attachment_id)) {
            wp_die(esc_html__('This compression test request is not allowed.', 'shrinkwell-image-optimizer'));
        }

        $result = $this->test_attachment_compression($attachment_id);

        if (is_wp_error($result)) {
            set_transient($this->test_notice_transient_key(), [
                'success' => false,
                'message' => $result->get_error_message(),
            ], MINUTE_IN_SECONDS);
        } else {
            set_transient($this->test_notice_transient_key(), [
                'success' => true,
                'message' => $this->compression_test_notice_message($attachment_id, $result),
            ], MINUTE_IN_SECONDS);
        }

        $redirect = wp_get_referer() ?: admin_url('upload.php');

        wp_safe_redirect(esc_url_raw(add_query_arg([
            'shrinkwell_result' => 'tested',
            'shrinkwell_notice_nonce' => wp_create_nonce('shrinkwell_admin_notice'),
        ], $redirect)));
        exit;
    }

    public function ajax_test_attachment_compression(): void
    {
        check_ajax_referer('shrinkwell_bulk_optimize', 'nonce');

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

        $attachment_id = absint(wp_unslash($_POST['attachment_id'] ?? 0));

        if (!$attachment_id) {
            wp_send_json_error(['message' => __('No image was selected for the compression test.', 'shrinkwell-image-optimizer')]);
        }

        $result = $this->test_attachment_compression($attachment_id);

        if (is_wp_error($result)) {
            wp_send_json_error([
                'message' => $result->get_error_message(),
            ]);
        }

        wp_send_json_success([
            'attachmentId' => $attachment_id,
            'title' => $this->attachment_title($attachment_id, __('this image', 'shrinkwell-image-optimizer')),
            'testedFiles' => (int) ($result['tested_files'] ?? 0),
            'beforeBytes' => (int) ($result['before_bytes'] ?? 0),
            'afterBytes' => (int) ($result['after_bytes'] ?? 0),
            'savedBytes' => (int) ($result['saved_bytes'] ?? 0),
            'message' => $this->compression_test_notice_message($attachment_id, $result),
        ]);
    }

    private function compression_test_notice_message(int $attachment_id, array $result): string
    {
        $saved_bytes = (int) ($result['saved_bytes'] ?? 0);

        if ($saved_bytes <= 0) {
            return sprintf(
                /* translators: %s: Image title. */
                __('Compression test complete for %s. Shrinkwell would keep this image as-is because the tested output was not smaller. No files were replaced.', 'shrinkwell-image-optimizer'),
                $this->attachment_title($attachment_id, __('this image', 'shrinkwell-image-optimizer'))
            );
        }

        return sprintf(
            /* translators: 1: Image title. 2: Human-readable potential saved size. 3: Number of tested files. */
            __('Compression test complete for %1$s. Shrinkwell would save about %2$s across %3$s files. No files were replaced.', 'shrinkwell-image-optimizer'),
            $this->attachment_title($attachment_id, __('this image', 'shrinkwell-image-optimizer')),
            size_format($saved_bytes, 1),
            number_format_i18n((int) ($result['tested_files'] ?? 0))
        );
    }

    public function handle_single_restore(): void
    {
        $attachment_id = absint(wp_unslash($_GET['attachment_id'] ?? 0));

        if (!$attachment_id || !current_user_can('upload_files') || !check_admin_referer('shrinkwell_restore_attachment_' . $attachment_id)) {
            wp_die(esc_html__('This restore request is not allowed.', 'shrinkwell-image-optimizer'));
        }

        $result = $this->restore_attachment($attachment_id);
        $this->add_bulk_log_entry([
            'attachment_id' => $attachment_id,
            'title' => $this->attachment_title($attachment_id),
            'status' => is_wp_error($result) ? 'error' : 'restored',
            'saved_bytes' => 0,
            'message' => is_wp_error($result)
                ? $result->get_error_message()
                : trim(sprintf(
                    /* translators: %d: Number of restored backup files for an image. */
                    __('Restored %d backup files.', 'shrinkwell-image-optimizer'),
                    (int) ($result['restored_files'] ?? 0)
                ) . ' ' . ((int) ($result['recreated_missing_files'] ?? 0) > 0
                    ? sprintf(
                        /* translators: %d: Number of missing media files recreated from backup. */
                        __('Recreated %d missing files from backup.', 'shrinkwell-image-optimizer'),
                        (int) ($result['recreated_missing_files'] ?? 0)
                    )
                    : '')),
        ]);
        $redirect = wp_get_referer() ?: admin_url('upload.php');

        wp_safe_redirect(esc_url_raw(add_query_arg([
            'shrinkwell_result' => is_wp_error($result) ? 'error' : 'restored',
            'shrinkwell_notice_nonce' => wp_create_nonce('shrinkwell_admin_notice'),
        ], $redirect)));
        exit;
    }

    public function handle_restore_all_backups(): void
    {
        if (!current_user_can('upload_files') || !check_admin_referer('shrinkwell_restore_all_backups')) {
            wp_die(esc_html__('This restore request is not allowed.', 'shrinkwell-image-optimizer'));
        }

        $restored_images = 0;
        $errors = 0;

        foreach ($this->backup_attachment_ids() as $attachment_id) {
            if (empty($this->attachment_backup_summary($attachment_id)['files'])) {
                continue;
            }

            $result = $this->restore_attachment($attachment_id);

            if (is_wp_error($result)) {
                $errors++;
                $this->add_bulk_log_entry([
                    'attachment_id' => $attachment_id,
                    'title' => $this->attachment_title($attachment_id),
                    'status' => 'error',
                    'saved_bytes' => 0,
                    'message' => $result->get_error_message(),
                ]);
                continue;
            }

            $restored_images++;
            $this->add_bulk_log_entry([
                'attachment_id' => $attachment_id,
                'title' => $this->attachment_title($attachment_id),
                'status' => 'restored',
                'saved_bytes' => 0,
                'message' => sprintf(
                    /* translators: %d: Number of restored backup files for an image. */
                    __('Restored %d backup files.', 'shrinkwell-image-optimizer'),
                    (int) ($result['restored_files'] ?? 0)
                ) . ' ' . ((int) ($result['recreated_missing_files'] ?? 0) > 0
                    ? sprintf(
                        /* translators: %d: Number of missing media files recreated from backup. */
                        __('Recreated %d missing files from backup.', 'shrinkwell-image-optimizer'),
                        (int) ($result['recreated_missing_files'] ?? 0)
                    )
                    : ''),
            ]);
        }

        $redirect = remove_query_arg(['shrinkwell_result', 'shrinkwell_count', 'shrinkwell_notice_nonce'], wp_get_referer() ?: admin_url('upload.php?page=shrinkwell-image-optimizer&tab=backups'));
        $redirect = add_query_arg([
            'tab' => 'backups',
            'shrinkwell_result' => $errors ? 'restored_partial' : 'restored_all',
            'shrinkwell_count' => $restored_images,
            'shrinkwell_errors' => $errors,
            'shrinkwell_notice_nonce' => wp_create_nonce('shrinkwell_admin_notice'),
        ], $redirect);

        wp_safe_redirect(esc_url_raw($redirect));
        exit;
    }

    public function ajax_bulk_ids(): void
    {
        check_ajax_referer('shrinkwell_bulk_optimize', 'nonce');

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

        $only_large = 1 === absint(wp_unslash($_POST['only_large'] ?? 0));
        $min_kb = max(1, absint(wp_unslash($_POST['min_kb'] ?? 500)));
        $min_bytes = $min_kb * 1024;
        $reoptimize = 1 === absint(wp_unslash($_POST['reoptimize'] ?? 0));
        $webp_only = 1 === absint(wp_unslash($_POST['webp_only'] ?? 0));
        $date_from = $this->sanitize_date_filter(sanitize_text_field(wp_unslash($_POST['date_from'] ?? '')));
        $date_to = $this->sanitize_date_filter(sanitize_text_field(wp_unslash($_POST['date_to'] ?? '')));
        $exclude_ids = array_filter(array_map('absint', (array) wp_unslash($_POST['exclude_ids'] ?? [])));
        $batch_limit = $this->bulk_batch_limit();

        if ($webp_only && !$this->can_create_webp_locally()) {
            wp_send_json_error(['message' => __('WebP generation is not available on this server.', 'shrinkwell-image-optimizer')]);
        }

        $query_args = [
            'post_type' => 'attachment',
            'post_mime_type' => $webp_only ? ['image/jpeg', 'image/png', 'image/webp'] : ['image/jpeg', 'image/png'],
            'post_status' => 'inherit',
            'fields' => 'ids',
            'posts_per_page' => ($only_large || $date_from || $date_to || $reoptimize) ? 1000 : $batch_limit,
            'orderby' => 'ID',
            'order' => 'ASC',
            'no_found_rows' => true,
        ];

        if ($exclude_ids) {
            // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in -- AJAX batching excludes already queued IDs to prevent duplicate work in one run.
            $query_args['post__not_in'] = $exclude_ids;
        }

        if ($date_from || $date_to) {
            $date_query = [
                'inclusive' => true,
            ];

            if ($date_from) {
                $date_query['after'] = $date_from;
            }

            if ($date_to) {
                $date_query['before'] = $date_to;
            }

            $query_args['date_query'] = [$date_query];
        }

        $query = ($reoptimize || $webp_only) ? new WP_Query($query_args) : new WP_Query(array_merge($query_args, [
            // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Admin-only bulk lookup is capped and falls back to local attachment checks.
            'meta_query' => [
                [
                    'key' => self::META,
                    'compare' => 'NOT EXISTS',
                ],
            ],
        ]));

        if (!$reoptimize && !$query->posts) {
            $query = new WP_Query(array_merge($query_args, [
                'posts_per_page' => 1000,
            ]));
        }

        $items = [];
        $scanned_ids = [];
        $skipped_too_large = 0;
        $skipped_examples = [];

        foreach ($query->posts as $attachment_id) {
            $scanned_ids[] = (int) $attachment_id;

            if ($webp_only && !$this->attachment_needs_webp_generation((int) $attachment_id)) {
                continue;
            }

            if (!$webp_only && !$this->is_optimizable_attachment((int) $attachment_id)) {
                continue;
            }

            if (!$webp_only && !$reoptimize && !$this->attachment_needs_optimization((int) $attachment_id)) {
                continue;
            }

            $file = get_attached_file((int) $attachment_id);

            if (!$file || !file_exists($file)) {
                continue;
            }

            if ($only_large && filesize($file) < $min_bytes) {
                continue;
            }

            $limit_error = $this->attachment_processing_limit_error((int) $attachment_id);

            if ($limit_error) {
                $skipped_too_large++;

                if (count($skipped_examples) < 5) {
                    $skipped_examples[] = [
                        'title' => $this->attachment_title((int) $attachment_id, basename((string) $file)),
                        'message' => $limit_error->get_error_message(),
                    ];
                }

                continue;
            }

            $items[] = [
                'id' => (int) $attachment_id,
                'title' => $this->attachment_title((int) $attachment_id, basename((string) $file)),
                'size' => size_format((int) filesize($file), 1),
                'dimensions' => $this->dimensions_text_for_path((string) $file),
                'limit' => size_format(((int) $this->settings()['max_file_size_mb']) * 1024 * 1024, 1),
            ];

            if (count($items) >= $batch_limit) {
                break;
            }
        }

        wp_send_json_success([
            'ids' => wp_list_pluck($items, 'id'),
            'items' => $items,
            'total' => count($items),
            'scannedIds' => $scanned_ids,
            'skippedTooLarge' => $skipped_too_large,
            'skippedExamples' => $skipped_examples,
            'skippedMessage' => $skipped_too_large > 0
                ? sprintf(
                    /* translators: %s: Number of images. */
                    _n('%s image is above the safe local processing limit.', '%s images are above the safe local processing limit.', $skipped_too_large, 'shrinkwell-image-optimizer'),
                    number_format_i18n($skipped_too_large)
                )
                : '',
        ]);
    }

    private function sanitize_date_filter($value): string
    {
        $value = sanitize_text_field((string) $value);

        return preg_match('/^\d{4}-\d{2}-\d{2}$/', $value) ? $value : '';
    }

    public function ajax_bulk_optimize(): void
    {
        check_ajax_referer('shrinkwell_bulk_optimize', 'nonce');

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

        $ids = array_filter(array_map('absint', (array) wp_unslash($_POST['ids'] ?? [])));
        $webp_only = 1 === absint(wp_unslash($_POST['webp_only'] ?? 0));

        if (!$ids) {
            wp_send_json_error(['message' => __('No images were selected for optimization.', 'shrinkwell-image-optimizer')]);
        }

        if ($webp_only && !$this->can_create_webp_locally()) {
            wp_send_json_error(['message' => __('WebP generation is not available on this server.', 'shrinkwell-image-optimizer')]);
        }

        $results = [
            'optimized' => 0,
            'errors' => 0,
            'skipped' => 0,
            'savedBytes' => 0,
            'message' => '',
        ];

        foreach ($ids as $attachment_id) {
            $result = $webp_only ? $this->generate_webp_for_attachment($attachment_id) : $this->optimize_attachment($attachment_id);

            if (is_wp_error($result)) {
                if ('shrinkwell_monthly_usage_limit_reached' === $result->get_error_code()) {
                    $results['limitReached'] = true;
                    $results['message'] = $result->get_error_message();
                    break;
                }

                $is_safe_skip = $this->is_safe_skip_error($result);
                if ($is_safe_skip) {
                    $results['skipped']++;
                } else {
                    $results['errors']++;
                }
                $results['message'] = $result->get_error_message();
                $this->add_bulk_log_entry([
                    'attachment_id' => $attachment_id,
                    'title' => $this->attachment_title($attachment_id),
                    'status' => $is_safe_skip ? 'skipped' : 'error',
                    'saved_bytes' => 0,
                    'message' => $is_safe_skip
                        ? sprintf(
                            /* translators: %s: Skip reason. */
                            __('Skipped safely: %s', 'shrinkwell-image-optimizer'),
                            $result->get_error_message()
                        )
                        : $result->get_error_message(),
                ]);
                continue;
            }

            $results['optimized']++;
            $results['savedBytes'] += (int) $result['saved_bytes'];
            $this->add_bulk_log_entry([
                'attachment_id' => $attachment_id,
                'title' => $this->attachment_title($attachment_id),
                'status' => $webp_only ? 'webp' : 'optimized',
                'saved_bytes' => (int) $result['saved_bytes'],
                'message' => $webp_only
                    ? sprintf(
                        /* translators: %d: Number of generated WebP files. */
                        __('Generated %d missing WebP files.', 'shrinkwell-image-optimizer'),
                        (int) ($result['webp_files'] ?? 0)
                    )
                    : sprintf(
                        /* translators: 1: Number of optimized files. 2: Number of WebP files. 3: Number of AVIF files. */
                        __('%1$d files, WebP %2$d, AVIF %3$d', 'shrinkwell-image-optimizer'),
                        (int) ($result['optimized_files'] ?? 0),
                        (int) ($result['webp_files'] ?? 0),
                        (int) ($result['avif_files'] ?? 0)
                    ),
            ]);
        }

        wp_send_json_success($results);
    }

    public function ajax_log_client_error(): void
    {
        check_ajax_referer('shrinkwell_bulk_optimize', 'nonce');

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

        $post_data = wp_unslash($_POST);
        $attachment_id = absint($post_data['attachment_id'] ?? 0);
        $title = sanitize_text_field(wp_specialchars_decode((string) ($post_data['title'] ?? ''), ENT_QUOTES));
        $message = sanitize_text_field(wp_specialchars_decode((string) ($post_data['message'] ?? ''), ENT_QUOTES));

        if (!$attachment_id && '' === $title) {
            wp_send_json_error(['message' => __('No image was provided for the error log.', 'shrinkwell-image-optimizer')]);
        }

        $this->add_bulk_log_entry([
            'attachment_id' => $attachment_id,
            'title' => $title ?: ($attachment_id ? $this->attachment_title($attachment_id) : __('Unknown image', 'shrinkwell-image-optimizer')),
            'status' => 'error',
            'saved_bytes' => 0,
            'message' => $message ?: __('The browser request stopped before WordPress returned a full response.', 'shrinkwell-image-optimizer'),
        ]);

        wp_send_json_success();
    }

    private function is_safe_skip_error(WP_Error $error): bool
    {
        return in_array($error->get_error_code(), [
            'shrinkwell_local_file_too_large',
            'shrinkwell_local_dimensions_too_large',
            'shrinkwell_no_webp_generated',
            'shrinkwell_empty_file',
            'shrinkwell_unsupported',
        ], true);
    }

    public function ajax_delivery_test(): void
    {
        check_ajax_referer('shrinkwell_bulk_optimize', 'nonce');

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

        $attachment_id = $this->find_delivery_test_attachment();

        if (!$attachment_id) {
            wp_send_json_error([
                'message' => __('No optimized JPEG or PNG image was found. Run media optimization first.', 'shrinkwell-image-optimizer'),
            ]);
        }

        $settings = $this->settings();
        $webp_srcset = $this->next_gen_srcset_for_attachment($attachment_id, 'medium', 'webp');
        $avif_srcset = $this->next_gen_srcset_for_attachment($attachment_id, 'medium', 'avif');
        $html = (string) wp_get_attachment_image($attachment_id, 'medium');

        if ('' === $html) {
            wp_send_json_error([
                'message' => __('The test image could not be rendered.', 'shrinkwell-image-optimizer'),
            ]);
        }

        $picture_html = $this->picture_html_for_image($html, $attachment_id, 'medium');
        $webp_enabled = !empty($settings['serve_webp']) && !empty($settings['create_webp']);
        $checks = [
            [
                'label' => __('WebP serving enabled', 'shrinkwell-image-optimizer'),
                'ok' => $webp_enabled,
            ],
            [
                'label' => __('WebP file found', 'shrinkwell-image-optimizer'),
                'ok' => '' !== $webp_srcset,
            ],
            [
                'label' => '' !== $avif_srcset
                    ? __('AVIF file found for this sample', 'shrinkwell-image-optimizer')
                    : __('AVIF not generated for this sample; WebP fallback is OK', 'shrinkwell-image-optimizer'),
                'ok' => true,
            ],
            [
                'label' => __('Picture fallback markup generated', 'shrinkwell-image-optimizer'),
                'ok' => false !== strpos($picture_html, '<picture'),
            ],
            [
                'label' => __('Original image fallback still present', 'shrinkwell-image-optimizer'),
                'ok' => false !== strpos($picture_html, '<img'),
            ],
        ];
        $delivery_passed = $webp_enabled && '' !== $webp_srcset && false !== strpos($picture_html, '<picture') && false !== strpos($picture_html, '<img');

        update_option(self::CHECKS_OPTION, array_merge($this->checks(), [
            'delivery_tested_at' => current_time('mysql'),
            'delivery_passed' => $delivery_passed ? 1 : 0,
            'delivery_title' => $this->attachment_title($attachment_id),
        ]), false);

        wp_send_json_success([
            'attachmentId' => $attachment_id,
            'title' => $this->attachment_title($attachment_id),
            'checks' => $checks,
            'passed' => $delivery_passed,
            'message' => __('If checks pass but the browser still shows old files, clear page cache and CDN cache. AVIF can vary per image because Shrinkwell only keeps AVIF copies that pass local safety and size checks.', 'shrinkwell-image-optimizer'),
        ]);
    }

    public function ajax_frontend_proof_scan(): void
    {
        check_ajax_referer('shrinkwell_bulk_optimize', 'nonce');

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

        $post_data = wp_unslash($_POST);
        $url = $this->normalize_frontend_scan_url((string) ($post_data['url'] ?? ''));

        if (is_wp_error($url)) {
            wp_send_json_error(['message' => $url->get_error_message()]);
        }

        $response = wp_safe_remote_get($url, [
            'timeout' => 12,
            'redirection' => 3,
            'user-agent' => 'Shrinkwell Image Optimizer/' . self::VERSION . '; ' . home_url('/'),
        ]);

        if (is_wp_error($response)) {
            wp_send_json_error([
                'message' => sprintf(
                    /* translators: %s: WordPress HTTP error message. */
                    __('Shrinkwell could not scan the page: %s', 'shrinkwell-image-optimizer'),
                    $response->get_error_message()
                ),
            ]);
        }

        $status_code = (int) wp_remote_retrieve_response_code($response);
        $html = (string) wp_remote_retrieve_body($response);

        if ($status_code < 200 || $status_code >= 300 || '' === trim($html)) {
            wp_send_json_error([
                'message' => sprintf(
                    /* translators: %d: HTTP status code. */
                    __('Shrinkwell could not read the page HTML. HTTP status: %d.', 'shrinkwell-image-optimizer'),
                    $status_code
                ),
            ]);
        }

        $picture_count = (int) preg_match_all('/<picture\b/i', $html);
        $webp_source_count = (int) preg_match_all('/<source\b[^>]*type=["\']image\/webp["\'][^>]*>/i', $html);
        $webp_url_count = (int) preg_match_all('/[\'"(\s][^\'")\s]+\.webp(?:\?[^\'")\s]*)?/i', $html);
        $img_count = (int) preg_match_all('/<img\b/i', $html);
        $fallback_img_count = (int) preg_match_all('/<img\b[^>]*\bsrc=["\'][^"\']+\.(?:jpe?g|png)(?:\?[^"\']*)?["\']/i', $html);
        $background_count = (int) preg_match_all('/url\(\s*[\'"]?[^)\'"]+\.(?:jpe?g|png)(?:\?[^)\'"]*)?[\'"]?\s*\)/i', $html, $background_matches);
        $settings = $this->settings();
        $webp_delivery_enabled = !empty($settings['serve_webp']) && !empty($settings['create_webp']);
        $payload_estimate = $this->frontend_payload_estimate($html);
        $rewrite_preview = $this->frontend_rewrite_preview($html, $payload_estimate, $picture_count, $webp_source_count);
        $cache_headers = $this->frontend_cache_header_summary($response);
        $webp_evidence_found = $webp_source_count > 0 || $webp_url_count > 0;
        $uncovered_ready = (int) ($payload_estimate['uncoveredReady'] ?? 0);
        $coverage_percent = (int) ($payload_estimate['coveragePercent'] ?? 0);
        $webp_delivery_complete = $webp_evidence_found && 0 === $uncovered_ready;
        $webp_delivery_partial = $webp_evidence_found && $uncovered_ready > 0;
        $cache_warning = (!empty($payload_estimate['webpReady']) && $uncovered_ready > 0 && $coverage_percent < 80) || !empty($rewrite_preview['wouldImprove']);
        $diagnosis = $this->frontend_delivery_diagnosis(
            $payload_estimate,
            $rewrite_preview,
            $picture_count,
            $webp_source_count,
            $webp_url_count,
            $img_count,
            $background_count,
            $webp_delivery_enabled
        );

        $background_samples = [];
        if (!empty($background_matches[0])) {
            foreach (array_slice($background_matches[0], 0, 5) as $background_match) {
                $background_samples[] = trim(wp_strip_all_tags($background_match));
            }
        }

        $checks = [
            [
                'label' => __('Page HTML scanned', 'shrinkwell-image-optimizer'),
                'ok' => true,
                'value' => esc_url_raw($url),
            ],
            [
                'label' => __('Shrinkwell WebP delivery enabled', 'shrinkwell-image-optimizer'),
                'ok' => $webp_delivery_enabled,
                'value' => $webp_delivery_enabled ? __('Yes', 'shrinkwell-image-optimizer') : __('No', 'shrinkwell-image-optimizer'),
            ],
            [
                'label' => __('Picture elements found', 'shrinkwell-image-optimizer'),
                'ok' => $picture_count > 0,
                'value' => number_format_i18n((int) $picture_count),
            ],
            [
                'label' => __('WebP source tags found', 'shrinkwell-image-optimizer'),
                'ok' => $webp_source_count > 0,
                'value' => number_format_i18n((int) $webp_source_count),
            ],
            [
                'label' => __('WebP URLs found in HTML', 'shrinkwell-image-optimizer'),
                'ok' => $webp_url_count > 0,
                'value' => number_format_i18n((int) $webp_url_count),
            ],
            [
                'label' => __('Image tags checked', 'shrinkwell-image-optimizer'),
                'ok' => $img_count > 0,
                'value' => number_format_i18n((int) $img_count),
            ],
            [
                'label' => __('JPEG/PNG fallback images present', 'shrinkwell-image-optimizer'),
                'ok' => true,
                'value' => number_format_i18n((int) $fallback_img_count),
            ],
            [
                'label' => __('JPEG/PNG background images detected', 'shrinkwell-image-optimizer'),
                'ok' => 0 === $background_count,
                'value' => number_format_i18n((int) $background_count),
            ],
            [
                'label' => __('Uncovered local images with WebP ready', 'shrinkwell-image-optimizer'),
                'ok' => 0 === $uncovered_ready,
                'value' => number_format_i18n($uncovered_ready),
            ],
        ];

        wp_send_json_success([
            'url' => esc_url_raw($url),
            'passed' => $webp_delivery_complete,
            'partial' => $webp_delivery_partial,
            'webpEvidenceFound' => $webp_evidence_found,
            'cacheWarning' => $cache_warning,
            'filesReady' => !empty($payload_estimate['webpReady']),
            'checks' => $checks,
            'estimate' => $payload_estimate,
            'rewritePreview' => $rewrite_preview,
            'cacheHeaders' => $cache_headers,
            'diagnosis' => $diagnosis,
            'backgroundSamples' => $background_samples,
            'message' => $this->frontend_proof_message($background_count, $webp_delivery_partial, $webp_delivery_complete),
        ]);
    }

    private function frontend_delivery_diagnosis(array $estimate, array $rewrite_preview, int $picture_count, int $webp_source_count, int $webp_url_count, int $img_count, int $background_count, bool $webp_delivery_enabled): array
    {
        $webp_ready = (int) ($estimate['webpReady'] ?? 0);
        $webp_existing = (int) ($estimate['webpExisting'] ?? $webp_ready);
        $larger_webp = (int) ($estimate['largerWebp'] ?? 0);
        $uncovered_ready = (int) ($estimate['uncoveredReady'] ?? 0);

        if (!$webp_delivery_enabled && $webp_existing > 0) {
            return [
                'code' => 'delivery_setting_off',
                'title' => __('WebP delivery is turned off in Shrinkwell settings', 'shrinkwell-image-optimizer'),
                'detail' => __('Shrinkwell found WebP files, but the setting that adds WebP delivery markup to the frontend is not enabled.', 'shrinkwell-image-optimizer'),
                'action' => __('Turn on "Serve WebP on the frontend" in Settings, clear cache, then run Page Proof again.', 'shrinkwell-image-optimizer'),
            ];
        }

        if ($webp_ready > 0 && 0 === $uncovered_ready && ($picture_count > 0 || $webp_source_count > 0)) {
            return [
                'code' => 'ok',
                'title' => __('Delivery looks good for this page sample', 'shrinkwell-image-optimizer'),
                'detail' => __('Shrinkwell found usable WebP delivery and no uncovered local JPG/PNG images with usable WebP files.', 'shrinkwell-image-optimizer'),
                'action' => __('No action needed. Re-test after cache, theme, or layout changes.', 'shrinkwell-image-optimizer'),
            ];
        }

        if (0 === $webp_existing) {
            return [
                'code' => 'missing_webp_files',
                'title' => __('WebP files are missing', 'shrinkwell-image-optimizer'),
                'detail' => __('This page uses JPG/PNG images, but Shrinkwell did not find matching WebP files for them.', 'shrinkwell-image-optimizer'),
                'action' => __('Run media optimization or rebuild missing WebP delivery files, then scan again.', 'shrinkwell-image-optimizer'),
            ];
        }

        if (0 === $webp_ready && $larger_webp > 0) {
            return [
                'code' => 'webp_not_smaller',
                'title' => __('Existing WebP files are not worth serving', 'shrinkwell-image-optimizer'),
                'detail' => __('Shrinkwell found WebP files, but they are larger than the original images. Serving them would not improve this page.', 'shrinkwell-image-optimizer'),
                'action' => __('Leave these originals as-is or re-optimize later with different settings. Shrinkwell will not serve larger WebP copies by default.', 'shrinkwell-image-optimizer'),
            ];
        }

        if (!empty($rewrite_preview['wouldImprove'])) {
            return [
                'code' => 'cache_hiding_rewrite',
                'title' => __('Cache is probably hiding Shrinkwell delivery', 'shrinkwell-image-optimizer'),
                'detail' => __('Shrinkwell can add WebP picture markup in a local preview, but the scanned live HTML still shows JPG/PNG URLs.', 'shrinkwell-image-optimizer'),
                'action' => __('Clear page cache, CDN cache, and any theme static HTML/CSS cache. Then run Page Proof again.', 'shrinkwell-image-optimizer'),
            ];
        }

        if ($webp_ready > 0 && 0 === $picture_count && 0 === $webp_source_count && $img_count > 0) {
            return [
                'code' => 'rewrite_not_visible',
                'title' => __('WebP delivery markup is not visible on this page', 'shrinkwell-image-optimizer'),
                'detail' => __('Usable WebP files exist, but the page HTML does not contain Shrinkwell picture/source markup.', 'shrinkwell-image-optimizer'),
                'action' => __('Clear cache first. If this stays the same, the theme, page builder, or cache layer may output images after Shrinkwell can rewrite them.', 'shrinkwell-image-optimizer'),
            ];
        }

        if ($background_count > 0) {
            return [
                'code' => 'background_images',
                'title' => __('Background images need manual review', 'shrinkwell-image-optimizer'),
                'detail' => __('This page uses JPG/PNG URLs inside CSS background images. Some page builders store these outside normal WordPress image markup.', 'shrinkwell-image-optimizer'),
                'action' => __('Review the listed background images in the page builder or theme settings.', 'shrinkwell-image-optimizer'),
            ];
        }

        if ($webp_ready > 0 && $uncovered_ready > 0 && ($picture_count > 0 || $webp_source_count > 0 || $webp_url_count > 0)) {
            return [
                'code' => 'mixed_delivery',
                'title' => __('Mixed delivery on this page', 'shrinkwell-image-optimizer'),
                'detail' => __('Some WebP delivery is visible, but other local JPG/PNG images with usable WebP files are still printed directly.', 'shrinkwell-image-optimizer'),
                'action' => __('Clear cache first. If the same images remain uncovered, review those specific page builder image fields.', 'shrinkwell-image-optimizer'),
            ];
        }

        return [
            'code' => 'needs_review',
            'title' => __('Page needs review', 'shrinkwell-image-optimizer'),
            'detail' => __('Shrinkwell found image URLs that could not be fully explained by this scan.', 'shrinkwell-image-optimizer'),
            'action' => __('Review the uncovered images below and run Page Proof again after cache is cleared.', 'shrinkwell-image-optimizer'),
        ];
    }

    private function frontend_rewrite_preview(string $html, array $current_estimate, int $current_picture_count, int $current_webp_source_count): array
    {
        $preview_html = $this->rewrite_frontend_html($html);
        $preview_estimate = $this->frontend_payload_estimate($preview_html);
        $preview_picture_count = (int) preg_match_all('/<picture\b/i', $preview_html);
        $preview_webp_source_count = (int) preg_match_all('/<source\b[^>]*type=["\']image\/webp["\'][^>]*>/i', $preview_html);
        $current_coverage = (int) ($current_estimate['coveragePercent'] ?? 0);
        $preview_coverage = (int) ($preview_estimate['coveragePercent'] ?? 0);

        return [
            'pictureElementsAfter' => $preview_picture_count,
            'webpSourceTagsAfter' => $preview_webp_source_count,
            'coverageAfter' => $preview_coverage,
            'uncoveredAfter' => (int) ($preview_estimate['uncoveredReady'] ?? 0),
            'wouldImprove' => $preview_picture_count > $current_picture_count || $preview_webp_source_count > $current_webp_source_count || $preview_coverage > $current_coverage,
        ];
    }

    private function frontend_cache_header_summary(array $response): array
    {
        $headers = [];
        $names = [
            'cache-control',
            'age',
            'x-cache',
            'x-varnish',
            'cf-cache-status',
            'server',
            'via',
        ];

        foreach ($names as $name) {
            $value = wp_remote_retrieve_header($response, $name);

            if (is_array($value)) {
                $value = implode(', ', array_map('strval', $value));
            }

            if ('' !== $value) {
                $headers[] = [
                    'name' => $name,
                    'value' => sanitize_text_field((string) $value),
                ];
            }
        }

        return $headers;
    }

    private function frontend_proof_message(int $background_count, bool $partial, bool $complete): string
    {
        if ($partial) {
            return __('Shrinkwell found WebP delivery, but also found local JPG/PNG URLs outside Shrinkwell picture markup. These are usually theme logos, page builder images, preloads, or cached markup.', 'shrinkwell-image-optimizer');
        }

        if ($complete) {
            return __('Shrinkwell did not find uncovered local JPG/PNG images with WebP files ready in this page sample.', 'shrinkwell-image-optimizer');
        }

        if ($background_count > 0) {
            return __('Some JPG/PNG background images were detected. Page builders often output these outside normal WordPress image markup, so review them manually.', 'shrinkwell-image-optimizer');
        }

        return __('No JPG/PNG background images were detected in the page HTML.', 'shrinkwell-image-optimizer');
    }

    private function normalize_frontend_scan_url(string $raw_url)
    {
        $raw_url = trim(wp_unslash($raw_url));

        if ('' === $raw_url) {
            return home_url('/');
        }

        if (0 === strpos($raw_url, '/')) {
            $raw_url = home_url($raw_url);
        }

        $url = esc_url_raw($raw_url, ['http', 'https']);

        if (!$url) {
            return new WP_Error('shrinkwell_invalid_scan_url', __('Enter a valid page URL from this website.', 'shrinkwell-image-optimizer'));
        }

        $home_host = wp_parse_url(home_url('/'), PHP_URL_HOST);
        $url_host = wp_parse_url($url, PHP_URL_HOST);

        if (!$home_host || !$url_host || strtolower($home_host) !== strtolower($url_host)) {
            return new WP_Error('shrinkwell_external_scan_url', __('For safety, Shrinkwell can only scan pages on this WordPress site.', 'shrinkwell-image-optimizer'));
        }

        return $url;
    }

    private function frontend_payload_estimate(string $html): array
    {
        $all_urls = $this->image_urls_from_html($html);
        $content_html = $this->frontend_content_image_html($html);
        $urls = $this->image_urls_from_html($content_html);
        $ignored_non_content_images = count(array_diff($all_urls, $urls));
        $html_without_pictures = preg_replace('/<picture\b[^>]*>.*?<\/picture>/is', '', $content_html);
        $html_without_pictures = is_string($html_without_pictures) ? html_entity_decode($html_without_pictures, ENT_QUOTES, get_bloginfo('charset')) : html_entity_decode($content_html, ENT_QUOTES, get_bloginfo('charset'));
        $original_bytes = 0;
        $webp_bytes = 0;
        $checked = 0;
        $webp_ready = 0;
        $webp_existing = 0;
        $larger_webp = 0;
        $uncovered_ready = 0;
        $uncovered_samples = [];
        $larger_webp_samples = [];

        foreach ($urls as $url) {
            if (!preg_match('/\.(jpe?g|png)(?:\?|$)/i', $url)) {
                continue;
            }

            $original_path = $this->upload_path_for_url($url);

            if (!$original_path || !file_exists($original_path)) {
                continue;
            }

            $checked++;
            $original_size = (int) filesize($original_path);
            $webp_path = $this->next_gen_path_for_url($url, 'webp');

            if ($webp_path && file_exists($webp_path)) {
                $webp_existing++;
                $original_bytes += $original_size;
                $webp_size = (int) filesize($webp_path);

                if (!$this->next_gen_copy_is_usable($original_path, $webp_path, 'webp')) {
                    $larger_webp++;

                    if (count($larger_webp_samples) < 8) {
                        $larger_webp_samples[] = [
                            'file' => basename((string) wp_parse_url($url, PHP_URL_PATH)),
                            'url' => esc_url_raw($url),
                            'original' => size_format($original_size, 1),
                            'webp' => size_format($webp_size, 1),
                        ];
                    }

                    continue;
                }

                $webp_ready++;
                $webp_bytes += $webp_size;

                if ($this->frontend_html_contains_uncovered_url($html_without_pictures, $url)) {
                    $uncovered_ready++;

                    if (count($uncovered_samples) < 8) {
                        $uncovered_samples[] = [
                            'file' => basename((string) wp_parse_url($url, PHP_URL_PATH)),
                            'url' => esc_url_raw($url),
                            'original' => size_format($original_size, 1),
                            'webp' => size_format($webp_size, 1),
                        ];
                    }
                }
            }
        }
        $covered_ready = max(0, $webp_ready - $uncovered_ready);

        return [
            'checked' => $checked,
            'webpReady' => $webp_ready,
            'webpExisting' => $webp_existing,
            'largerWebp' => $larger_webp,
            'coveredReady' => $covered_ready,
            'uncoveredReady' => $uncovered_ready,
            'coveragePercent' => $webp_ready > 0 ? (int) round(($covered_ready / $webp_ready) * 100) : 0,
            'uncoveredSamples' => $uncovered_samples,
            'largerWebpSamples' => $larger_webp_samples,
            'ignoredNonContentImages' => $ignored_non_content_images,
            'originalBytes' => $original_bytes,
            'webpBytes' => $webp_bytes,
            'savedBytes' => max(0, $original_bytes - $webp_bytes),
            'savedPercent' => $original_bytes > 0 ? round((($original_bytes - $webp_bytes) / $original_bytes) * 100) : 0,
            'originalFormatted' => size_format($original_bytes, 1),
            'webpFormatted' => size_format($webp_bytes, 1),
            'savedFormatted' => size_format(max(0, $original_bytes - $webp_bytes), 1),
        ];
    }

    private function frontend_content_image_html(string $html): string
    {
        $content_html = preg_replace('/<link\b(?=[^>]*\brel=["\'][^"\']*(?:icon|apple-touch-icon|mask-icon)[^"\']*["\'])[^>]*>/i', '', $html);
        $content_html = is_string($content_html) ? $content_html : $html;
        $content_html = preg_replace('/<meta\b(?=[^>]*(?:name|property)=["\'][^"\']*(?:msapplication-TileImage|og:image|twitter:image)[^"\']*["\'])[^>]*>/i', '', $content_html);

        return is_string($content_html) ? $content_html : $html;
    }

    private function frontend_html_contains_uncovered_url(string $html, string $url): bool
    {
        $path = (string) wp_parse_url($url, PHP_URL_PATH);

        if ('' === $path) {
            return false;
        }

        $decoded_url = html_entity_decode($url, ENT_QUOTES, get_bloginfo('charset'));

        return false !== strpos($html, $decoded_url) || false !== strpos($html, $path);
    }

    private function image_urls_from_html(string $html): array
    {
        $urls = [];

        if (preg_match_all('/(?:https?:)?\/\/[^\'"()\s<>]+?\.(?:jpe?g|png|webp)(?:\?[^\'"()\s<>]*)?/i', $html, $absolute_matches)) {
            foreach ($absolute_matches[0] as $url) {
                $urls[] = 0 === strpos($url, '//') ? (is_ssl() ? 'https:' : 'http:') . $url : $url;
            }
        }

        if (preg_match_all('/[\'"(]\s*(\/[^\'"()\s<>]+?\.(?:jpe?g|png|webp)(?:\?[^\'"()\s<>]*)?)/i', $html, $relative_matches)) {
            foreach ($relative_matches[1] as $url) {
                $urls[] = home_url($url);
            }
        }

        $urls = array_values(array_unique(array_map('esc_url_raw', $urls)));

        return array_slice(array_filter($urls), 0, 300);
    }

    public function ajax_activate_license(): void
    {
        check_ajax_referer('shrinkwell_activate_license_nonce', 'nonce');

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

        $post_data = wp_unslash($_POST); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verified above.
        $license_key = trim(sanitize_text_field((string) ($post_data['license_key'] ?? '')));

        if ('' === $license_key) {
            wp_send_json_error(['message' => __('Enter a license code first.', 'shrinkwell-image-optimizer')], 400);
        }

        if (!function_exists('shrinkwell_fs')) {
            wp_send_json_error(['message' => __('License activation is not available in this build.', 'shrinkwell-image-optimizer')], 400);
        }

        $freemius = shrinkwell_fs();
        if (!is_object($freemius) || !method_exists($freemius, 'opt_in')) {
            wp_send_json_error(['message' => __('License activation is not available right now. Please try again after refreshing the page.', 'shrinkwell-image-optimizer')], 400);
        }

        $user = wp_get_current_user();
        $result = $freemius->opt_in(
            (string) $user->user_email,
            (string) $user->user_firstname,
            (string) $user->user_lastname,
            $license_key,
            false,
            false,
            false,
            null,
            [],
            false
        );

        if (is_object($result) && isset($result->error)) {
            $message = is_object($result->error) && !empty($result->error->message)
                ? (string) $result->error->message
                : __('Freemius could not activate this license code.', 'shrinkwell-image-optimizer');
            wp_send_json_error(['message' => $message], 400);
        }

        if (false === $result || null === $result) {
            wp_send_json_error(['message' => __('Freemius could not activate this license code.', 'shrinkwell-image-optimizer')], 400);
        }

        wp_send_json_success([
            'message' => __('License code submitted. Refreshing Shrinkwell status...', 'shrinkwell-image-optimizer'),
            'reload' => true,
        ]);
    }

    public function ajax_seo_scan(): void
    {
        check_ajax_referer('shrinkwell_bulk_optimize', 'nonce');

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

        $query = new WP_Query([
            'post_type' => 'attachment',
            'post_mime_type' => ['image/jpeg', 'image/png', 'image/webp'],
            'post_status' => 'inherit',
            'fields' => 'ids',
            'posts_per_page' => 250,
            'orderby' => 'ID',
            'order' => 'DESC',
            'no_found_rows' => true,
            'meta_query' => [
                'relation' => 'OR',
                [
                    'key' => '_wp_attachment_image_alt',
                    'compare' => 'NOT EXISTS',
                ],
                [
                    'key' => '_wp_attachment_image_alt',
                    'value' => '',
                    'compare' => '=',
                ],
            ],
        ]);
        $issues = [];
        $missing_alt = 0;
        $weak_filename = 0;
        $scanned = 0;

        foreach ($query->posts as $attachment_id) {
            $attachment_id = (int) $attachment_id;
            $file = get_attached_file($attachment_id);

            if (!$file || !file_exists($file)) {
                continue;
            }

            $scanned++;
            $alt = trim((string) get_post_meta($attachment_id, '_wp_attachment_image_alt', true));
            $filename = basename($file);
            $item_issues = [];
            $weak_alt = '' !== $alt && $this->is_weak_seo_text($alt, $filename);
            $weak_image_filename = $this->is_weak_image_filename($filename);

            if ('' === $alt) {
                $missing_alt++;
                $item_issues[] = __('Missing alt text', 'shrinkwell-image-optimizer');
            } elseif ($weak_alt) {
                $item_issues[] = __('Weak alt text', 'shrinkwell-image-optimizer');
            }

            if ($weak_image_filename) {
                $weak_filename++;
                $item_issues[] = __('Weak filename', 'shrinkwell-image-optimizer');
            }

            if ($item_issues && count($issues) < 250) {
                $title = $this->attachment_title($attachment_id, $filename);
                $suggestion = $this->seo_alt_suggestion($attachment_id, $title, $filename, $alt);
                $thumb = wp_get_attachment_image_src($attachment_id, 'thumbnail');
                $issues[] = [
                    'id' => $attachment_id,
                    'title' => $title,
                    'filename' => $filename,
                    'alt' => $alt,
                    'missingAlt' => '' === $alt,
                    'weakFilename' => $weak_image_filename,
                    'currentTitle' => get_the_title($attachment_id),
                    'suggestion' => $suggestion,
                    'safeSuggestion' => '' !== $suggestion,
                    'statusType' => '' === $alt ? 'missing' : 'attention',
                    'status' => '' === $alt ? __('Missing important SEO data', 'shrinkwell-image-optimizer') : __('Needs attention', 'shrinkwell-image-optimizer'),
                    'issues' => $item_issues,
                    'thumbnail' => is_array($thumb) ? (string) $thumb[0] : '',
                    'editUrl' => get_edit_post_link($attachment_id, 'raw'),
                ];
            }
        }

        update_option(self::CHECKS_OPTION, array_merge($this->checks(), [
            'seo_scanned_at' => current_time('mysql'),
            'seo_scanned' => $scanned,
            'seo_issues' => $missing_alt + $weak_filename,
        ]), false);
        update_option('shrinkwell_seo_missing_alt_count', $missing_alt, false);

        wp_send_json_success([
            'scanned' => $scanned,
            'missingAlt' => $missing_alt,
            'weakFilename' => $weak_filename,
            'issues' => $issues,
            'message' => __('Shrinkwell only reports these issues. Review images manually before changing alt text or filenames.', 'shrinkwell-image-optimizer'),
        ]);
    }

    public function ajax_save_seo_fields(): void
    {
        check_ajax_referer('shrinkwell_bulk_optimize', 'nonce');

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

        $post_data = wp_unslash($_POST);
        $attachment_id = absint($post_data['attachment_id'] ?? 0);
        $alt = sanitize_text_field(wp_specialchars_decode((string) ($post_data['alt'] ?? ''), ENT_QUOTES));
        $title = sanitize_text_field(wp_specialchars_decode((string) ($post_data['title'] ?? ''), ENT_QUOTES));

        if (!$attachment_id || 'attachment' !== get_post_type($attachment_id)) {
            wp_send_json_error(['message' => __('Image not found.', 'shrinkwell-image-optimizer')], 404);
        }

        update_post_meta($attachment_id, '_wp_attachment_image_alt', $alt);

        if ('' !== $title) {
            $updated = wp_update_post([
                'ID' => $attachment_id,
                'post_title' => $title,
            ], true);

            if (is_wp_error($updated)) {
                wp_send_json_error(['message' => $updated->get_error_message()]);
            }
        }

        wp_send_json_success([
            'message' => __('Image SEO data saved.', 'shrinkwell-image-optimizer'),
            'alt' => $alt,
            'title' => '' !== $title ? $title : get_the_title($attachment_id),
        ]);
    }

    private function is_weak_image_filename(string $filename): bool
    {
        $name = strtolower((string) pathinfo($filename, PATHINFO_FILENAME));
        $name = preg_replace('/-\d+x\d+$/', '', $name);

        if (!$name) {
            return true;
        }

        if (preg_match('/^(img|dsc|image|photo|screenshot|screen-shot|untitled|whatsapp-image)[-_ ]?\d*$/', $name)) {
            return true;
        }

        if (preg_match('/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/', $name)) {
            return true;
        }

        $letters = preg_replace('/[^a-z]/', '', $name);

        return strlen((string) $letters) < 4;
    }

    private function seo_alt_suggestion(int $attachment_id, string $title, string $filename, string $alt = ''): string
    {
        $candidates = [
            ['value' => $alt, 'allow_filename_match' => false],
            ['value' => $title, 'allow_filename_match' => true],
            ['value' => (string) get_post_field('post_excerpt', $attachment_id), 'allow_filename_match' => false],
            ['value' => (string) get_post_field('post_content', $attachment_id), 'allow_filename_match' => false],
            ['value' => (string) pathinfo($filename, PATHINFO_FILENAME), 'allow_filename_match' => true],
        ];

        $parent_id = (int) wp_get_post_parent_id($attachment_id);
        if ($parent_id > 0) {
            $candidates[] = ['value' => (string) get_the_title($parent_id), 'allow_filename_match' => false];
        }

        foreach ($candidates as $candidate) {
            $source = $this->clean_seo_text((string) ($candidate['value'] ?? ''));

            if ('' === $source || $this->is_weak_seo_text($source, $filename, !empty($candidate['allow_filename_match']))) {
                continue;
            }

            return $source;
        }

        return '';
    }

    private function clean_seo_text(string $value): string
    {
        $value = wp_strip_all_tags(wp_specialchars_decode($value, ENT_QUOTES));
        $value = preg_replace('/\.[a-z0-9]{2,5}$/i', '', $value);
        $value = preg_replace('/-\d+x\d+$/', '', (string) $value);
        $value = preg_replace('/[-_]+/', ' ', (string) $value);
        $value = preg_replace('/\s+/', ' ', (string) $value);

        return trim(sanitize_text_field((string) $value));
    }

    private function is_weak_seo_text(string $value, string $filename = '', bool $allow_filename_match = false): bool
    {
        $clean = strtolower($this->clean_seo_text($value));

        if ('' === $clean) {
            return true;
        }

        $filename_clean = $filename ? strtolower($this->clean_seo_text((string) pathinfo($filename, PATHINFO_FILENAME))) : '';

        if (!$allow_filename_match && $filename_clean && $clean === $filename_clean) {
            return true;
        }

        if (preg_match('/^(img|dsc|image|photo|screenshot|screen shot|untitled|whatsapp image|chatgpt image)(\s+\d+|\s+[a-z0-9]{4,}|\s+\d{4}.*)?$/i', $clean)) {
            return true;
        }

        if (preg_match('/^[a-f0-9]{8}(?:\s|-)[a-f0-9]{4}(?:\s|-)[a-f0-9]{4}(?:\s|-)[a-f0-9]{4}(?:\s|-)[a-f0-9]{12}$/', $clean)) {
            return true;
        }

        $letters = preg_replace('/[^a-z]/', '', $clean);

        return strlen((string) $letters) < 4;
    }

    private function dimensions_text_for_path(string $path): string
    {
        $dimensions = wp_getimagesize($path);

        if (!is_array($dimensions) || empty($dimensions[0]) || empty($dimensions[1])) {
            return __('unknown dimensions', 'shrinkwell-image-optimizer');
        }

        return sprintf(
            /* translators: 1: Image width in pixels. 2: Image height in pixels. */
            __('%1$d x %2$d px', 'shrinkwell-image-optimizer'),
            (int) $dimensions[0],
            (int) $dimensions[1]
        );
    }

    public function ajax_recommendation_scan(): void
    {
        check_ajax_referer('shrinkwell_bulk_optimize', 'nonce');

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

        $query = new WP_Query([
            'post_type' => 'attachment',
            'post_mime_type' => ['image/jpeg', 'image/png', 'image/webp'],
            'post_status' => 'inherit',
            'fields' => 'ids',
            'posts_per_page' => 100,
            'orderby' => 'ID',
            'order' => 'DESC',
        ]);

        $total_files = 0;
        $total_bytes = 0;
        $large_files = 0;
        $oversized_images = 0;

        foreach ($query->posts as $attachment_id) {
            $file = get_attached_file((int) $attachment_id);

            if (!$file || !file_exists($file)) {
                continue;
            }

            $total_files++;
            $bytes = (int) filesize($file);
            $total_bytes += $bytes;

            if ($bytes >= 700 * 1024) {
                $large_files++;
            }

            $dimensions = wp_getimagesize($file);

            if (is_array($dimensions) && ((int) $dimensions[0] > 2560 || (int) $dimensions[1] > 2560)) {
                $oversized_images++;
            }
        }

        $average_bytes = $total_files ? (int) floor($total_bytes / $total_files) : 0;
        $server_status = $this->server_status();
        $is_woocommerce_site = class_exists('WooCommerce') || post_type_exists('product');
        $profile = 'balanced';
        $profile_reason = __('Balanced is the best starting point for most sites because it reduces file size without making photos look visibly compressed.', 'shrinkwell-image-optimizer');

        if ($is_woocommerce_site && $total_files) {
            $profile = 'high';
            $profile_reason = __('High compression is recommended for WooCommerce catalogs because product grids load many images and smaller files usually matter more than preserving every source pixel.', 'shrinkwell-image-optimizer');
        } elseif ($total_files && ($average_bytes >= 700 * 1024 || $large_files >= max(3, (int) floor($total_files * 0.35)))) {
            $profile = 'high';
            $profile_reason = __('High compression is recommended because this site has many large image files. It should improve loading speed the most.', 'shrinkwell-image-optimizer');
        } elseif ($total_files && $average_bytes < 250 * 1024 && $large_files <= 1) {
            $profile = 'lossless';
            $profile_reason = __('Near lossless is enough because the sampled images are already fairly small. This keeps quality very close to the original.', 'shrinkwell-image-optimizer');
        }

        $suggested_settings = [
            'compression_profile' => $profile,
            'quality' => (int) (self::compression_profiles()[$profile]['quality'] ?? 82),
            'auto_optimize' => 1,
            'optimize_thumbnails' => 1,
            'strip_exif' => 1,
            'create_webp' => $server_status['webp_create'] ? 1 : 0,
            'serve_webp' => $server_status['webp_create'] ? 1 : 0,
            'create_avif' => $server_status['avif'] ? 1 : 0,
            'serve_avif' => $server_status['avif'] ? 1 : 0,
            'max_width' => $oversized_images ? 2560 : (int) $this->settings()['max_width'],
            'max_height' => $oversized_images ? 0 : (int) $this->settings()['max_height'],
            'backup_originals' => 1,
        ];

        $recommendations = [
            [
                'label' => __('Compression type', 'shrinkwell-image-optimizer'),
                'value' => self::compression_profiles()[$profile]['label'],
                'reason' => $profile_reason,
            ],
            [
                'label' => __('Generate WebP versions', 'shrinkwell-image-optimizer'),
                'value' => $server_status['webp_create'] ? __('On', 'shrinkwell-image-optimizer') : __('Off', 'shrinkwell-image-optimizer'),
                'reason' => $server_status['webp_create']
                    ? __('Recommended because WebP is widely supported and usually makes images smaller.', 'shrinkwell-image-optimizer')
                    : __('This server does not report WebP creation support.', 'shrinkwell-image-optimizer'),
            ],
            [
                'label' => __('Use WebP automatically', 'shrinkwell-image-optimizer'),
                'value' => $server_status['webp_create'] ? __('On', 'shrinkwell-image-optimizer') : __('Off', 'shrinkwell-image-optimizer'),
                'reason' => __('Keeps JPEG/PNG fallback while serving smaller files to modern browsers.', 'shrinkwell-image-optimizer'),
            ],
            [
                'label' => __('Generate AVIF versions', 'shrinkwell-image-optimizer'),
                'value' => $server_status['avif'] ? __('On', 'shrinkwell-image-optimizer') : __('Off', 'shrinkwell-image-optimizer'),
                'reason' => $server_status['avif']
                    ? __('Recommended when this server passes the AVIF encoder check. Shrinkwell still skips files that exceed safe local AVIF processing limits.', 'shrinkwell-image-optimizer')
                    : __('This server does not report AVIF creation support. WebP delivery can still be used as the stable fallback.', 'shrinkwell-image-optimizer'),
            ],
            [
                'label' => __('Resize large originals', 'shrinkwell-image-optimizer'),
                'value' => $oversized_images ? __('On, max width 2560px', 'shrinkwell-image-optimizer') : __('Keep current setting', 'shrinkwell-image-optimizer'),
                'reason' => $oversized_images
                    ? __('Some images are larger than needed for most websites. Resizing originals can save a lot of space.', 'shrinkwell-image-optimizer')
                    : __('The sampled images do not look oversized.', 'shrinkwell-image-optimizer'),
            ],
            [
                'label' => __('Optimize thumbnails', 'shrinkwell-image-optimizer'),
                'value' => __('On', 'shrinkwell-image-optimizer'),
                'reason' => __('Recommended because WordPress themes often load thumbnails in grids, archives, and product cards.', 'shrinkwell-image-optimizer'),
            ],
        ];

        wp_send_json_success([
            'sampled' => $total_files,
            'averageSize' => size_format($average_bytes, 1),
            'largeFiles' => $large_files,
            'oversizedImages' => $oversized_images,
            'recommendations' => $recommendations,
            'settings' => $suggested_settings,
        ]);
    }

    public function ajax_apply_recommendations(): void
    {
        check_ajax_referer('shrinkwell_bulk_optimize', 'nonce');

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

        $raw_settings = isset($_POST['settings']) ? sanitize_textarea_field(wp_unslash($_POST['settings'])) : '';
        $settings = is_string($raw_settings) ? json_decode($raw_settings, true) : [];

        if (!is_array($settings)) {
            wp_send_json_error(['message' => __('No recommended settings were received.', 'shrinkwell-image-optimizer')]);
        }

        $allowed_keys = array_keys(self::default_settings());
        $current = $this->settings();
        $filtered = [];

        foreach ($allowed_keys as $key) {
            if (array_key_exists($key, $settings)) {
                $filtered[$key] = $settings[$key];
            }
        }

        $new_settings = $this->sanitize_settings(array_merge($current, $filtered));
        update_option(self::OPTION, $new_settings, false);

        wp_send_json_success([
            'settings' => $new_settings,
            'message' => __('Recommended settings have been saved. Shrinkwell will now optimize existing images.', 'shrinkwell-image-optimizer'),
        ]);
    }

    private function test_attachment_compression(int $attachment_id)
    {
        if (!$this->is_optimizable_attachment($attachment_id)) {
            return new WP_Error('shrinkwell_unsupported', __('Shrinkwell does not recompress existing WebP media items. WebP files are already a modern delivery format.', 'shrinkwell-image-optimizer'));
        }

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

        $metadata = (array) wp_get_attachment_metadata($attachment_id);
        $file = get_attached_file($attachment_id);

        if (!$file || !file_exists($file)) {
            return new WP_Error('shrinkwell_missing_file', __('Original image file is missing.', 'shrinkwell-image-optimizer'));
        }

        $tested_files = 0;
        $total_before = 0;
        $total_after = 0;
        $first_error = null;

        foreach ($this->attachment_files((string) $file, $metadata) as $path) {
            if (!file_exists($path) || !$this->is_optimizable_path($path)) {
                continue;
            }

            $result = $this->test_file_compression($path, $path === $file);

            if (is_wp_error($result)) {
                $first_error = $first_error ?: $result;
                continue;
            }

            $tested_files++;
            $total_before += (int) $result['before'];
            $total_after += (int) $result['after'];
        }

        if (!$tested_files) {
            return $first_error ?: new WP_Error('shrinkwell_no_files_processed', __('No image files could be processed for this attachment.', 'shrinkwell-image-optimizer'));
        }

        return [
            'attachment_id' => $attachment_id,
            'tested_files' => $tested_files,
            'before_bytes' => $total_before,
            'after_bytes' => $total_after,
            'saved_bytes' => max(0, $total_before - $total_after),
        ];
    }

    private function test_file_compression(string $path, bool $allow_resize = false)
    {
        $settings = $this->settings();
        $before = filesize($path);

        if (!$before) {
            return new WP_Error('shrinkwell_empty_file', __('Image file is empty.', 'shrinkwell-image-optimizer'));
        }

        $local_check = $this->check_local_processing_limits($path, (int) $before);

        if (is_wp_error($local_check)) {
            return $local_check;
        }

        if (function_exists('wp_raise_memory_limit')) {
            wp_raise_memory_limit('image');
        }

        $editor = wp_get_image_editor($path);

        if (is_wp_error($editor)) {
            return $editor;
        }

        $size = $editor->get_size();
        $max_width = (int) $settings['max_width'];
        $max_height = (int) $settings['max_height'];

        if ($allow_resize && (($max_width && $size['width'] > $max_width) || ($max_height && $size['height'] > $max_height))) {
            $editor->resize($max_width ?: null, $max_height ?: null, false);
        }

        $editor->set_quality($this->quality_for_settings($settings));

        $mime = $this->mime_for_path($path);
        $temp_path = $this->temp_path($path);
        $saved = $editor->save($temp_path, $mime);

        if (is_wp_error($saved) || empty($saved['path']) || !file_exists($saved['path'])) {
            return is_wp_error($saved) ? $saved : new WP_Error('shrinkwell_save_failed', __('Could not save optimized image.', 'shrinkwell-image-optimizer'));
        }

        $temp_size = (int) filesize($saved['path']);
        $would_replace = $temp_size > 0 && $temp_size < (int) $before;

        if (!$would_replace && !empty($settings['strip_exif']) && $temp_size > 0 && $temp_size <= (int) round((int) $before * 1.05)) {
            $would_replace = true;
        }

        wp_delete_file($saved['path']);

        return [
            'before' => (int) $before,
            'after' => $would_replace ? $temp_size : (int) $before,
            'would_replace' => $would_replace,
        ];
    }

    private function attachment_needs_webp_generation(int $attachment_id): bool
    {
        if (!$this->can_create_webp_locally()) {
            return false;
        }

        $metadata = (array) wp_get_attachment_metadata($attachment_id);
        $file = get_attached_file($attachment_id);

        if (!$file || !file_exists($file)) {
            return false;
        }

        foreach ($this->attachment_files($file, $metadata) as $path) {
            if (!file_exists($path) || !$this->is_supported_path($path)) {
                continue;
            }

            if ('image/webp' === $this->mime_for_path($path)) {
                continue;
            }

            $webp_path = preg_replace('/\.(jpe?g|png|webp)$/i', '.webp', $path);

            if ($webp_path && !file_exists($webp_path)) {
                return true;
            }
        }

        return false;
    }

    private function generate_webp_for_attachment(int $attachment_id, ?array $metadata = null)
    {
        if (!$this->is_supported_attachment($attachment_id) || !$this->can_create_webp_locally()) {
            return new WP_Error('shrinkwell_webp_unavailable', __('WebP generation is not available on this server.', 'shrinkwell-image-optimizer'));
        }

        $metadata = $metadata ?: (array) wp_get_attachment_metadata($attachment_id);
        $file = get_attached_file($attachment_id);

        if (!$file || !file_exists($file)) {
            return new WP_Error('shrinkwell_missing_file', __('Original image file is missing.', 'shrinkwell-image-optimizer'));
        }

        $files = $this->attachment_files($file, $metadata);
        $usage_tracker = $this->usage_tracker();
        $usage_bytes = $this->usage_bytes_for_attachment_files($files);

        if ($usage_tracker && method_exists($usage_tracker, 'can_optimize_bytes') && !$usage_tracker->can_optimize_bytes($usage_bytes)) {
            return $this->monthly_usage_limit_error();
        }

        $webp_files = [];
        $payload_saved = 0;
        $first_error = null;

        foreach ($files as $path) {
            if (!file_exists($path) || !$this->is_supported_path($path) || 'image/webp' === $this->mime_for_path($path)) {
                continue;
            }

            $bytes = (int) filesize($path);
            $local_check = $this->check_local_processing_limits($path, $bytes);

            if (is_wp_error($local_check)) {
                $first_error = $first_error ?: $local_check;
                continue;
            }

            $webp_path = $this->create_webp_copy($path);

            if (!$webp_path || !file_exists($webp_path)) {
                continue;
            }

            $webp_files[] = $webp_path;
            $payload_saved += max(0, $bytes - (int) filesize($webp_path));
        }

        if (!$webp_files) {
            return $first_error ?: new WP_Error('shrinkwell_no_webp_generated', __('No WebP copy was needed or worth keeping for this image. The source may already be WebP, existing WebP files may already be present, or the generated WebP was not smaller than the original.', 'shrinkwell-image-optimizer'));
        }

        $previous_meta = (array) get_post_meta($attachment_id, self::META, true);
        update_post_meta($attachment_id, self::META, array_merge($previous_meta, [
            'webp_generated_at' => current_time('mysql'),
            'webp_files' => (int) ($previous_meta['webp_files'] ?? 0) + count($webp_files),
            'quality' => $this->quality_for_settings($this->settings()),
            'settings_signature' => $previous_meta['settings_signature'] ?? $this->optimization_signature(),
        ]));

        if ($usage_tracker && method_exists($usage_tracker, 'add_usage_bytes')) {
            $usage_tracker->add_usage_bytes($usage_bytes);
        }

        return [
            'attachment_id' => $attachment_id,
            'saved_bytes' => $payload_saved,
            'webp_files' => count($webp_files),
        ];
    }

    public function optimize_attachment(int $attachment_id, ?array $metadata = null)
    {
        if (!$this->is_optimizable_attachment($attachment_id)) {
            return new WP_Error('shrinkwell_unsupported', __('Shrinkwell does not recompress existing WebP media items. WebP files are already a modern delivery format.', 'shrinkwell-image-optimizer'));
        }

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

        $metadata = $metadata ?: (array) wp_get_attachment_metadata($attachment_id);
        $file = get_attached_file($attachment_id);

        if (!$file || !file_exists($file)) {
            return new WP_Error('shrinkwell_missing_file', __('Original image file is missing.', 'shrinkwell-image-optimizer'));
        }

        $files = $this->attachment_files($file, $metadata);
        $usage_tracker = $this->usage_tracker();
        $usage_bytes = $this->usage_bytes_for_attachment_files($files);

        if (
            $usage_tracker
            && method_exists($usage_tracker, 'can_optimize_bytes')
            && !$usage_tracker->can_optimize_bytes($usage_bytes)
        ) {
            return $this->monthly_usage_limit_error();
        }

        $total_before = 0;
        $total_after = 0;
        $optimized_files = [];
        $webp_files = [];
        $avif_files = [];
        $first_error = null;

        foreach ($files as $path) {
            if (!file_exists($path) || !$this->is_optimizable_path($path)) {
                continue;
            }

            $result = $this->optimize_file($path, $path === $file);

            if (is_wp_error($result)) {
                if (!$first_error) {
                    $first_error = $result;
                }
                continue;
            }

            $total_before += (int) $result['before'];
            $total_after += (int) $result['after'];

            if (!empty($result['optimized'])) {
                $optimized_files[] = $path;
            }

            if (!empty($result['webp'])) {
                $webp_files[] = $result['webp'];
            }

            if (!empty($result['avif'])) {
                $avif_files[] = $result['avif'];
            }
        }

        if (0 === $total_before) {
            return $first_error ?: new WP_Error('shrinkwell_no_files_processed', __('No image files could be processed for this attachment.', 'shrinkwell-image-optimizer'));
        }

        $main_saved_bytes = max(0, $total_before - $total_after);
        $delivery_saved_bytes = 0;

        foreach ($files as $path) {
            if (!file_exists($path) || !$this->is_optimizable_path($path)) {
                continue;
            }

            $delivery_saved_bytes += $this->best_next_gen_savings_for_path($path);
        }

        $saved_bytes = $main_saved_bytes + $delivery_saved_bytes;
        $previous_meta = (array) get_post_meta($attachment_id, self::META, true);
        $previous_saved = isset($previous_meta['saved_bytes']) ? (int) $previous_meta['saved_bytes'] : 0;

        update_post_meta($attachment_id, self::META, [
            'optimized_at' => current_time('mysql'),
            'saved_bytes' => $saved_bytes,
            'main_saved_bytes' => $main_saved_bytes,
            'delivery_saved_bytes' => $delivery_saved_bytes,
            'optimized_files' => count($optimized_files),
            'webp_files' => count($webp_files),
            'avif_files' => count($avif_files),
            'compression_profile' => (string) $this->settings()['compression_profile'],
            'quality' => $this->quality_for_settings($this->settings()),
            'settings_signature' => $this->optimization_signature(),
        ]);
        delete_transient(self::ZERO_SAVINGS_TRANSIENT);
        delete_transient(self::SAVINGS_PERCENT_TRANSIENT);
        delete_transient('shrinkwell_media_counts');

        if (!empty($metadata)) {
            $metadata['filesize'] = file_exists($file) ? filesize($file) : ($metadata['filesize'] ?? 0);
            $metadata = $this->refresh_original_dimensions($file, $metadata);
            wp_update_attachment_metadata($attachment_id, $metadata);
        }

        $this->update_global_stats($previous_saved, $saved_bytes, empty($previous_meta['optimized_at']));

        if (
            $usage_tracker
            && ($optimized_files || $webp_files || $avif_files)
            && method_exists($usage_tracker, 'add_usage_bytes')
        ) {
            $usage_tracker->add_usage_bytes($usage_bytes);
        }

        /**
         * Fires after an attachment has been processed by Shrinkwell.
         *
         * @param int   $attachment_id Attachment ID.
         * @param array $result        Optimization result summary.
         */
        do_action('shrinkwell_after_optimization', $attachment_id, [
            'saved_bytes' => $saved_bytes,
            'optimized_files' => count($optimized_files),
            'webp_files' => count($webp_files),
            'avif_files' => count($avif_files),
        ]);

        return [
            'attachment_id' => $attachment_id,
            'saved_bytes' => $saved_bytes,
            'optimized_files' => count($optimized_files),
            'webp_files' => count($webp_files),
            'avif_files' => count($avif_files),
            'metadata' => $metadata,
        ];
    }

    public function restore_attachment(int $attachment_id)
    {
        $metadata = (array) wp_get_attachment_metadata($attachment_id);
        $file = get_attached_file($attachment_id);

        if (!$file) {
            return new WP_Error('shrinkwell_missing_file', __('WordPress has no file path for this media item, so Shrinkwell cannot find a matching backup.', 'shrinkwell-image-optimizer'));
        }

        $restored = 0;
        $recreated_missing = 0;
        $copy_errors = [];

        foreach ($this->attachment_files($file, $metadata, true) as $path) {
            $backup_path = $path . self::BACKUP_SUFFIX;

            if (!file_exists($backup_path)) {
                continue;
            }

            if (!$this->directory_is_writable(dirname($path))) {
                $copy_errors[] = sprintf(
                    /* translators: %s: Target filename. */
                    __('Could not restore %s because the target folder is not writable.', 'shrinkwell-image-optimizer'),
                    wp_basename($path)
                );
                continue;
            }

            $was_missing = !file_exists($path);

            if (!copy($backup_path, $path)) {
                $copy_errors[] = sprintf(
                    /* translators: %s: Target filename. */
                    __('Could not copy the backup back to %s.', 'shrinkwell-image-optimizer'),
                    wp_basename($path)
                );
                continue;
            }

            $restored++;
            $recreated_missing += $was_missing ? 1 : 0;
            $this->delete_webp_copy($path);
        }

        if (!$restored) {
            if ($copy_errors) {
                return new WP_Error('shrinkwell_restore_failed', implode(' ', $copy_errors));
            }

            return new WP_Error(
                'shrinkwell_no_backup',
                sprintf(
                    /* translators: %s: Current media filename. */
                    __('No matching Shrinkwell backup files were found for %s. WordPress still has the media record, but the file/backups on disk no longer match this attachment.', 'shrinkwell-image-optimizer'),
                    wp_basename($file)
                )
            );
        }

        $previous_meta = (array) get_post_meta($attachment_id, self::META, true);
        $previous_saved = isset($previous_meta['saved_bytes']) ? (int) $previous_meta['saved_bytes'] : 0;

        delete_post_meta($attachment_id, self::META);
        delete_transient(self::ZERO_SAVINGS_TRANSIENT);
        delete_transient(self::SAVINGS_PERCENT_TRANSIENT);
        delete_transient('shrinkwell_media_counts');

        if (!empty($previous_meta['optimized_at'])) {
            $this->remove_from_global_stats($previous_saved);
        }

        $metadata['filesize'] = file_exists($file) ? filesize($file) : ($metadata['filesize'] ?? 0);
        $metadata = $this->refresh_original_dimensions($file, $metadata);
        wp_update_attachment_metadata($attachment_id, $metadata);

        return [
            'attachment_id' => $attachment_id,
            'restored_files' => $restored,
            'recreated_missing_files' => $recreated_missing,
        ];
    }

    private function check_local_processing_limits(string $path, int $bytes)
    {
        $settings = $this->settings();
        $max_mb = min(100, max(1, (int) ($settings['max_file_size_mb'] ?? 10)));
        $max_bytes = (int) apply_filters('shrinkwell_max_local_source_bytes', $max_mb * 1024 * 1024);
        $dimensions = wp_getimagesize($path);
        $width = is_array($dimensions) && !empty($dimensions[0]) ? (int) $dimensions[0] : 0;
        $height = is_array($dimensions) && !empty($dimensions[1]) ? (int) $dimensions[1] : 0;
        $dimension_text = $width && $height ? sprintf('%1$d x %2$d px', $width, $height) : __('unknown dimensions', 'shrinkwell-image-optimizer');
        $filename = wp_basename($path);

        if ($max_bytes > 0 && $bytes > $max_bytes) {
            return new WP_Error(
                'shrinkwell_local_file_too_large',
                sprintf(
                    /* translators: 1: Filename. 2: Current file size. 3: Current image dimensions. 4: File size limit. */
                    __('Image skipped: %1$s is %2$s and %3$s. Shrinkwell is currently set to optimize files up to %4$s. Reduce the file size, increase the limit in Advanced settings, or optimize this image before upload.', 'shrinkwell-image-optimizer'),
                    $filename,
                    size_format($bytes, 1),
                    $dimension_text,
                    size_format($max_bytes, 1)
                )
            );
        }

        if (!$width || !$height) {
            return true;
        }

        $max_pixels = (int) apply_filters('shrinkwell_max_local_source_pixels', 20 * 1000 * 1000);
        $pixels = $width * $height;

        if ($max_pixels > 0 && $pixels > $max_pixels) {
            return new WP_Error(
                'shrinkwell_local_dimensions_too_large',
                sprintf(
                    /* translators: 1: Filename. 2: Current image dimensions. 3: Pixel limit. 4: Current file size. */
                    __('Image skipped: %1$s is %2$s and %4$s. The safe local processing limit is about %3$s pixels on this hosting setup. Reduce the dimensions first, increase PHP memory, or raise the limit with the shrinkwell_max_local_source_pixels filter.', 'shrinkwell-image-optimizer'),
                    $filename,
                    $dimension_text,
                    number_format_i18n($max_pixels),
                    size_format($bytes, 1)
                )
            );
        }

        return true;
    }

    private function can_create_avif_locally(string $path, int $bytes): bool
    {
        $max_bytes = (int) apply_filters('shrinkwell_max_local_avif_source_bytes', 2 * 1024 * 1024);

        if ($max_bytes > 0 && $bytes > $max_bytes) {
            return false;
        }

        $dimensions = wp_getimagesize($path);

        if (!is_array($dimensions) || empty($dimensions[0]) || empty($dimensions[1])) {
            return true;
        }

        $max_pixels = (int) apply_filters('shrinkwell_max_local_avif_source_pixels', 4 * 1000 * 1000);

        return $max_pixels <= 0 || ((int) $dimensions[0] * (int) $dimensions[1]) <= $max_pixels;
    }

    private function optimize_file(string $path, bool $allow_resize = false)
    {
        $settings = $this->settings();
        $before = filesize($path);

        if (!$before) {
            return new WP_Error('shrinkwell_empty_file', __('Image file is empty.', 'shrinkwell-image-optimizer'));
        }

        $local_check = $this->check_local_processing_limits($path, (int) $before);

        if (is_wp_error($local_check)) {
            return $local_check;
        }

        if (!empty($settings['backup_originals'])) {
            if (!$this->backup_file($path)) {
                return new WP_Error(
                    'shrinkwell_backup_failed',
                    __('Could not create a backup before optimizing. Check folder permissions.', 'shrinkwell-image-optimizer')
                );
            }
        }

        if (function_exists('wp_raise_memory_limit')) {
            wp_raise_memory_limit('image');
        }

        $editor = wp_get_image_editor($path);

        if (is_wp_error($editor)) {
            return $editor;
        }

        $size = $editor->get_size();
        $max_width = (int) $settings['max_width'];
        $max_height = (int) $settings['max_height'];

        if ($allow_resize && (($max_width && $size['width'] > $max_width) || ($max_height && $size['height'] > $max_height))) {
            $editor->resize($max_width ?: null, $max_height ?: null, false);
        }

        $editor->set_quality($this->quality_for_settings($settings));

        $mime = $this->mime_for_path($path);
        $temp_path = $this->temp_path($path);
        $saved = $editor->save($temp_path, $mime);

        if (is_wp_error($saved) || empty($saved['path']) || !file_exists($saved['path'])) {
            return is_wp_error($saved) ? $saved : new WP_Error('shrinkwell_save_failed', __('Could not save optimized image.', 'shrinkwell-image-optimizer'));
        }

        $temp_size = filesize($saved['path']);
        $optimized = false;

        $should_replace = $temp_size > 0 && $temp_size < $before;

        if (!$should_replace && !empty($settings['strip_exif']) && $temp_size > 0 && $temp_size <= (int) round($before * 1.05)) {
            $should_replace = true;
        }

        if ($should_replace) {
            if (!copy($saved['path'], $path)) {
                wp_delete_file($saved['path']);
                return new WP_Error('shrinkwell_replace_failed', __('Could not replace the original image with the optimized version.', 'shrinkwell-image-optimizer'));
            }

            wp_delete_file($saved['path']);
            $optimized = true;
        } else {
            wp_delete_file($saved['path']);
        }

        clearstatcache(true, $path);

        $webp = null;
        if (!empty($settings['create_webp']) && $this->can_create_webp_locally()) {
            $webp = $this->create_webp_copy($path);
        }

        $avif = null;
        if (!empty($settings['create_avif']) && $this->server_can_use_avif() && $this->can_create_avif_locally($path, (int) filesize($path))) {
            $avif = $this->create_avif_copy($path);
        }

        return [
            'before' => $before,
            'after' => filesize($path),
            'optimized' => $optimized,
            'webp' => $webp,
            'avif' => $avif,
        ];
    }

    private function create_webp_copy(string $path): ?string
    {
        return $this->create_next_gen_copy($path, 'webp', 'image/webp');
    }

    private function create_avif_copy(string $path): ?string
    {
        return $this->create_next_gen_copy($path, 'avif', 'image/avif');
    }

    private function best_next_gen_savings_for_path(string $path): int
    {
        if (!file_exists($path)) {
            return 0;
        }

        $source_size = (int) filesize($path);
        $best_savings = 0;

        foreach (['webp', 'avif'] as $extension) {
            $next_path = preg_replace('/\.(jpe?g|png|webp)$/i', '.' . $extension, $path);

            if (!$next_path || !file_exists($next_path)) {
                continue;
            }

            $best_savings = max($best_savings, $source_size - (int) filesize($next_path));
        }

        return max(0, $best_savings);
    }

    private function create_next_gen_copy(string $path, string $extension, string $mime_type): ?string
    {
        $mime = $this->mime_for_path($path);
        $current_extension = strtolower((string) pathinfo($path, PATHINFO_EXTENSION));
        $bytes = file_exists($path) ? (int) filesize($path) : 0;

        if (!in_array($mime, ['image/jpeg', 'image/png', 'image/webp'], true)) {
            return null;
        }

        if ($current_extension === $extension || $mime === $mime_type) {
            return null;
        }

        if (is_wp_error($this->check_local_processing_limits($path, $bytes))) {
            return null;
        }

        if ('avif' === $extension && !$this->can_create_avif_locally($path, $bytes)) {
            return null;
        }

        $next_path = preg_replace('/\.(jpe?g|png|webp)$/i', '.' . $extension, $path);

        if (!$next_path) {
            return null;
        }

        if ($this->path_is_registered_media_attachment($next_path)) {
            return null;
        }

        if (file_exists($next_path)) {
            if ($this->is_next_gen_copy_worth_keeping($path, $next_path, $extension)) {
                return $next_path;
            }

            $this->delete_generated_file($next_path);
        }

        if ('webp' === $extension) {
            $cwebp_result = $this->create_webp_copy_with_cwebp($path, $next_path);

            if ($cwebp_result) {
                return $cwebp_result;
            }
        }

        $crash_marker = $next_path . '.shrinkwell-crashed';

        if (file_exists($crash_marker)) {
            return null;
        }

        if (function_exists('wp_raise_memory_limit')) {
            wp_raise_memory_limit('image');
        }

        $this->write_file($crash_marker, '');

        $editor = wp_get_image_editor($path);

        if (is_wp_error($editor)) {
            wp_delete_file($crash_marker);
            return null;
        }

        $editor->set_quality($this->quality_for_settings($this->settings()));

        $result = $editor->save($next_path, $mime_type);

        if (is_wp_error($result) || empty($result['path'])) {
            wp_delete_file($crash_marker);
            return null;
        }

        if (!$this->is_next_gen_copy_worth_keeping($path, $result['path'], $extension)) {
            $this->delete_generated_file($result['path']);
            wp_delete_file($crash_marker);
            return null;
        }

        wp_delete_file($crash_marker);

        return $result['path'];
    }

    private function create_webp_copy_with_cwebp(string $path, string $next_path): ?string
    {
        $cwebp_path = $this->locate_cwebp();

        if (!$cwebp_path || !$this->can_use_exec()) {
            return null;
        }

        $quality = $this->quality_for_settings($this->settings());
        $command = escapeshellarg($cwebp_path)
            . ' -q ' . escapeshellarg((string) $quality)
            . ' -m 4 -mt -metadata none '
            . escapeshellarg($path)
            . ' -o '
            . escapeshellarg($next_path)
            . ' 2>&1';

        $output = [];
        $return_var = 1;
        @exec($command, $output, $return_var);

        if (0 !== $return_var || !$this->is_next_gen_copy_worth_keeping($path, $next_path, 'webp')) {
            if (file_exists($next_path) && !$this->path_is_registered_media_attachment($next_path)) {
                wp_delete_file($next_path);
            }

            return null;
        }

        return $next_path;
    }

    private function is_next_gen_copy_worth_keeping(string $source_path, string $next_path, string $extension): bool
    {
        if (!file_exists($next_path)) {
            return false;
        }

        $source_size = file_exists($source_path) ? (int) filesize($source_path) : 0;
        $next_size = (int) filesize($next_path);

        if ($next_size <= 0) {
            return false;
        }

        if ($source_size <= 0 || $next_size < $source_size) {
            return true;
        }

        /**
         * Filters whether Shrinkwell should keep a generated next-gen file that is
         * not smaller than the source image.
         *
         * @param bool   $keep        Whether to keep the larger/equal next-gen file.
         * @param string $source_path Source image path.
         * @param string $next_path   Generated WebP/AVIF path.
         * @param string $extension   Generated extension, webp or avif.
         * @param int    $source_size Source file size in bytes.
         * @param int    $next_size   Generated file size in bytes.
         */
        return (bool) apply_filters(
            'shrinkwell_keep_larger_next_gen_copy',
            false,
            $source_path,
            $next_path,
            $extension,
            $source_size,
            $next_size
        );
    }

    private function next_gen_copy_is_usable(string $source_path, string $next_path, string $extension): bool
    {
        return $this->is_next_gen_copy_worth_keeping($source_path, $next_path, $extension);
    }

    private function locate_cwebp(): ?string
    {
        if (self::$cwebp_checked) {
            return self::$cwebp_path;
        }

        self::$cwebp_checked = true;

        $paths = [
            '/usr/bin/cwebp',
            '/usr/local/bin/cwebp',
            '/bin/cwebp',
            '/opt/local/bin/cwebp',
        ];

        foreach ($paths as $path) {
            if (@is_executable($path)) {
                self::$cwebp_path = $path;
                return self::$cwebp_path;
            }
        }

        if ($this->can_use_exec()) {
            $located = trim((string) @exec('command -v cwebp'));

            if ('' !== $located && @is_executable($located)) {
                self::$cwebp_path = $located;
            }
        }

        return self::$cwebp_path;
    }

    private function can_use_exec(): bool
    {
        if (!function_exists('exec')) {
            return false;
        }

        $disabled = array_map('trim', explode(',', (string) ini_get('disable_functions')));

        return !in_array('exec', $disabled, true);
    }

    private function next_gen_srcset_for_image_html(string $html, int $attachment_id, $size, string $extension): string
    {
        $large_srcset = $this->next_gen_large_image_srcset_from_html($html, $extension);

        if ('' !== $large_srcset) {
            return $large_srcset;
        }

        if ($this->is_primary_product_image_html($html)) {
            return '';
        }

        return $this->next_gen_srcset_for_attachment($attachment_id, $size, $extension);
    }

    private function next_gen_srcset_from_html_fallback(string $html, string $src, string $extension): string
    {
        $large_srcset = $this->next_gen_large_image_srcset_from_html($html, $extension);

        if ('' !== $large_srcset) {
            return $large_srcset;
        }

        if ($this->is_primary_product_image_html($html)) {
            return '';
        }

        $next_url = $this->next_gen_url_for_url($src, $extension);

        return $next_url ?: '';
    }

    private function next_gen_large_image_srcset_from_html(string $html, string $extension): string
    {
        if (!$this->is_primary_product_image_html($html)) {
            return '';
        }

        $large_url = $this->attribute_from_html($html, 'data-large_image');

        if ('' === $large_url) {
            return '';
        }

        $next_url = $this->next_gen_url_for_url($large_url, $extension);

        if (!$next_url) {
            return '';
        }

        $large_width = absint($this->attribute_from_html($html, 'data-large_image_width'));

        return $large_width ? $next_url . ' ' . $large_width . 'w' : $next_url;
    }

    private function is_primary_product_image_html(string $html): bool
    {
        return false !== stripos($html, 'wp-post-image') && '' !== $this->attribute_from_html($html, 'data-large_image');
    }

    private function next_gen_srcset_for_attachment(int $attachment_id, $size, string $extension): string
    {
        $srcset = wp_get_attachment_image_srcset($attachment_id, $size);

        if (!$srcset) {
            $image = wp_get_attachment_image_src($attachment_id, $size);

            if (empty($image[0])) {
                return '';
            }

            $next_url = $this->next_gen_url_for_url($image[0], $extension);

            if (!$next_url) {
                return '';
            }

            return empty($image[1]) ? $next_url : $next_url . ' ' . absint($image[1]) . 'w';
        }

        $next_sources = [];
        $candidates = array_filter(array_map('trim', explode(',', $srcset)));

        foreach ($candidates as $candidate) {
            if (!preg_match('/^(\S+)(\s+\d+[wx])?$/', $candidate, $matches)) {
                continue;
            }

            $next_url = $this->next_gen_url_for_url($matches[1], $extension);

            if (!$next_url) {
                continue;
            }

            $next_sources[] = $next_url . ($matches[2] ?? '');
        }

        return implode(', ', $next_sources);
    }

    private function find_delivery_test_attachment(): int
    {
        $query = new WP_Query([
            'post_type' => 'attachment',
            'post_mime_type' => ['image/jpeg', 'image/png'],
            'post_status' => 'inherit',
            'fields' => 'ids',
            'posts_per_page' => 30,
            'orderby' => 'ID',
            'order' => 'DESC',
            // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Admin-only delivery test checks a small recent media sample.
            'meta_query' => [
                [
                    'key' => self::META,
                    'compare' => 'EXISTS',
                ],
            ],
        ]);

        foreach ($query->posts as $attachment_id) {
            if (
                '' !== $this->next_gen_srcset_for_attachment((int) $attachment_id, 'medium', 'webp') ||
                '' !== $this->next_gen_srcset_for_attachment((int) $attachment_id, 'medium', 'avif')
            ) {
                return (int) $attachment_id;
            }
        }

        return 0;
    }

    private function attribute_from_html(string $html, string $attribute): string
    {
        if (!preg_match('/\s' . preg_quote($attribute, '/') . '=["\']([^"\']+)["\']/', $html, $matches)) {
            return '';
        }

        return html_entity_decode($matches[1], ENT_QUOTES);
    }

    private function replace_html_attribute(string $html, string $attribute, string $value): string
    {
        $escaped_attribute = preg_quote($attribute, '/');

        return preg_replace_callback(
            '/(\s' . $escaped_attribute . '=)(["\'])([^"\']*)(\2)/i',
            static function (array $matches) use ($value): string {
                return $matches[1] . $matches[2] . esc_url_raw($value) . $matches[4];
            },
            $html,
            1
        ) ?? $html;
    }

    private function add_html_attribute(string $html, string $attribute, string $value): string
    {
        $attribute_html = ' ' . esc_attr($attribute) . '="' . esc_attr($value) . '"';

        if (preg_match('/\s*\/>$/', $html)) {
            return preg_replace('/\s*\/>$/', $attribute_html . ' />', $html, 1) ?? $html;
        }

        return preg_replace('/>$/', $attribute_html . '>', $html, 1) ?? $html;
    }

    private function next_gen_srcset_from_srcset(string $srcset, string $extension): string
    {
        $next_sources = [];
        $candidates = array_filter(array_map('trim', explode(',', $srcset)));

        foreach ($candidates as $candidate) {
            if (!preg_match('/^(\S+)(\s+\d+[wx])?$/', $candidate, $matches)) {
                continue;
            }

            $next_url = $this->next_gen_url_for_url($matches[1], $extension);

            if (!$next_url) {
                continue;
            }

            $next_sources[] = $next_url . ($matches[2] ?? '');
        }

        return implode(', ', $next_sources);
    }

    private function is_offset_inside_picture(string $content, int $offset): bool
    {
        $before = substr($content, 0, $offset);
        $last_picture_open = strripos($before, '<picture');

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

        $last_picture_close = strripos($before, '</picture>');

        return false === $last_picture_close || $last_picture_open > $last_picture_close;
    }

    private function source_html(string $mime_type, string $srcset, string $sizes = ''): string
    {
        $source = '<source type="' . esc_attr($mime_type) . '" srcset="' . esc_attr($srcset) . '"';

        if ($sizes) {
            $source .= ' sizes="' . esc_attr($sizes) . '"';
        }

        return $source . '>';
    }

    private function next_gen_url_for_url(string $url, string $extension): ?string
    {
        $clean_url = strtok($url, '?');

        if (!$clean_url || !preg_match('/\.(jpe?g|png|webp)$/i', $clean_url)) {
            return null;
        }

        $current_extension = strtolower((string) pathinfo((string) wp_parse_url($clean_url, PHP_URL_PATH), PATHINFO_EXTENSION));

        if ($current_extension === $extension) {
            return null;
        }

        $next_url = preg_replace('/\.(jpe?g|png|webp)$/i', '.' . $extension, $clean_url);

        if (!$next_url) {
            return null;
        }

        $source_path = $this->upload_path_for_url($clean_url);
        $next_path = $this->upload_path_for_url($next_url);

        return $source_path && $next_path && $this->next_gen_copy_is_usable($source_path, $next_path, $extension) ? $next_url : null;
    }

    private function next_gen_path_for_url(string $url, string $extension): ?string
    {
        $clean_url = strtok($url, '?');

        if (!$clean_url || !preg_match('/\.(jpe?g|png|webp)$/i', $clean_url)) {
            return null;
        }

        $current_extension = strtolower((string) pathinfo((string) wp_parse_url($clean_url, PHP_URL_PATH), PATHINFO_EXTENSION));

        if ($current_extension === $extension) {
            return null;
        }

        $next_url = preg_replace('/\.(jpe?g|png|webp)$/i', '.' . $extension, $clean_url);

        return $next_url ? $this->upload_path_for_url($next_url) : null;
    }

    private function upload_path_for_url(string $url): ?string
    {
        $clean_url = strtok(html_entity_decode($url, ENT_QUOTES), '?');
        $uploads = wp_upload_dir();
        $base_url = (string) ($uploads['baseurl'] ?? '');
        $base_dir = (string) ($uploads['basedir'] ?? '');

        if (!$clean_url || '' === $base_url || '' === $base_dir) {
            return null;
        }

        $base_path = (string) wp_parse_url($base_url, PHP_URL_PATH);
        $url_path = (string) wp_parse_url($clean_url, PHP_URL_PATH);
        $url_host = (string) wp_parse_url($clean_url, PHP_URL_HOST);
        $allowed_hosts = array_filter([
            (string) wp_parse_url($base_url, PHP_URL_HOST),
            (string) wp_parse_url(home_url('/'), PHP_URL_HOST),
        ]);

        if ('' !== $url_path && '/' !== $url_path[0]) {
            $url_path = '/' . $url_path;
        }

        if ('' !== $url_host && !in_array($url_host, $allowed_hosts, true)) {
            return null;
        }

        if (0 === strpos($clean_url, $base_url)) {
            $relative_path = ltrim(substr($clean_url, strlen($base_url)), '/');
        } elseif ('' !== $base_path && '' !== $url_path && 0 === strpos($url_path, $base_path)) {
            $relative_path = ltrim(substr($url_path, strlen($base_path)), '/');
        } else {
            return null;
        }

        return trailingslashit($base_dir) . $relative_path;
    }

    private function path_is_registered_media_attachment(string $path): bool
    {
        $uploads = wp_upload_dir();
        $base_dir = wp_normalize_path((string) ($uploads['basedir'] ?? ''));
        $normalized_path = wp_normalize_path($path);

        if ('' === $base_dir || 0 !== strpos($normalized_path, trailingslashit($base_dir))) {
            return false;
        }

        $relative_path = ltrim(substr($normalized_path, strlen(trailingslashit($base_dir))), '/');

        if ('' === $relative_path) {
            return false;
        }

        $query = new WP_Query([
            'post_type' => 'attachment',
            'post_status' => 'inherit',
            'fields' => 'ids',
            'posts_per_page' => 1,
            'no_found_rows' => true,
            // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Exact file-path lookup protects real media records before deleting generated files.
            'meta_query' => [
                [
                    'key' => '_wp_attached_file',
                    'value' => $relative_path,
                    'compare' => '=',
                ],
            ],
        ]);

        return !empty($query->posts);
    }

    private function delete_generated_file(string $path): void
    {
        if (file_exists($path) && !$this->path_is_registered_media_attachment($path)) {
            wp_delete_file($path);
        }
    }

    private function delete_webp_copy(string $path): void
    {
        $webp_path = preg_replace('/\.(jpe?g|png)$/i', '.webp', $path);

        if ($webp_path) {
            $this->delete_generated_file($webp_path);
        }

        if ($webp_path && file_exists($webp_path . '.shrinkwell-crashed')) {
            wp_delete_file($webp_path . '.shrinkwell-crashed');
        }

        $avif_path = preg_replace('/\.(jpe?g|png|webp)$/i', '.avif', $path);

        if ($avif_path) {
            $this->delete_generated_file($avif_path);
        }

        if ($avif_path && file_exists($avif_path . '.shrinkwell-crashed')) {
            wp_delete_file($avif_path . '.shrinkwell-crashed');
        }
    }

    private function attachment_files(string $original_file, array $metadata, ?bool $include_thumbnails = null): array
    {
        $files = [$original_file];
        $base_dir = trailingslashit(dirname($original_file));
        $include_thumbnails = null === $include_thumbnails ? !empty($this->settings()['optimize_thumbnails']) : $include_thumbnails;

        if (!$include_thumbnails || empty($metadata['sizes']) || !is_array($metadata['sizes'])) {
            return array_unique($files);
        }

        foreach ($metadata['sizes'] as $size) {
            if (empty($size['file'])) {
                continue;
            }

            $files[] = $base_dir . $size['file'];
        }

        return array_unique($files);
    }

    private function is_supported_attachment(int $attachment_id): bool
    {
        return in_array(get_post_mime_type($attachment_id), ['image/jpeg', 'image/png', 'image/webp'], true);
    }

    private function is_optimizable_attachment(int $attachment_id): bool
    {
        return in_array(get_post_mime_type($attachment_id), ['image/jpeg', 'image/png'], true);
    }

    private function attachment_title(int $attachment_id, string $fallback = ''): string
    {
        $title = get_the_title($attachment_id);
        $title = $title ? wp_specialchars_decode($title, ENT_QUOTES) : $fallback;

        return sanitize_text_field((string) $title);
    }

    private function is_supported_path(string $path): bool
    {
        return in_array($this->mime_for_path($path), ['image/jpeg', 'image/png', 'image/webp'], true);
    }

    private function is_optimizable_path(string $path): bool
    {
        return in_array($this->mime_for_path($path), ['image/jpeg', 'image/png'], true);
    }

    private function mime_for_path(string $path): string
    {
        $type = wp_check_filetype($path);

        return (string) ($type['type'] ?? '');
    }

    private function backup_file(string $path): bool
    {
        $backup_path = $path . self::BACKUP_SUFFIX;

        if (file_exists($backup_path)) {
            return true;
        }

        return copy($path, $backup_path);
    }

    private function temp_path(string $path): string
    {
        $info = pathinfo($path);
        $extension = empty($info['extension']) ? 'tmp' : $info['extension'];

        return trailingslashit($info['dirname']) . '.' . $info['filename'] . '.shrinkwell-tmp.' . $extension;
    }

    private function refresh_original_dimensions(string $file, array $metadata): array
    {
        $dimensions = wp_getimagesize($file);

        if (is_array($dimensions)) {
            $metadata['width'] = (int) $dimensions[0];
            $metadata['height'] = (int) $dimensions[1];
        }

        return $metadata;
    }

    private function restore_link(int $attachment_id): string
    {
        $url = wp_nonce_url(
            admin_url('admin-post.php?action=shrinkwell_restore_attachment&attachment_id=' . $attachment_id),
            'shrinkwell_restore_attachment_' . $attachment_id
        );

        return '<a href="' . esc_url($url) . '">' . esc_html__('Restore original', 'shrinkwell-image-optimizer') . '</a>';
    }

    private function update_global_stats(int $previous_saved, int $new_saved, bool $is_new_optimization): void
    {
        $stats = wp_parse_args((array) get_option(self::STATS_OPTION, []), [
            'optimized_images' => 0,
            'saved_bytes' => 0,
        ]);

        if ($is_new_optimization) {
            $stats['optimized_images'] = (int) $stats['optimized_images'] + 1;
        }

        $stats['saved_bytes'] = max(0, (int) $stats['saved_bytes'] - $previous_saved + $new_saved);

        update_option(self::STATS_OPTION, $stats, false);
        delete_transient(self::SAVINGS_PERCENT_TRANSIENT);
    }

    private function remove_from_global_stats(int $previous_saved): void
    {
        $stats = wp_parse_args((array) get_option(self::STATS_OPTION, []), [
            'optimized_images' => 0,
            'saved_bytes' => 0,
        ]);

        $stats['optimized_images'] = max(0, (int) $stats['optimized_images'] - 1);
        $stats['saved_bytes'] = max(0, (int) $stats['saved_bytes'] - $previous_saved);

        update_option(self::STATS_OPTION, $stats, false);
        delete_transient(self::SAVINGS_PERCENT_TRANSIENT);
    }
}

register_activation_hook(SHRINKWELL_PLUGIN_FILE, ['Shrinkwell_Image_Optimizer', 'activate']);
register_uninstall_hook(SHRINKWELL_PLUGIN_FILE, ['Shrinkwell_Image_Optimizer', 'uninstall']);
