<?php

/**
 * Image Optimization REST API Controller
 *
 * @package ProRank\SEO\Core\RestApi
 * @since   1.0.0
 */

declare(strict_types=1);

namespace ProRank\SEO\Core\RestApi;

use WP_REST_Server;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;

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


/**
 * ImageOptimizationController class
 */


class ImageOptimizationController extends \WP_REST_Controller {

    /**
     * MIME types that are currently optimizable by the image optimization pipeline.
     *
     * @var array<int, string>
     */
    private const OPTIMIZABLE_MIME_TYPES = ['image/jpeg', 'image/png'];

    /**
     * Default number of retry attempts for failed bulk optimizations.
     *
     * @var int
     */
    private const DEFAULT_BULK_RETRY_LIMIT = 3;
    
    /**
     * Module instance
     *
     * @var object|null
     */
    private $module = null;
    
    /**
     * Constructor
     */
    public function __construct() {
        $this->namespace = 'prorank-seo/v1';
        $this->rest_base = 'image-optimization';
    }
    
    /**
     * Register routes
     *
     * @return void
     */
    public function register_routes(): void {
        // Get optimization statistics
        register_rest_route(
            $this->namespace,
            '/' . $this->rest_base . '/stats',
            [
                [
                    'methods'             => WP_REST_Server::READABLE,
                    'callback'            => [$this, 'get_stats'],
                    'permission_callback' => [$this, 'get_items_permissions_check'],
                ],
            ]
        );
        
        // Start bulk conversion
        register_rest_route(
            $this->namespace,
            '/' . $this->rest_base . '/bulk-convert',
            [
                [
                    'methods'             => WP_REST_Server::CREATABLE,
                    'callback'            => [$this, 'bulk_convert'],
                    'permission_callback' => [$this, 'create_item_permissions_check'],
                ],
            ]
        );
        
        // Get conversion progress
        register_rest_route(
            $this->namespace,
            '/' . $this->rest_base . '/progress',
            [
                [
                    'methods'             => WP_REST_Server::READABLE,
                    'callback'            => [$this, 'get_progress'],
                    'permission_callback' => [$this, 'get_items_permissions_check'],
                ],
            ]
        );
        
        // Stop optimization
        register_rest_route(
            $this->namespace,
            '/' . $this->rest_base . '/stop',
            [
                [
                    'methods'             => WP_REST_Server::CREATABLE,
                    'callback'            => [$this, 'stop_optimization'],
                    'permission_callback' => [$this, 'create_item_permissions_check'],
                ],
            ]
        );

        // Delete all backups
        register_rest_route(
            $this->namespace,
            '/' . $this->rest_base . '/delete-backups',
            [
                [
                    'methods'             => WP_REST_Server::DELETABLE,
                    'callback'            => [$this, 'delete_backups'],
                    'permission_callback' => [$this, 'create_item_permissions_check'],
                ],
            ]
        );

        // Restore all backups
        register_rest_route(
            $this->namespace,
            '/' . $this->rest_base . '/restore-backups',
            [
                [
                    'methods'             => WP_REST_Server::CREATABLE,
                    'callback'            => [$this, 'restore_backups'],
                    'permission_callback' => [$this, 'create_item_permissions_check'],
                ],
            ]
        );
        
        // Get image sizes
        register_rest_route(
            $this->namespace,
            '/' . $this->rest_base . '/sizes',
            [
                [
                    'methods'             => WP_REST_Server::READABLE,
                    'callback'            => [$this, 'get_sizes'],
                    'permission_callback' => [$this, 'get_items_permissions_check'],
                ],
            ]
        );
        
        // Check server support for image optimization
        register_rest_route(
            $this->namespace,
            '/' . $this->rest_base . '/check-support',
            [
                [
                    'methods'             => WP_REST_Server::READABLE,
                    'callback'            => [$this, 'check_support'],
                    'permission_callback' => [$this, 'get_items_permissions_check'],
                ],
            ]
        );
        
        // Get quota status
        register_rest_route(
            $this->namespace,
            '/' . $this->rest_base . '/quota',
            [
                [
                    'methods'             => WP_REST_Server::READABLE,
                    'callback'            => [$this, 'get_quota'],
                    'permission_callback' => [$this, 'get_items_permissions_check'],
                ],
            ]
        );
        
        // Check API quota (force refresh)
        register_rest_route(
            $this->namespace,
            '/' . $this->rest_base . '/check-quota',
            [
                [
                    'methods'             => WP_REST_Server::READABLE,
                    'callback'            => [$this, 'check_quota'],
                    'permission_callback' => [$this, 'get_items_permissions_check'],
                ],
                [
                    'methods'             => WP_REST_Server::CREATABLE,
                    'callback'            => [$this, 'check_quota'],
                    'permission_callback' => [$this, 'create_item_permissions_check'],
                ],
            ]
        );

        // Single image optimization
        register_rest_route(
            $this->namespace,
            '/images/(?P<id>\d+)/optimize',
            [
                [
                    'methods'             => WP_REST_Server::CREATABLE,
                    'callback'            => [$this, 'optimize_single_image'],
                    'permission_callback' => [$this, 'create_item_permissions_check'],
                    'args'                => [
                        'id' => [
                            'description' => __('Attachment ID', 'prorank-seo'),
                            'type'        => 'integer',
                            'required'    => true,
                        ],
                    ],
                ],
            ]
        );

        // Get single image optimization stats
        register_rest_route(
            $this->namespace,
            '/images/(?P<id>\d+)/optimization-stats',
            [
                [
                    'methods'             => WP_REST_Server::READABLE,
                    'callback'            => [$this, 'get_single_image_stats'],
                    'permission_callback' => [$this, 'get_items_permissions_check'],
                    'args'                => [
                        'id' => [
                            'description' => __('Attachment ID', 'prorank-seo'),
                            'type'        => 'integer',
                            'required'    => true,
                        ],
                    ],
                ],
            ]
        );

        register_rest_route(
            $this->namespace,
            '/' . $this->rest_base . '/images/(?P<id>\d+)/optimization-stats',
            [
                [
                    'methods'             => WP_REST_Server::READABLE,
                    'callback'            => [$this, 'get_single_image_stats'],
                    'permission_callback' => [$this, 'get_items_permissions_check'],
                    'args'                => [
                        'id' => [
                            'description' => __('Attachment ID', 'prorank-seo'),
                            'type'        => 'integer',
                            'required'    => true,
                        ],
                    ],
                ],
            ]
        );

        // Batch stats for multiple images (used by media library grid)
        register_rest_route(
            $this->namespace,
            '/images/batch-stats',
            [
                [
                    'methods'             => WP_REST_Server::READABLE,
                    'callback'            => [$this, 'get_batch_image_stats'],
                    'permission_callback' => [$this, 'get_items_permissions_check'],
                    'args'                => [
                        'ids' => [
                            'description' => __('Comma-separated attachment IDs', 'prorank-seo'),
                            'type'        => 'string',
                            'required'    => true,
                        ],
                    ],
                ],
            ]
        );

        // Restore single image
        register_rest_route(
            $this->namespace,
            '/images/(?P<id>\d+)/restore',
            [
                [
                    'methods'             => WP_REST_Server::CREATABLE,
                    'callback'            => [$this, 'restore_single_image'],
                    'permission_callback' => [$this, 'create_item_permissions_check'],
                    'args'                => [
                        'id' => [
                            'description' => __('Attachment ID', 'prorank-seo'),
                            'type'        => 'integer',
                            'required'    => true,
                        ],
                    ],
                ],
            ]
        );

        // Get unoptimized images list
        register_rest_route(
            $this->namespace,
            '/images/unoptimized',
            [
                [
                    'methods'             => WP_REST_Server::READABLE,
                    'callback'            => [$this, 'get_unoptimized_images'],
                    'permission_callback' => [$this, 'get_items_permissions_check'],
                ],
            ]
        );

        register_rest_route(
            $this->namespace,
            '/' . $this->rest_base . '/images/unoptimized',
            [
                [
                    'methods'             => WP_REST_Server::READABLE,
                    'callback'            => [$this, 'get_unoptimized_images'],
                    'permission_callback' => [$this, 'get_items_permissions_check'],
                ],
            ]
        );

        // Bulk optimize endpoint (single image at a time via POST)
        register_rest_route(
            $this->namespace,
            '/images/optimize',
            [
                [
                    'methods'             => WP_REST_Server::CREATABLE,
                    'callback'            => [$this, 'optimize_image_by_id'],
                    'permission_callback' => [$this, 'create_item_permissions_check'],
                    'args'                => [
                        'attachment_id' => [
                            'description' => __('Attachment ID', 'prorank-seo'),
                            'type'        => 'integer',
                            'required'    => true,
                        ],
                    ],
                ],
            ]
        );

        register_rest_route(
            $this->namespace,
            '/' . $this->rest_base . '/images/optimize',
            [
                [
                    'methods'             => WP_REST_Server::CREATABLE,
                    'callback'            => [$this, 'optimize_image_by_id'],
                    'permission_callback' => [$this, 'create_item_permissions_check'],
                    'args'                => [
                        'attachment_id' => [
                            'description' => __('Attachment ID', 'prorank-seo'),
                            'type'        => 'integer',
                            'required'    => true,
                        ],
                    ],
                ],
            ]
        );
    }
    
    /**
     * Get module instance
     *
     * @return object|null
     */
    private function get_module() {
        if ($this->module !== null) {
            return $this->module;
        }

        if (!class_exists('\\ProRank\\SEO\\Plugin')) {
            return null;
        }

        $plugin = \ProRank\SEO\Plugin::get_instance();
        if (!$plugin || !$plugin->is_initialized()) {
            return null;
        }

        $module_manager = $plugin->modules();
        if (!$module_manager) {
            return null;
        }

        $module = $module_manager->get_module('image_optimization');
        if (!is_object($module)) {
            return null;
        }

        foreach (['is_enabled', 'get_stats', 'get_quota_status'] as $method) {
            if (!method_exists($module, $method)) {
                return null;
            }
        }

        $this->module = $module;

        return $this->module;
    }
    
    /**
     * Check permissions for reading
     *
     * @param WP_REST_Request $request Request object
     * @return bool|WP_Error
     */
    public function get_items_permissions_check($request) {
        if (!current_user_can('manage_options') && !current_user_can('upload_files')) {
            return new WP_Error(
                'rest_forbidden',
                __('Sorry, you are not allowed to view image optimization data.', 'prorank-seo'),
                ['status' => 403]
            );
        }
        
        return true;
    }
    
    /**
     * Check permissions for creating
     *
     * @param WP_REST_Request $request Request object
     * @return bool|WP_Error
     */
    public function create_item_permissions_check($request) {
        if (!current_user_can('manage_options') && !current_user_can('upload_files')) {
            return new WP_Error(
                'rest_forbidden',
                __('Sorry, you are not allowed to manage image optimization.', 'prorank-seo'),
                ['status' => 403]
            );
        }
        
        // Image optimization create actions are allowed for admins.
        // Cloud operations will still enforce license/credit checks at execution time.
        return true;
    }

    /**
     * Get the currently saved optimization method.
     *
     * @return string
     */
    private function get_optimization_method(): string {
        return 'local';
    }

    /**
     * Return a clear cloud-unavailable REST error when cloud mode is selected.
     *
     * @param object $module Module instance.
     * @return WP_Error|null
     */
    private function get_cloud_unavailable_error($module): ?WP_Error {
        return null;
    }
    
    /**
     * Get optimization statistics
     *
     * @param WP_REST_Request $request Request object
     * @return WP_REST_Response|WP_Error
     */
    public function get_stats(WP_REST_Request $request) {
        $module = $this->get_module();

        // Always return basic stats even if module is disabled
        if (!$module || !$module->is_enabled()) {
            global $wpdb;
            $mime_placeholders = implode(', ', array_fill(0, count(self::OPTIMIZABLE_MIME_TYPES), '%s'));

            // Get basic counts directly from database (optimizable sources only).
            $total_count = (int) $wpdb->get_var(
                $wpdb->prepare(
                    "SELECT COUNT(*) FROM {$wpdb->posts}
                     WHERE post_type = 'attachment'
                     AND post_status = 'inherit'
                     AND post_mime_type IN ({$mime_placeholders})",
                    ...self::OPTIMIZABLE_MIME_TYPES
                )
            );

            $optimized_count = (int) $wpdb->get_var(
                $wpdb->prepare(
                    "SELECT COUNT(DISTINCT p.ID)
                     FROM {$wpdb->posts} p
                     WHERE p.post_type = 'attachment'
                     AND p.post_status = 'inherit'
                     AND p.post_mime_type IN ({$mime_placeholders})
                     AND EXISTS (
                         SELECT 1 FROM {$wpdb->postmeta} pm
                         WHERE pm.post_id = p.ID
                         AND (
                             (pm.meta_key = '_prorank_webp_generated' AND pm.meta_value = '1')
                             OR (pm.meta_key = '_prorank_avif_generated' AND pm.meta_value = '1')
                             OR (pm.meta_key = '_prorank_jxl_generated' AND pm.meta_value = '1')
                             OR (pm.meta_key = '_prorank_bytes_saved' AND CAST(pm.meta_value AS UNSIGNED) > 0)
                         )
                     )",
                    ...self::OPTIMIZABLE_MIME_TYPES
                )
            );

            $bytes_saved = (int) $wpdb->get_var(
                "SELECT SUM(meta_value) FROM {$wpdb->postmeta}
                 WHERE meta_key = '_prorank_bytes_saved'"
            ) ?: 0;

            return rest_ensure_response([
                'success' => true,
                'module_enabled' => false,
                'stats' => [
                    'total' => $total_count,
                    'total_images' => $total_count,
                    'optimized' => $optimized_count,
                    'optimized_images' => $optimized_count,
                    'bytes_saved' => $bytes_saved,
                    'pending' => max(0, $total_count - $optimized_count),
                    'source_images' => $total_count,
                    'optimization_percentage' => $total_count > 0
                        ? round(($optimized_count / $total_count) * 100, 2)
                        : 0,
                ],
            ]);
        }

        $stats = $module->get_stats();
        
        // Add human-readable sizes
        if (isset($stats['total_savings'])) {
            $stats['total_savings_formatted'] = size_format($stats['total_savings']);
        }
        
        return rest_ensure_response([
            'success' => true,
            'module_enabled' => true,
            'stats' => $stats,
        ]);
    }
    
    /**
     * Start bulk conversion - IMPROVED VERSION
     *
     * This version schedules ALL unoptimized images at once using batch processing,
     * stores progress in database (not transients), and supports auto-continuation.
     *
     * @param WP_REST_Request $request Request object
     * @return WP_REST_Response|WP_Error
     */
    public function bulk_convert(WP_REST_Request $request) {
        $module = $this->get_module();
        if (!$module) {
            return new WP_Error(
                'module_not_found',
                __('Image optimization module not found.', 'prorank-seo'),
                ['status' => 404]
            );
        }

        if (!$module->is_enabled()) {
            return new WP_Error(
                'module_disabled',
                __('Image optimization module is disabled.', 'prorank-seo'),
                ['status' => 400]
            );
        }

        $settings = get_option('prorank_image_optimization_settings', []);
        $optimization_method = is_array($settings) ? (string) ($settings['optimization_method'] ?? 'local') : 'local';
        $webp_enabled = is_array($settings) ? !empty($settings['webp_enabled']) : false;

        if ($optimization_method === 'local' && !$webp_enabled) {
            return new WP_Error(
                'no_effective_local_output',
                __('Enable WebP before starting local bulk optimization. In local mode, WebP is the effective bulk output in this build.', 'prorank-seo'),
                ['status' => 400]
            );
        }

        $cloud_unavailable = $this->get_cloud_unavailable_error($module);
        if ($cloud_unavailable instanceof WP_Error) {
            return $cloud_unavailable;
        }

        // Check if there's an active job
        $active_job = $this->get_active_bulk_job();
        if ($active_job && in_array($active_job['status'] ?? '', ['running', 'processing'], true)) {
            // Check if job is stale (no update in 10 minutes)
            $last_update = $active_job['updated_at'] ?? $active_job['started_at'] ?? 0;
            if (time() - $last_update < 600) {
                $this->sync_job_to_transient($active_job);
                $this->trigger_action_scheduler();

                return rest_ensure_response($this->prepare_running_job_response($active_job));
            }
            // Job is stale, mark it as failed
            $this->update_bulk_job($active_job['job_id'], ['status' => 'failed', 'error' => 'Job timed out']);
            delete_option('prorank_bulk_job_active');
            update_option('prorank_bulk_job_last', $active_job['job_id'], false);
        }

        // Count ALL unoptimized images using optimized query
        $total_count = $this->count_unoptimized_images();

        if ($total_count === 0) {
            $failed_count = $this->count_failed_images();
            $no_gain_count = $this->count_no_gain_images();
            $unresolved_count = $this->count_unresolved_images();
            $retry_limit = $this->get_bulk_retry_limit();

            if ($unresolved_count > 0 && ($failed_count > 0 || $no_gain_count > 0)) {
                if ($failed_count > 0 && $no_gain_count > 0) {
                    $message = sprintf(
                        /* translators: %1$d unresolved images, %2$d failed images, %3$d no-gain images, %4$d retry limit */
                        __('No queueable images remain right now. %1$d image(s) still lack modern variants: %2$d attachment(s) are still hard failures after up to %4$d attempts, and %3$d image(s) were left unchanged because no meaningfully smaller output was produced.', 'prorank-seo'),
                        $unresolved_count,
                        $failed_count,
                        $no_gain_count,
                        $retry_limit
                    );
                } elseif ($failed_count > 0) {
                    $message = sprintf(
                        /* translators: %1$d unresolved images, %2$d failed images, %3$d retry limit */
                        __('No retryable images remain right now. %1$d image(s) still lack modern variants, and %2$d attachment(s) are flagged as failed after up to %3$d attempts.', 'prorank-seo'),
                        $unresolved_count,
                        $failed_count,
                        $retry_limit
                    );
                } else {
                    $message = sprintf(
                        /* translators: %1$d unresolved images, %2$d no-gain images */
                        __('No queueable images remain right now. %1$d image(s) still lack modern variants, and %2$d image(s) were already evaluated and left unchanged because no meaningfully smaller output was produced.', 'prorank-seo'),
                        $unresolved_count,
                        $no_gain_count
                    );
                }

                return rest_ensure_response([
                    'success' => true,
                    'complete' => true,
                    'no_retryable_images' => true,
                    'failed_images' => $failed_count,
                    'no_gain_images' => $no_gain_count,
                    'unresolved_images' => $unresolved_count,
                    'message' => $message,
                ]);
            }

            return rest_ensure_response([
                'success' => true,
                'complete' => true,
                'message' => __('All images have been optimized', 'prorank-seo'),
            ]);
        }

        // Create new job with unique ID
        $job_id = wp_generate_uuid4();
        $batch_size = apply_filters('prorank_bulk_optimization_batch_size', 50);

        $job_data = [
            'job_id' => $job_id,
            'status' => 'running',
            'total' => $total_count,
            'processed' => 0,
            'failed' => 0,
            'skipped' => 0,
            'bytes_saved' => 0,
            'started_at' => time(),
            'updated_at' => time(),
            'batch_size' => $batch_size,
            'current_batch' => 0,
            'current_image' => '',
            'log' => [],
            'failed_images' => [],
            'error' => null,
        ];

        // Store job in database (persistent, not transient!)
        update_option('prorank_bulk_job_' . $job_id, $job_data, false);
        update_option('prorank_bulk_job_active', $job_id, false);

        // Clear last completed job to avoid stale status display
        delete_option('prorank_bulk_job_last');

        // Also update transient for backward compatibility with UI polling
        $this->sync_job_to_transient($job_data);

        // Schedule the first batch of images
        $scheduled = $this->schedule_image_batch($job_id, 0, $batch_size);

        // Trigger Action Scheduler immediately
        $this->trigger_action_scheduler();

        return rest_ensure_response([
            'success' => true,
            'complete' => false,
            'job_id' => $job_id,
            'total' => $total_count,
            'scheduled' => $scheduled,
            'batch_size' => $batch_size,
            'message' => sprintf(
                /* translators: %1$d: scheduled images, %2$d: total images */
                __('Started bulk optimization: %1$d of %2$d images scheduled in first batch', 'prorank-seo'),
                $scheduled,
                $total_count
            ),
        ]);
    }

    /**
     * Count unoptimized images using optimized direct SQL query
     *
     * @return int
     */
    private function count_unoptimized_images(): int {
        global $wpdb;
        $retry_limit = $this->get_bulk_retry_limit();

        $mime_placeholders = implode(', ', array_fill(0, count(self::OPTIMIZABLE_MIME_TYPES), '%s'));
        $count = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(p.ID)
                 FROM {$wpdb->posts} p
                 WHERE p.post_type = 'attachment'
                 AND p.post_mime_type IN ({$mime_placeholders})
                 AND p.post_status = 'inherit'
                 AND NOT EXISTS (
                     SELECT 1 FROM {$wpdb->postmeta} pm
                     WHERE pm.post_id = p.ID
                     AND (
                         (pm.meta_key = '_prorank_webp_generated' AND pm.meta_value = '1')
                         OR (pm.meta_key = '_prorank_avif_generated' AND pm.meta_value = '1')
                         OR (pm.meta_key = '_prorank_jxl_generated' AND pm.meta_value = '1')
                         OR (pm.meta_key = '_prorank_bytes_saved' AND CAST(pm.meta_value AS UNSIGNED) > 0)
                     )
                 )
                 AND NOT EXISTS (
                     SELECT 1 FROM {$wpdb->postmeta} pm_no_gain
                     WHERE pm_no_gain.post_id = p.ID
                     AND (
                         (pm_no_gain.meta_key = '_prorank_optimization_no_gain' AND pm_no_gain.meta_value = '1')
                         OR (pm_no_gain.meta_key = '_prorank_optimization_failure_reason' AND pm_no_gain.meta_value = 'no_effective_output')
                     )
                 )
                 AND NOT EXISTS (
                     SELECT 1 FROM {$wpdb->postmeta} pm_attempts
                     WHERE pm_attempts.post_id = p.ID
                     AND pm_attempts.meta_key = '_prorank_optimization_attempts'
                     AND CAST(pm_attempts.meta_value AS UNSIGNED) >= %d
                 )",
                ...array_merge(self::OPTIMIZABLE_MIME_TYPES, [$retry_limit])
            )
        );

        return (int) $count;
    }

    /**
     * Count unresolved images that still lack a generated modern output.
     *
     * @return int
     */
    private function count_unresolved_images(): int {
        global $wpdb;

        $mime_placeholders = implode(', ', array_fill(0, count(self::OPTIMIZABLE_MIME_TYPES), '%s'));
        $count = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(p.ID)
                 FROM {$wpdb->posts} p
                 WHERE p.post_type = 'attachment'
                 AND p.post_mime_type IN ({$mime_placeholders})
                 AND p.post_status = 'inherit'
                 AND NOT EXISTS (
                     SELECT 1 FROM {$wpdb->postmeta} pm
                     WHERE pm.post_id = p.ID
                     AND (
                         (pm.meta_key = '_prorank_webp_generated' AND pm.meta_value = '1')
                         OR (pm.meta_key = '_prorank_avif_generated' AND pm.meta_value = '1')
                         OR (pm.meta_key = '_prorank_jxl_generated' AND pm.meta_value = '1')
                         OR (pm.meta_key = '_prorank_bytes_saved' AND CAST(pm.meta_value AS UNSIGNED) > 0)
                     )
                 )",
                ...self::OPTIMIZABLE_MIME_TYPES
            )
        );

        return (int) $count;
    }

    /**
     * Count unresolved images currently marked as failed.
     *
     * @return int
     */
    private function count_failed_images(): int {
        global $wpdb;

        $mime_placeholders = implode(', ', array_fill(0, count(self::OPTIMIZABLE_MIME_TYPES), '%s'));
        $count = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(DISTINCT p.ID)
                 FROM {$wpdb->posts} p
                 WHERE p.post_type = 'attachment'
                 AND p.post_mime_type IN ({$mime_placeholders})
                 AND p.post_status = 'inherit'
                 AND NOT EXISTS (
                     SELECT 1 FROM {$wpdb->postmeta} pm
                     WHERE pm.post_id = p.ID
                     AND (
                         (pm.meta_key = '_prorank_webp_generated' AND pm.meta_value = '1')
                         OR (pm.meta_key = '_prorank_avif_generated' AND pm.meta_value = '1')
                         OR (pm.meta_key = '_prorank_jxl_generated' AND pm.meta_value = '1')
                         OR (pm.meta_key = '_prorank_bytes_saved' AND CAST(pm.meta_value AS UNSIGNED) > 0)
                     )
                 )
                 AND NOT EXISTS (
                     SELECT 1 FROM {$wpdb->postmeta} pm_no_gain
                     WHERE pm_no_gain.post_id = p.ID
                     AND (
                         (pm_no_gain.meta_key = '_prorank_optimization_no_gain' AND pm_no_gain.meta_value = '1')
                         OR (pm_no_gain.meta_key = '_prorank_optimization_failure_reason' AND pm_no_gain.meta_value = 'no_effective_output')
                     )
                 )
                 AND EXISTS (
                     SELECT 1 FROM {$wpdb->postmeta} pm_failed
                     WHERE pm_failed.post_id = p.ID
                     AND pm_failed.meta_key = '_prorank_optimization_failed'
                     AND pm_failed.meta_value = '1'
                 )",
                ...self::OPTIMIZABLE_MIME_TYPES
            )
        );

        return (int) $count;
    }

    /**
     * Count unresolved images that were evaluated but intentionally left unchanged because no smaller output was produced.
     *
     * @return int
     */
    private function count_no_gain_images(): int {
        global $wpdb;

        $mime_placeholders = implode(', ', array_fill(0, count(self::OPTIMIZABLE_MIME_TYPES), '%s'));
        $count = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(DISTINCT p.ID)
                 FROM {$wpdb->posts} p
                 WHERE p.post_type = 'attachment'
                 AND p.post_mime_type IN ({$mime_placeholders})
                 AND p.post_status = 'inherit'
                 AND NOT EXISTS (
                     SELECT 1 FROM {$wpdb->postmeta} pm
                     WHERE pm.post_id = p.ID
                     AND (
                         (pm.meta_key = '_prorank_webp_generated' AND pm.meta_value = '1')
                         OR (pm.meta_key = '_prorank_avif_generated' AND pm.meta_value = '1')
                         OR (pm.meta_key = '_prorank_jxl_generated' AND pm.meta_value = '1')
                         OR (pm.meta_key = '_prorank_bytes_saved' AND CAST(pm.meta_value AS UNSIGNED) > 0)
                     )
                 )
                 AND EXISTS (
                     SELECT 1 FROM {$wpdb->postmeta} pm_no_gain
                     WHERE pm_no_gain.post_id = p.ID
                     AND (
                         (pm_no_gain.meta_key = '_prorank_optimization_no_gain' AND pm_no_gain.meta_value = '1')
                         OR (pm_no_gain.meta_key = '_prorank_optimization_failure_reason' AND pm_no_gain.meta_value = 'no_effective_output')
                     )
                 )",
                ...self::OPTIMIZABLE_MIME_TYPES
            )
        );

        return (int) $count;
    }

    /**
     * Get unoptimized image IDs using optimized query with pagination
     *
     * @param int $limit  Number of images to get
     * @param int $offset Offset for pagination
     * @return array Array of attachment IDs
     */
    private function get_unoptimized_image_ids(int $limit, int $offset = 0): array {
        global $wpdb;
        $retry_limit = $this->get_bulk_retry_limit();

        // Prioritize smaller images first for faster perceived progress
        $mime_placeholders = implode(', ', array_fill(0, count(self::OPTIMIZABLE_MIME_TYPES), '%s'));
        $results = $wpdb->get_col(
            $wpdb->prepare(
                "SELECT p.ID
                 FROM {$wpdb->posts} p
                 LEFT JOIN {$wpdb->postmeta} pm_meta ON p.ID = pm_meta.post_id
                     AND pm_meta.meta_key = '_wp_attachment_metadata'
                 WHERE p.post_type = 'attachment'
                 AND p.post_mime_type IN ({$mime_placeholders})
                 AND p.post_status = 'inherit'
                 AND NOT EXISTS (
                     SELECT 1 FROM {$wpdb->postmeta} pm
                     WHERE pm.post_id = p.ID
                     AND (
                         (pm.meta_key = '_prorank_webp_generated' AND pm.meta_value = '1')
                         OR (pm.meta_key = '_prorank_avif_generated' AND pm.meta_value = '1')
                         OR (pm.meta_key = '_prorank_jxl_generated' AND pm.meta_value = '1')
                         OR (pm.meta_key = '_prorank_bytes_saved' AND CAST(pm.meta_value AS UNSIGNED) > 0)
                     )
                 )
                 AND NOT EXISTS (
                     SELECT 1 FROM {$wpdb->postmeta} pm_no_gain
                     WHERE pm_no_gain.post_id = p.ID
                     AND (
                         (pm_no_gain.meta_key = '_prorank_optimization_no_gain' AND pm_no_gain.meta_value = '1')
                         OR (pm_no_gain.meta_key = '_prorank_optimization_failure_reason' AND pm_no_gain.meta_value = 'no_effective_output')
                     )
                 )
                 AND NOT EXISTS (
                     SELECT 1 FROM {$wpdb->postmeta} pm_attempts
                     WHERE pm_attempts.post_id = p.ID
                     AND pm_attempts.meta_key = '_prorank_optimization_attempts'
                     AND CAST(pm_attempts.meta_value AS UNSIGNED) >= %d
                 )
                 ORDER BY p.ID ASC
                 LIMIT %d OFFSET %d",
                array_merge(self::OPTIMIZABLE_MIME_TYPES, [$retry_limit, $limit, $offset])
            )
        );

        return array_map('intval', $results);
    }

    /**
     * Resolve the per-attachment retry limit for failed bulk jobs.
     *
     * @return int
     */
    private function get_bulk_retry_limit(): int {
        return max(1, (int) apply_filters('prorank_image_optimization_retry_limit', self::DEFAULT_BULK_RETRY_LIMIT));
    }

    /**
     * Schedule a batch of images for optimization
     *
     * @param string $job_id    Job ID
     * @param int    $offset    Offset for getting images
     * @param int    $batch_size Batch size
     * @return int Number of images scheduled
     */
    private function schedule_image_batch(string $job_id, int $offset, int $batch_size): int {
        $image_ids = $this->get_unoptimized_image_ids($batch_size, $offset);

        if (empty($image_ids)) {
            return 0;
        }

        // Check if Action Scheduler is available
        if (function_exists('as_enqueue_async_action')) {
            foreach ($image_ids as $attachment_id) {
                // Schedule using the original prorank_optimize_image action
                as_enqueue_async_action(
                    'prorank_optimize_image',
                    ['attachment_id' => $attachment_id],
                    'prorank-image-optimization'
                );
            }

            // Schedule continuation action to process next batch
            as_schedule_single_action(
                time() + 5,
                'prorank_bulk_optimization_continue',
                ['job_id' => $job_id, 'next_offset' => $offset + $batch_size],
                'prorank-image-optimization'
            );
        } else {
            // Fallback: Use WP-Cron for continuation
            wp_schedule_single_event(
                time() + 10,
                'prorank_bulk_optimization_batch',
                [$job_id, $offset]
            );
        }

        return count($image_ids);
    }

    /**
     * Continue bulk optimization by scheduling the next batch.
     *
     * @param string $job_id Job ID.
     * @param int    $next_offset Next offset.
     * @return void
     */
    public function handle_bulk_optimization_continue(string $job_id = '', int $next_offset = 0): void {
        if ($job_id === '') {
            return;
        }

        $job_data = get_option('prorank_bulk_job_' . $job_id, []);
        if (!is_array($job_data) || empty($job_data)) {
            return;
        }

        $status = $job_data['status'] ?? 'running';
        if (in_array($status, ['completed', 'stopped', 'failed'], true)) {
            return;
        }

        $batch_size = isset($job_data['batch_size'])
            ? (int) $job_data['batch_size']
            : (int) apply_filters('prorank_bulk_optimization_batch_size', 50);

        $scheduled = $this->schedule_image_batch($job_id, $next_offset, $batch_size);

        if ($scheduled <= 0) {
            $this->update_bulk_job($job_id, [
                'status' => 'completed',
                'completed_at' => time(),
            ]);
            update_option('prorank_bulk_job_last', $job_id, false);
            delete_option('prorank_bulk_job_active');
        } else {
            $this->update_bulk_job($job_id, [
                'status' => 'running',
                'current_batch' => (int) ($job_data['current_batch'] ?? 0) + 1,
            ]);
        }

        $this->trigger_action_scheduler();
    }

    /**
     * Get active bulk job
     *
     * @return array|null Job data or null
     */
    private function get_active_bulk_job(): ?array {
        // First check for active running job
        $active_job_id = get_option('prorank_bulk_job_active', '');
        if (!empty($active_job_id)) {
            $job_data = get_option('prorank_bulk_job_' . $active_job_id, null);
            if (!is_array($job_data) || empty($job_data)) {
                delete_option('prorank_bulk_job_active');
            } else {
                $status = (string) ($job_data['status'] ?? '');
                if (in_array($status, ['completed', 'stopped', 'failed'], true)) {
                    delete_option('prorank_bulk_job_active');
                    update_option('prorank_bulk_job_last', $active_job_id, false);
                } elseif (in_array($status, ['running', 'processing'], true)) {
                    return $job_data;
                } else {
                    delete_option('prorank_bulk_job_active');
                }
            }
        }

        // Then check for recently completed job (to show final status)
        $last_job_id = get_option('prorank_bulk_job_last', '');
        if (!empty($last_job_id)) {
            $job_data = get_option('prorank_bulk_job_' . $last_job_id, null);
            if (is_array($job_data) && in_array($job_data['status'] ?? '', ['completed', 'stopped', 'failed'], true)) {
                // Only return completed jobs from last 5 minutes
                $completed_at = $job_data['completed_at'] ?? $job_data['updated_at'] ?? 0;
                if (time() - $completed_at < 300) {
                    return $job_data;
                }
            }
        }

        return null;
    }

    /**
     * Normalize the response payload for an already-running bulk job.
     *
     * @param array $job_data Job data.
     * @return array<string, mixed>
     */
    private function prepare_running_job_response(array $job_data): array {
        return [
            'success' => true,
            'complete' => false,
            'already_running' => true,
            'job_id' => $job_data['job_id'] ?? '',
            'total' => (int) ($job_data['total'] ?? 0),
            'scheduled' => max(0, (int) (($job_data['total'] ?? 0) - ($job_data['processed'] ?? 0))),
            'batch_size' => (int) ($job_data['batch_size'] ?? apply_filters('prorank_bulk_optimization_batch_size', 50)),
            'processed' => (int) ($job_data['processed'] ?? 0),
            'failed' => (int) ($job_data['failed'] ?? 0),
            'skipped' => (int) ($job_data['skipped'] ?? 0),
            'bytes_saved' => (int) ($job_data['bytes_saved'] ?? 0),
            'status' => $job_data['status'] ?? 'running',
            'current_image' => $job_data['current_image'] ?? '',
            'started_at' => (int) ($job_data['started_at'] ?? time()),
            'updated_at' => (int) ($job_data['updated_at'] ?? time()),
            'message' => __('Bulk optimization is already running. Resuming the existing job.', 'prorank-seo'),
        ];
    }

    /**
     * Update bulk job data
     *
     * @param string $job_id Job ID
     * @param array  $update Data to update
     * @return bool
     */
    private function update_bulk_job(string $job_id, array $update): bool {
        $job_data = get_option('prorank_bulk_job_' . $job_id, []);
        if (empty($job_data)) {
            return false;
        }

        $job_data = array_merge($job_data, $update);
        $job_data['updated_at'] = time();

        // Update in database
        update_option('prorank_bulk_job_' . $job_id, $job_data, false);

        // Sync to transient for backward compatibility
        $this->sync_job_to_transient($job_data);

        return true;
    }

    /**
     * Public wrapper to update bulk job data and sync progress.
     *
     * @param string $job_id Job ID.
     * @param array  $update Data to update.
     * @return void
     */
    public function record_bulk_job_update(string $job_id, array $update): void {
        if ($job_id === '') {
            return;
        }

        $this->update_bulk_job($job_id, $update);
    }

    /**
     * Sync job data to transient for backward compatibility with UI
     *
     * @param array $job_data Job data
     * @return void
     */
    private function sync_job_to_transient(array $job_data): void {
        $progress = [
            'current' => $job_data['processed'] ?? 0,
            'total' => $job_data['total'] ?? 0,
            'current_image' => $job_data['current_image'] ?? '',
            'bytes_saved' => $job_data['bytes_saved'] ?? 0,
            'completed' => ($job_data['status'] ?? '') === 'completed',
            'log' => $job_data['log'] ?? [],
            'percent' => ($job_data['total'] ?? 0) > 0
                ? round((($job_data['processed'] ?? 0) / $job_data['total']) * 100, 2)
                : 0,
            'updated_at' => $job_data['updated_at'] ?? time(),
            'active_index' => ($job_data['processed'] ?? 0) + 1,
            'summary' => [
                'processed' => $job_data['processed'] ?? 0,
                'saved_bytes' => $job_data['bytes_saved'] ?? 0,
                'failed' => $job_data['failed'] ?? 0,
                'skipped' => $job_data['skipped'] ?? 0,
            ],
            'job_id' => $job_data['job_id'] ?? '',
            'status' => $job_data['status'] ?? 'idle',
            'error' => $job_data['error'] ?? null,
        ];

        set_transient('prorank_image_optimization_progress', $progress, 2 * HOUR_IN_SECONDS);

        $is_running = in_array($job_data['status'] ?? '', ['running', 'processing'], true);
        if ($is_running) {
            set_transient('prorank_image_conversion_running', true, 2 * HOUR_IN_SECONDS);
        } else {
            delete_transient('prorank_image_conversion_running');
        }
    }

    /**
     * Trigger Action Scheduler to run immediately
     *
     * @return void
     */
    private function trigger_action_scheduler(): void {
        // Spawn cron
        if (function_exists('spawn_cron')) {
            spawn_cron();
        }

        // Try to run Action Scheduler directly
        if (class_exists('\ActionScheduler_QueueRunner')) {
            try {
                \ActionScheduler_QueueRunner::instance()->run();
            } catch (\Exception $e) {
                // Ignore - cron will handle it
            }
        }
    }

    /**
     * Nudge the background queue when the bulk job appears idle but not stale enough to fail.
     *
     * @param array $active_job Active job data.
     * @return void
     */
    private function maybe_nudge_active_bulk_job(array $active_job): void {
        $status = (string) ($active_job['status'] ?? '');
        if (!in_array($status, ['running', 'processing'], true)) {
            return;
        }

        $last_update = (int) ($active_job['updated_at'] ?? $active_job['started_at'] ?? 0);
        if ($last_update <= 0 || (time() - $last_update) < 20) {
            return;
        }

        $nudge_lock_key = 'prorank_image_optimization_queue_nudge';
        if (get_transient($nudge_lock_key)) {
            return;
        }

        set_transient($nudge_lock_key, 1, 15);
        $this->trigger_action_scheduler();
    }
    
    /**
     * Get conversion progress - IMPROVED VERSION
     *
     * Reads from persistent database storage and falls back to transients
     *
     * @param WP_REST_Request $request Request object
     * @return WP_REST_Response|WP_Error
     */
    public function get_progress(WP_REST_Request $request) {
        $module = $this->get_module();
        if (!$module) {
            return new WP_Error(
                'module_not_found',
                __('Image optimization module not found.', 'prorank-seo'),
                ['status' => 404]
            );
        }

        // Get stats for overall progress
        $stats = $module->get_stats();

        // First try to get active job from database (persistent)
        $active_job = $this->get_active_bulk_job();
        if ($active_job) {
            $this->maybe_nudge_active_bulk_job($active_job);
        }

        // Fall back to transient for backward compatibility
        $progress_data = get_transient('prorank_image_optimization_progress');
        $running = get_transient('prorank_image_conversion_running');

        // Determine status from job or transient
        $status = 'idle';
        if ($active_job) {
            $status = $active_job['status'] ?? 'idle';
            // Check for stale job (no update in 90 seconds)
            $last_update = $active_job['updated_at'] ?? 0;
            if ($status === 'running' && time() - $last_update > 90) {
                $status = 'stale';
            }
        } elseif ($running) {
            $status = 'running';
        } elseif ($progress_data && isset($progress_data['completed']) && $progress_data['completed']) {
            $status = 'completed';
        } elseif ($progress_data && isset($progress_data['error']) && $progress_data['error']) {
            $status = 'error';
        }

        // Build progress response - prefer job data over transient
        $current = $active_job['processed'] ?? $progress_data['current'] ?? 0;
        $total = $active_job['total'] ?? $progress_data['total'] ?? $stats['total'] ?? 0;
        $bytes_saved = $active_job['bytes_saved'] ?? $progress_data['bytes_saved'] ?? $stats['bytes_saved'] ?? 0;

        $percent = $total > 0 ? round(($current / $total) * 100, 2) : 0;

        // Calculate estimated time remaining
        $eta = null;
        if ($active_job && $status === 'running') {
            $elapsed = time() - ($active_job['started_at'] ?? time());
            $processed = $active_job['processed'] ?? 0;
            if ($processed > 0 && $elapsed > 0) {
                $avg_time_per_image = $elapsed / $processed;
                $remaining_images = $total - $processed;
                $eta = (int) ($avg_time_per_image * $remaining_images);
            }
        }

        $progress = [
            'current' => $current,
            'total' => $total,
            'current_image' => $active_job['current_image'] ?? $progress_data['current_image'] ?? '',
            'bytes_saved' => $bytes_saved,
            'status' => $status,
            'updated_at' => $active_job['updated_at'] ?? $progress_data['updated_at'] ?? null,
            'log' => $active_job['log'] ?? (isset($progress_data['log']) && is_array($progress_data['log'])
                ? array_values($progress_data['log'])
                : []),
            'percent' => $percent,
            'active_index' => $current + 1,
            'summary' => [
                'processed' => $current,
                'saved_bytes' => $bytes_saved,
                'failed' => $active_job['failed'] ?? $progress_data['summary']['failed'] ?? 0,
                'skipped' => $active_job['skipped'] ?? $progress_data['summary']['skipped'] ?? 0,
            ],
            'job_id' => $active_job['job_id'] ?? $progress_data['job_id'] ?? null,
            'eta_seconds' => $eta,
            'started_at' => $active_job['started_at'] ?? null,
            'stale_after_seconds' => 90,
        ];

        if ($status === 'error') {
            $progress['error_message'] = $active_job['error'] ?? $progress_data['error_message'] ?? __('An error occurred', 'prorank-seo');
        }

        // Include failed images list if any
        if (!empty($active_job['failed_images'])) {
            $progress['failed_images'] = array_slice($active_job['failed_images'], -20); // Last 20 failures
        }

        return rest_ensure_response([
            'success' => true,
            'progress' => $progress,
            // Keep legacy format for backwards compatibility
            'running' => in_array($status, ['running', 'processing'], true),
            'optimized' => $stats['optimized'] ?? 0,
            'total' => $stats['total'] ?? 0,
            'remaining' => $stats['pending'] ?? max(0, ($stats['total'] ?? 0) - ($stats['optimized'] ?? 0)),
        ]);
    }

    /**
     * Stop the current optimization process - IMPROVED VERSION
     *
     * Stops the active job and cancels all pending scheduled actions
     *
     * @param WP_REST_Request $request Request object
     * @return WP_REST_Response|WP_Error
     */
    public function stop_optimization(WP_REST_Request $request) {
        // Get active job
        $active_job = $this->get_active_bulk_job();

        if ($active_job) {
            $job_id = $active_job['job_id'];

            // Update job status to stopped
            $this->update_bulk_job($job_id, [
                'status' => 'stopped',
                'stopped_at' => time(),
            ]);

            // Cancel all pending actions for this job using Action Scheduler
            if (function_exists('as_unschedule_all_actions')) {
                // Cancel individual image optimization actions
                as_unschedule_all_actions('prorank_optimize_image', null, 'prorank-image-optimization');
                // Cancel continuation actions
                as_unschedule_all_actions('prorank_bulk_optimization_continue', null, 'prorank-image-optimization');
            }

            // Clear active job marker
            delete_option('prorank_bulk_job_active');
        }

        // Clear transients for backward compatibility
        delete_transient('prorank_image_conversion_running');

        $progress_data = get_transient('prorank_image_optimization_progress');
        if ($progress_data) {
            $progress_data['status'] = 'stopped';
            $progress_data['completed'] = false;
            $progress_data['stopped'] = true;
            set_transient('prorank_image_optimization_progress', $progress_data, HOUR_IN_SECONDS);
        }

        // Clear any WP-Cron scheduled tasks
        $timestamp = wp_next_scheduled('prorank_image_optimization_batch');
        if ($timestamp) {
            wp_unschedule_event($timestamp, 'prorank_image_optimization_batch');
        }
        $timestamp = wp_next_scheduled('prorank_bulk_optimize_images');
        if ($timestamp) {
            wp_unschedule_event($timestamp, 'prorank_bulk_optimize_images');
        }

        return rest_ensure_response([
            'success' => true,
            'message' => __('Optimization stopped successfully. Pending images have been cancelled.', 'prorank-seo'),
            'stopped_job' => $active_job['job_id'] ?? null,
            'images_processed' => $active_job['processed'] ?? 0,
        ]);
    }

    /**
     * Restore all backups
     *
     * @param WP_REST_Request $request Request object
     * @return WP_REST_Response|WP_Error
     */
    public function restore_backups(WP_REST_Request $request) {
        $module = $this->get_module();
        if (!$module) {
            return new WP_Error(
                'module_not_found',
                __('Image optimization module not found.', 'prorank-seo'),
                ['status' => 404]
            );
        }

        if (!method_exists($module, 'restore_all_backups')) {
            return new WP_Error(
                'not_supported',
                __('Backup restoration is not available.', 'prorank-seo'),
                ['status' => 400]
            );
        }

        $result = $module->restore_all_backups();

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

        $restored = (int) ($result['restored'] ?? 0);
        $failed = (int) ($result['failed'] ?? 0);
        $missing_files = (int) ($result['missing_files'] ?? 0);

        $message = sprintf(
            /* translators: %d: number of images restored */
            __('Restored %d images from backups.', 'prorank-seo'),
            $restored
        );

        if ($restored === 0 && $missing_files > 0) {
            $message = sprintf(
                /* translators: %d: number of missing backup files */
                __('No images were restored because %d backup files are missing on disk.', 'prorank-seo'),
                $missing_files
            );
        } elseif ($restored > 0 && $failed > 0) {
            $message = sprintf(
                /* translators: 1: restored count, 2: failed count */
                __('Restored %1$d images from backups. %2$d could not be restored.', 'prorank-seo'),
                $restored,
                $failed
            );
        }

        return rest_ensure_response([
            'success' => true,
            'restored' => $restored,
            'failed' => $failed,
            'missing_files' => $missing_files,
            'message' => $message,
        ]);
    }

    /**
     * Delete all backup files
     *
     * @param WP_REST_Request $request Request object
     * @return WP_REST_Response|WP_Error
     */
    public function delete_backups(WP_REST_Request $request) {
        $module = $this->get_module();
        if (!$module) {
            return new WP_Error(
                'module_not_found',
                __('Image optimization module not found.', 'prorank-seo'),
                ['status' => 404]
            );
        }
        
        if (!$module->is_enabled()) {
            return new WP_Error(
                'module_disabled',
                __('Image optimization module is disabled.', 'prorank-seo'),
                ['status' => 400]
            );
        }
        
        $success = $module->delete_all_backups();
        
        if (!$success) {
            return new WP_Error(
                'delete_failed',
                __('Failed to delete backup files.', 'prorank-seo'),
                ['status' => 500]
            );
        }
        
        return rest_ensure_response([
            'success' => true,
            'message' => __('All backup files have been deleted.', 'prorank-seo'),
        ]);
    }
    
    /**
     * Get registered image sizes
     *
     * @param WP_REST_Request $request Request object
     * @return WP_REST_Response|WP_Error
     */
    public function get_sizes(WP_REST_Request $request) {
        $module = $this->get_module();
        if (!$module) {
            // Return default sizes even if module not found
            return rest_ensure_response([
                'success' => true,
                'sizes' => [
                    ['name' => 'thumbnail', 'label' => 'Thumbnail', 'width' => 150, 'height' => 150],
                    ['name' => 'medium', 'label' => 'Medium', 'width' => 300, 'height' => 300],
                    ['name' => 'medium_large', 'label' => 'Medium Large', 'width' => 768, 'height' => 0],
                    ['name' => 'large', 'label' => 'Large', 'width' => 1024, 'height' => 1024],
                ],
            ]);
        }
        
        $sizes = $module->get_image_sizes();
        
        return rest_ensure_response([
            'success' => true,
            'sizes' => $sizes,
        ]);
    }
    
    /**
     * Check server support for image optimization
     *
     * @param WP_REST_Request $request Request object
     * @return WP_REST_Response|WP_Error
     */
    public function check_support(WP_REST_Request $request) {
        try {
            return rest_ensure_response([
                'success' => true,
                'support' => $this->build_support_payload(),
            ]);
        } catch (\Throwable $e) {
            prorank_log('[ProRank SEO] Image support check failed: ' . $e->getMessage());
            return rest_ensure_response([
                'success' => false,
                'support' => $this->build_support_payload(),
                'message' => __('Unable to detect server image capabilities.', 'prorank-seo'),
            ]);
        }
    }
    
    /**
     * Get quota status
     *
     * @param WP_REST_Request $request Request object
     * @return WP_REST_Response|WP_Error
     */
    public function get_quota($request) {
        return rest_ensure_response([
            'tier' => 'free',
            'status' => 'no_license',
            'used' => 0,
            'remaining' => 0,
            'percentage' => 0,
            'message' => __('Cloud image optimization is not available in the free plugin.', 'prorank-seo'),
        ]);
    }
    
    /**
     * Check API quota (force refresh from API)
     *
     * @param WP_REST_Request $request Request object
     * @return WP_REST_Response|WP_Error
     */
    public function check_quota($request) {
        return rest_ensure_response([
            'tier' => 'free',
            'status' => 'no_license',
            'used' => 0,
            'remaining' => 0,
            'percentage' => 0,
            'message' => __('Cloud image optimization is not available in the free plugin.', 'prorank-seo'),
        ]);
    }

    /**
     * Build the server support payload from the active optimization engine.
     *
     * @return array<string, mixed>
     */
    private function build_support_payload(): array {
        $capabilities = [];
        if (class_exists('\\ProRank\\SEO\\Core\\ImageOptimization\\OptimizationEngine')) {
            $engine = new \ProRank\SEO\Core\ImageOptimization\OptimizationEngine();
            $capabilities = $engine->get_capabilities();
        }

        $tools = is_array($capabilities['tools'] ?? null) ? $capabilities['tools'] : [];
        $cloud = is_array($capabilities['cloud'] ?? null) ? $capabilities['cloud'] : [];
        $features = is_array($capabilities['features'] ?? null) ? $capabilities['features'] : [];
        $imagick_formats = isset($tools['imagick']['formats']) && is_array($tools['imagick']['formats'])
            ? array_map('strtolower', $tools['imagick']['formats'])
            : [];
        $local_webp = isset($tools['cwebp']) || in_array('webp', $imagick_formats, true) || function_exists('imagewebp');
        $local_avif = isset($tools['avifenc']) || in_array('avif', $imagick_formats, true) || function_exists('imageavif');
        $local_jxl = isset($tools['cjxl']) || in_array('jxl', $imagick_formats, true);
        $local_jpegli = isset($tools['cjpegli']);
        $local_png_pipeline = isset($tools['pngquant']) || isset($tools['oxipng']) || isset($tools['optipng']) || isset($tools['ect']);
        $cloud_formats = array_values(array_filter(array_map('strval', $cloud['formats'] ?? [])));

        $support = [
            'gd' => isset($tools['gd']) || extension_loaded('gd'),
            'imagick' => isset($tools['imagick']) || (extension_loaded('imagick') && class_exists('Imagick')),
            'webp' => $local_webp,
            'avif' => $local_avif,
            'jxl' => $local_jxl,
            'jpegli' => $local_jpegli,
            'png_pipeline' => $local_png_pipeline,
            'htaccess' => $this->server_supports_htaccess(),
            'details' => [],
            'server' => [
                'software' => $this->get_server_software(),
                'php_version' => PHP_VERSION,
                'memory_limit' => ini_get('memory_limit'),
                'max_execution_time' => ini_get('max_execution_time'),
                'upload_max_filesize' => ini_get('upload_max_filesize'),
            ],
            'cloud' => [
                'available' => (bool) ($cloud['available'] ?? false),
                'formats' => $cloud_formats,
                'version' => isset($cloud['version']) ? (string) $cloud['version'] : '',
                'capabilities' => [
                    'jxl' => in_array('jxl', $cloud_formats, true),
                    'jpegli' => (bool) ($features['jpegli_encoding'] ?? false),
                    'png_pipeline' => $local_png_pipeline || !empty($cloud_formats),
                ],
            ],
        ];

        if (function_exists('gd_info')) {
            $gd_info = gd_info();
            $support['details']['gd_version'] = $gd_info['GD Version'] ?? 'Unknown';
        }

        if (extension_loaded('imagick') && class_exists('Imagick')) {
            try {
                $version = \Imagick::getVersion();
                $support['details']['imagick_version'] = $version['versionString'] ?? 'Unknown';
            } catch (\Throwable $e) {
                $support['details']['imagick_error'] = $e->getMessage();
            }
        }

        if (isset($tools['cjxl']) || isset($tools['cjpegli'])) {
            $support['details']['libjxl_version'] = __('CLI encoder detected', 'prorank-seo');
        }

        return $support;
    }

    /**
     * Get the best available server software description.
     *
     * @return string
     */
    private function get_server_software(): string {
        $server_software = prorank_get_server_var('SERVER_SOFTWARE');
        return $server_software !== '' ? $server_software : 'Unknown';
    }

    /**
     * Determine whether rewrite-based delivery is supported by the web server.
     *
     * @return bool
     */
    private function server_supports_htaccess(): bool {
        $server_software = $this->get_server_software();
        if (
            stripos($server_software, 'apache') === false
            && stripos($server_software, 'litespeed') === false
        ) {
            return false;
        }

        if (function_exists('apache_get_modules')) {
            $modules = apache_get_modules();
            return in_array('mod_rewrite', $modules, true);
        }

        return true;
    }

    /**
     * Optimize a single image
     *
     * @param WP_REST_Request $request Request object
     * @return WP_REST_Response|WP_Error
     */
    public function optimize_single_image(WP_REST_Request $request) {
        @ini_set('memory_limit', '512M');
        @set_time_limit(120);

        $attachment_id = (int) $request->get_param('id');

        if (!wp_attachment_is_image($attachment_id)) {
            return new WP_Error(
                'not_an_image',
                __('The specified attachment is not an image.', 'prorank-seo'),
                ['status' => 400]
            );
        }

        $module = $this->get_module();
        if (!$module) {
            return new WP_Error(
                'module_not_found',
                __('Image optimization module not found.', 'prorank-seo'),
                ['status' => 404]
            );
        }

        $cloud_unavailable = $this->get_cloud_unavailable_error($module);
        if ($cloud_unavailable instanceof WP_Error) {
            return $cloud_unavailable;
        }

        try {
            if (method_exists($module, 'process_single_image_optimization')) {
                $module->process_single_image_optimization($attachment_id);
                $result = true;
            } else {
                $metadata = wp_get_attachment_metadata($attachment_id);
                if (!$metadata) {
                    $file_path = get_attached_file($attachment_id);
                    if ($file_path && file_exists($file_path)) {
                        if (!function_exists('wp_generate_attachment_metadata')) {
                            require_once ABSPATH . 'wp-admin/includes/image.php';
                        }

                        $generated_metadata = wp_generate_attachment_metadata($attachment_id, $file_path);
                        if (is_array($generated_metadata) && !empty($generated_metadata)) {
                            wp_update_attachment_metadata($attachment_id, $generated_metadata);
                            $metadata = $generated_metadata;
                        }
                    }
                }

                if (!$metadata) {
                    update_post_meta($attachment_id, '_prorank_optimization_failed', '1');
                    update_post_meta($attachment_id, '_prorank_optimization_failure_reason', 'missing_attachment_metadata');
                    return new WP_Error(
                        'no_metadata',
                        __('Unable to get attachment metadata.', 'prorank-seo'),
                        ['status' => 400]
                    );
                }

                $result = $module->generate_optimized_images($metadata, $attachment_id);
            }
        } catch (\Throwable $e) {
            update_post_meta($attachment_id, '_prorank_optimization_failed', '1');
            update_post_meta($attachment_id, '_prorank_optimization_failure_reason', $e->getMessage());
            return new WP_Error(
                'optimization_failed',
                $e->getMessage(),
                ['status' => 500]
            );
        }

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

        // Get updated stats
        $stats = $this->get_attachment_optimization_stats($attachment_id);
        $skip_reason = (string) get_post_meta($attachment_id, '_prorank_optimization_skip_reason', true);
        if ($skip_reason === '' && (string) get_post_meta($attachment_id, '_prorank_optimization_failure_reason', true) === 'no_effective_output') {
            $skip_reason = 'no_effective_output';
        }

        return rest_ensure_response([
            'success' => true,
            'message' => $skip_reason === 'no_effective_output'
                ? __('No smaller output was produced for this image. The original was kept.', 'prorank-seo')
                : __('Image optimized successfully.', 'prorank-seo'),
            'skipped' => $skip_reason !== '',
            'skip_reason' => $skip_reason,
            'stats'   => $stats,
            'saved_percent' => $stats['percent_saved'] ?? 0,
            'saved_size'    => $stats['bytes_saved_formatted'] ?? '0 KB',
            'webp_generated' => $stats['webp_generated'] ?? false,
            'avif_generated' => $stats['avif_generated'] ?? false,
        ]);
    }

    /**
     * Optimize image by attachment ID (POST body parameter)
     *
     * @param WP_REST_Request $request Request object
     * @return WP_REST_Response|WP_Error
     */
    public function optimize_image_by_id(WP_REST_Request $request) {
        $attachment_id = (int) $request->get_param('attachment_id');

        if (!$attachment_id) {
            return new WP_Error(
                'missing_id',
                __('Attachment ID is required.', 'prorank-seo'),
                ['status' => 400]
            );
        }

        // Reuse the optimize_single_image logic
        $request->set_param('id', $attachment_id);
        return $this->optimize_single_image($request);
    }

    /**
     * Get single image optimization stats
     *
     * @param WP_REST_Request $request Request object
     * @return WP_REST_Response|WP_Error
     */
    public function get_single_image_stats(WP_REST_Request $request) {
        $attachment_id = (int) $request->get_param('id');

        if (!wp_attachment_is_image($attachment_id)) {
            return new WP_Error(
                'not_an_image',
                __('The specified attachment is not an image.', 'prorank-seo'),
                ['status' => 400]
            );
        }

        try {
            $stats = $this->get_attachment_optimization_stats($attachment_id);

            return rest_ensure_response([
                'success'   => true,
                'optimized' => $stats['optimized'],
                'stats'     => $stats,
            ]);
        } catch (\Throwable $e) {
            return new WP_Error(
                'stats_error',
                $e->getMessage(),
                ['status' => 500]
            );
        }
    }

    /**
     * Restore single image to original
     *
     * @param WP_REST_Request $request Request object
     * @return WP_REST_Response|WP_Error
     */
    public function restore_single_image(WP_REST_Request $request) {
        $attachment_id = (int) $request->get_param('id');

        if (!wp_attachment_is_image($attachment_id)) {
            return new WP_Error(
                'not_an_image',
                __('The specified attachment is not an image.', 'prorank-seo'),
                ['status' => 400]
            );
        }

        $module = $this->get_module();
        if (!$module) {
            return new WP_Error(
                'module_not_found',
                __('Image optimization module not found.', 'prorank-seo'),
                ['status' => 404]
            );
        }

        $result = $module->restore_original($attachment_id);

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

        return rest_ensure_response([
            'success' => true,
            'message' => __('Original image restored successfully.', 'prorank-seo'),
        ]);
    }

    /**
     * Get list of unoptimized images
     *
     * @param WP_REST_Request $request Request object
     * @return WP_REST_Response|WP_Error
     */
    public function get_unoptimized_images(WP_REST_Request $request) {
        $attachment_ids = $this->get_unoptimized_image_ids(100, 0);

        return rest_ensure_response([
            'success'        => true,
            'attachment_ids' => $attachment_ids,
            'count'          => count($attachment_ids),
        ]);
    }

    /**
     * Get optimization stats for a single attachment
     *
     * @param int $attachment_id Attachment ID
     * @return array Stats array
     */
    /**
     * Get stats for multiple images in a single request.
     *
     * @param WP_REST_Request $request Request object
     * @return WP_REST_Response|WP_Error
     */
    public function get_batch_image_stats(WP_REST_Request $request) {
        $ids_param = $request->get_param('ids');
        $ids = array_filter(array_map('intval', explode(',', (string) $ids_param)));

        if (empty($ids) || count($ids) > 100) {
            return new WP_Error(
                'invalid_ids',
                __('Provide 1-100 comma-separated attachment IDs.', 'prorank-seo'),
                ['status' => 400]
            );
        }

        $results = [];
        foreach ($ids as $id) {
            try {
                if (!wp_attachment_is_image($id)) {
                    continue;
                }
                $stats = $this->get_attachment_optimization_stats($id);
                $results[$id] = [
                    'optimized' => $stats['optimized'],
                    'stats'     => $stats,
                ];
            } catch (\Throwable $e) {
                $results[$id] = [
                    'optimized' => false,
                    'error'     => $e->getMessage(),
                ];
            }
        }

        return rest_ensure_response([
            'success' => true,
            'results' => $results,
        ]);
    }

    private function get_attachment_optimization_stats(int $attachment_id): array {
        $original_size = (int) get_post_meta($attachment_id, '_prorank_original_size', true);
        $bytes_saved = (int) get_post_meta($attachment_id, '_prorank_bytes_saved', true);
        $webp_generated = get_post_meta($attachment_id, '_prorank_webp_generated', true);
        $avif_generated = get_post_meta($attachment_id, '_prorank_avif_generated', true);
        $jxl_generated = get_post_meta($attachment_id, '_prorank_jxl_generated', true);
        $backup_path = get_post_meta($attachment_id, '_prorank_backup_path', true);

        $file_path = get_attached_file($attachment_id);
        $has_current_file = is_string($file_path) && $file_path !== '' && file_exists($file_path);
        $current_size = $has_current_file ? (int) filesize($file_path) : 0;
        $has_backup = is_string($backup_path) && $backup_path !== '' && file_exists($backup_path);

        $percent = ($original_size > 0) ? round(($bytes_saved / $original_size) * 100, 1) : 0;

        return [
            'optimized'              => ($webp_generated === '1' || $avif_generated === '1' || $jxl_generated === '1' || $bytes_saved > 0),
            'original_size'          => $original_size,
            'current_size'           => $current_size,
            'bytes_saved'            => $bytes_saved,
            'percent_saved'          => $percent,
            'saved_percent'          => $percent, // alias for compatibility
            'webp_generated'         => (bool) $webp_generated,
            'avif_generated'         => (bool) $avif_generated,
            'jxl_generated'          => (bool) $jxl_generated,
            'has_backup'             => $has_backup,
            'original_size_formatted' => size_format($original_size),
            'current_size_formatted' => size_format($current_size),
            'bytes_saved_formatted'  => size_format($bytes_saved),
        ];
    }

    /**
     * Format numbers using the active Image Optimization module if available.
     *
     * @param object|null $module
     * @param mixed       $number
     * @return string
     */
    private function format_number($module, $number): string {
        if (is_object($module)) {
            $class = get_class($module);
            if (is_callable([$class, 'format_number'])) {
                return $class::format_number($number);
            }
        }

        if (is_numeric($number)) {
            return function_exists('number_format_i18n')
                ? number_format_i18n((float) $number)
                : (string) $number;
        }

        return (string) $number;
    }

}
