<?php
/**
 * Cron Handler for StaticQ
 * 
 * Processes pending attachments from the main table.
 * Handles: upload to bucket, size generation, WebP, local deletion.
 * 
 * @package suspended-cloud/cloudflare-media-offload
 * @since 2.0.0
 */

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

class SQMedia_Cron_Handler {
    
    /**
     * Cron hook name
     */
    const CRON_HOOK = 'sqmedia_process_queue';
    
    /**
     * Database instance
     */
    private $db;
    
    /**
     * Constructor
     */
    public function __construct() {
        $this->db = sqmedia()->get_database();
        $this->init_hooks();
    }
    
    /**
     * Initialize hooks
     */
    private function init_hooks(): void {
        // Register cron action
        add_action(self::CRON_HOOK, [$this, 'process_queue']);
        
        // Custom cron interval
        add_filter('cron_schedules', [$this, 'add_cron_intervals']);
    }
    
    /**
     * Add custom cron intervals
     */
    public function add_cron_intervals(array $schedules): array {
        $schedules['sqmedia_five_minutes'] = [
            'interval' => 300,
            'display' => __('Every 5 Minutes', 'staticq-media'),
        ];
        
        return $schedules;
    }
    
    /**
     * Schedule cron events
     */
    public static function schedule_events(): void {
        if (!wp_next_scheduled(self::CRON_HOOK)) {
            wp_schedule_event(time(), 'sqmedia_five_minutes', self::CRON_HOOK);
        }
    }
    
    /**
     * Unschedule cron events
     */
    public static function unschedule_events(): void {
        $timestamp = wp_next_scheduled(self::CRON_HOOK);
        if ($timestamp) {
            wp_unschedule_event($timestamp, self::CRON_HOOK);
        }
        wp_clear_scheduled_hook(self::CRON_HOOK);
    }
    
    /**
     * Unschedule registration-specific cron events
     * 
     * Note: Registration currently runs via AJAX batches, not cron.
     * This method exists for backwards compatibility.
     */
    public static function unschedule_registration(): void {
        // Registration doesn't use a separate cron hook - it runs via AJAX batches
        // This method is a no-op but exists for backwards compatibility
    }
    
    /**
     * Process the queue (main cron job)
     */
    public function process_queue(): void {
        // Track when cron last ran
        update_option('sqmedia_last_cron_run', time());
        
        // Check if configured
        if (!SQMedia_Settings::is_fully_configured()) {
            return;
        }
        
        // Get batch size and max retries
        $batch_size = SQMedia_Settings::get_batch_size();
        $max_retries = SQMedia_Settings::get_max_retries();
        
        // Get pending attachments (status IN ('pending', 'failed') AND retry_count < max_retries)
        $pending = $this->db->get_pending_attachments($batch_size, $max_retries);
        
        if (empty($pending)) {
            return;
        }
        
        if (function_exists('sq')) {
            sqmedia()->log('Processing queue', 'debug', ['count' => count($pending)]);
        }
        
        foreach ($pending as $record) {
            $this->process_attachment($record);
        }
    }
    
    /**
     * Process a single attachment
     */
    private function process_attachment(object $record): void {
        $attachment_id = $record->attachment_id;
        
        // Mark as processing
        $this->db->mark_processing($attachment_id);
        
        try {
            $result = $this->do_process($record);
            
            if ($result['success']) {
                // Update record with results (NEW simplified schema)
                $update_data = [
                    'status' => 'completed',
                    'storage_location' => $result['storage_location'],
                    'has_webp' => !empty($result['webp_generated']) ? 1 : 0,
                    'last_error' => null,
                ];
                
                // Save original_image and its location if exists
                if (!empty($result['original_image'])) {
                    $update_data['original_image'] = $result['original_image'];
                    $update_data['original_location'] = $result['original_location'] ?? 'alongside';
                }
                
                $this->db->update_record($attachment_id, $update_data);

                // Fix stale image URLs in post_content after scaling
                if (!empty($result['original_image'])) {
                    $metadata = wp_get_attachment_metadata($attachment_id);
                    if ($metadata) {
                        $this->fix_post_content_after_scaling($attachment_id, $metadata);
                    }
                }

                if (function_exists('sq')) {
                    sqmedia()->log('Attachment processed successfully', 'debug', [
                        'attachment_id' => $attachment_id,
                        'storage_location' => $result['storage_location'],
                    ]);
                }
            } else {
                throw new \Exception($result['error'] ?? 'Unknown error');
            }
            
        } catch (\Exception $e) {
            // Mark as failed
            $this->db->mark_failed($attachment_id, $e->getMessage());
            
            if (function_exists('sq')) {
                sqmedia()->log('Attachment processing failed', 'error', [
                    'attachment_id' => $attachment_id,
                    'error' => $e->getMessage(),
                    'retry_count' => $record->retry_count + 1,
                ]);
            }
        }
    }
    
    /**
     * Do the actual processing
     */
    private function do_process(object $record): array {
        $attachment_id = $record->attachment_id;
        
        // Determine if this is an image or other media type
        $mime_type = get_post_mime_type($attachment_id);
        $is_image = $mime_type && strpos($mime_type, 'image/') === 0;
        
        // Get attachment info
        $metadata = wp_get_attachment_metadata($attachment_id);
        
        // For non-images, metadata might be empty or minimal - that's OK
        if (!$is_image && empty($metadata)) {
            $metadata = [];
        }
        
        // Get file path directly from postmeta to avoid WP Offload Media hijacking get_attached_file()
        $relative_file = get_post_meta($attachment_id, '_wp_attached_file', true);
        
        // If the file path looks like a cloud URL (from WP Offload Media), skip processing
        if (preg_match('#^[a-z0-9]+://#i', $relative_file)) {
            return [
                'success' => false,
                'error' => 'File is managed by another offload plugin',
            ];
        }
        
        $upload_dir = wp_upload_dir();
        $file = $upload_dir['basedir'] . '/' . $relative_file;
        
        if (!$file || !file_exists($file)) {
            return [
                'success' => false,
                'error' => 'Attachment file not found locally',
            ];
        }
        
        // Route to appropriate processing method
        if ($is_image) {
            return $this->process_image($record, $file, $metadata, $attachment_id);
        } else {
            return $this->process_non_image($record, $file, $metadata, $attachment_id);
        }
    }
    
    /**
     * Process non-image media (video, audio, documents, etc.)
     * 
     * Simplified path: just upload master to bucket, optionally delete local.
     * No size generation, no WebP.
     */
    private function process_non_image(object $record, string $file, array $metadata, int $attachment_id): array {
        $cloud_enabled = SQMedia_Settings::is_cloud_enabled();
        $delete_local = SQMedia_Settings::should_delete_local_files();
        $bucket_prefix = SQMedia_Settings::get_current_prefix();
        $upload_dir = wp_upload_dir();
        $master_dir = $record->master_dir;

        $master_local = true;
        $master_bucket = false;
        $preview_count = 0;

        // Get file size for logging
        $file_size = filesize($file);
        $file_size_mb = round($file_size / (1024 * 1024), 2);

        if ($cloud_enabled) {
            // Upload master to bucket (chunked upload will be used automatically for large files)
            $s3 = sqmedia()->get_storage_handler();
            $bucket_path = $bucket_prefix . '/' . $record->master_path;

            // Pass attachment_id for chunked upload tracking
            $upload_options = [
                'attachment_id' => $attachment_id,
            ];

            $upload_result = $s3->upload_file($file, $bucket_path, $upload_options);

            if (!$upload_result) {
                // Check if this is a chunked upload in progress (not an error)
                if ($s3->has_pending_chunked_upload($file, $bucket_path)) {
                    return [
                        'success' => false,
                        'error' => 'Large file upload in progress, will continue on next cron run',
                        'chunked_upload_pending' => true,
                    ];
                }

                return [
                    'success' => false,
                    'error' => 'Failed to upload to bucket: ' . $s3->get_last_error(),
                ];
            }

            $master_bucket = true;

            // Upload preview thumbnails if they exist (e.g., PDF previews generated by WordPress)
            if (!empty($metadata['sizes']) && is_array($metadata['sizes'])) {
                foreach ($metadata['sizes'] as $size_name => $size_data) {
                    if (empty($size_data['file'])) {
                        continue;
                    }

                    $preview_local = $upload_dir['basedir'] . '/' . $master_dir . '/' . $size_data['file'];

                    if (file_exists($preview_local)) {
                        $preview_bucket = $bucket_prefix . '/' . $master_dir . '/' . $size_data['file'];

                        if ($s3->upload_file($preview_local, $preview_bucket)) {
                            $preview_count++;
                        }
                    }
                }
            }
        }
        
        // Delete local files if configured
        if ($delete_local && $master_bucket) {
            // Delete master
            if (file_exists($file)) {
                wp_delete_file($file);
                $master_local = false;
            }

            // Delete preview thumbnails
            if (!empty($metadata['sizes']) && is_array($metadata['sizes'])) {
                foreach ($metadata['sizes'] as $size_name => $size_data) {
                    if (empty($size_data['file'])) {
                        continue;
                    }

                    $preview_local = $upload_dir['basedir'] . '/' . $master_dir . '/' . $size_data['file'];

                    if (file_exists($preview_local)) {
                        wp_delete_file($preview_local);
                    }
                }
            }
        }
        
        // Determine final storage location
        $storage_location = $this->determine_storage_location($master_local, $master_bucket);
        
        return [
            'success' => true,
            'storage_location' => $storage_location,
            'master_local' => $master_local,
            'master_bucket' => $master_bucket,
            'webp_generated' => false,
            'original_image' => null,
            'is_image' => false,
            'preview_count' => $preview_count,
        ];
    }
    
    /**
     * Process image attachment (full pipeline with sizes and WebP)
     */
    private function process_image(object $record, string $file, array $metadata, int $attachment_id): array {
        // Get settings
        $cloud_enabled = SQMedia_Settings::is_cloud_enabled();
        $delete_local = SQMedia_Settings::should_delete_local_files();
        $enable_webp = SQMedia_Settings::is_webp_enabled();
        // CF Image Resizing requires a bucket as the destination for generated variants,
        // so it is incoherent in local-only mode. Force WP-native encoding when no cloud.
        $use_cloudflare = $cloud_enabled && SQMedia_Settings::is_cloudflare_enabled();

        $upload_dir = wp_upload_dir();
        $bucket_prefix = SQMedia_Settings::get_current_prefix();
        $master_dir = $record->master_dir;

        sqmedia()->log('cron: process_image entry', 'debug', [
            'attachment_id' => $attachment_id,
            'master_path' => $record->master_path,
            'cloud_enabled' => $cloud_enabled,
            'delete_local' => $delete_local,
            'enable_webp' => $enable_webp,
            'use_cloudflare' => $use_cloudflare,
            'metadata_sizes' => isset($metadata['sizes']) ? array_keys($metadata['sizes']) : null,
            'has_original_image' => !empty($metadata['original_image']),
        ]);

        // Initialize tracking
        $sizes = [];
        $webp_sizes = [];
        $master_local = true;
        $master_bucket = false;

        // Step 0: Check if big image scaling is needed
        $scaled_result = $this->maybe_scale_big_image($file, $record, $metadata, $bucket_prefix, $use_cloudflare, $cloud_enabled);
        if ($scaled_result) {
            // Update file path and record info if scaling happened
            $file = $scaled_result['file'];
            $record = $scaled_result['record'];
            $metadata = $scaled_result['metadata'];

            // Update WordPress metadata with original_image
            if (!empty($scaled_result['original_image'])) {
                $metadata['original_image'] = $scaled_result['original_image'];
                wp_update_attachment_metadata($attachment_id, $metadata);
            }

            // Update _wp_attached_file to point to scaled version
            update_attached_file($attachment_id, $file);

            sqmedia()->log('cron: master scaled', 'debug', [
                'attachment_id' => $attachment_id,
                'new_master_path' => $record->master_path,
                'original_image' => $scaled_result['original_image'] ?? null,
            ]);
        }

        // Step 1: Upload master to bucket (cloud modes only)
        if ($cloud_enabled) {
            $s3 = sqmedia()->get_storage_handler();
            $bucket_path = $bucket_prefix . '/' . $record->master_path;

            $master_upload_ok = $s3->upload_file($file, $bucket_path);
            sqmedia()->log('cron: master upload', 'debug', [
                'attachment_id' => $attachment_id,
                'bucket_path' => $bucket_path,
                'success' => $master_upload_ok,
                'error' => $master_upload_ok ? null : $s3->get_last_error(),
            ]);

            if (!$master_upload_ok) {
                return [
                    'success' => false,
                    'error' => 'Failed to upload master to bucket: ' . $s3->get_last_error(),
                ];
            }
            $master_bucket = true;

            // Step 2: Place the unscaled original at the configured location
            // (local + bucket). Handles both `preserve` (alongside master in
            // the main upload directory) and `move_to_originals` (under an
            // `originals/<master_dir>/` subdirectory). The helper detects the
            // file's current local location and moves it only when needed.
            if (!empty($metadata['original_image'])) {
                $this->place_original_file(
                    $upload_dir['basedir'],
                    $bucket_prefix,
                    $master_dir,
                    $metadata['original_image'],
                    $s3
                );
            }
        } else {
            // Local-only mode: still need to honor `move_to_originals` by
            // moving the local original into the originals/ subdirectory.
            if (!empty($metadata['original_image'])) {
                $this->place_original_file(
                    $upload_dir['basedir'],
                    $bucket_prefix,
                    $master_dir,
                    $metadata['original_image'],
                    null
                );
            }
        }

        // Check if this image type supports resizing (SVG, ICO don't)
        $attachment_record = SQMedia_Attachment_Record::find($attachment_id);
        $supports_resizing = $attachment_record && $attachment_record->supports_resizing();

        // For non-resizable images (SVG, ICO), just upload master - no sizes needed
        if (!$supports_resizing) {

            // Delete local if configured
            if ($delete_local && $master_bucket && file_exists($file)) {
                wp_delete_file($file);
                $master_local = false;
            }

            // Update record
            $attachment_record->set_storage_location($this->determine_storage_location($master_local, $master_bucket));
            $attachment_record->set_status('completed');
            $attachment_record->save();

            return [
                'success' => true,
                'sizes' => [],
                'webp_sizes' => [],
                'master_local' => $master_local,
                'master_bucket' => $master_bucket,
                'is_image' => true,
                'supports_resizing' => false,
            ];
        }

        // Step 3: Generate/upload sizes
        if ($use_cloudflare) {
            sqmedia()->log('cron: sizes path = cloudflare', 'debug', ['attachment_id' => $attachment_id]);
            // Generate sizes via Cloudflare (only reachable when cloud is enabled — see force above)
            $sizes = $this->generate_sizes_cloudflare($record, $metadata, $bucket_prefix);

            // If storage is "both", download Cloudflare-generated sizes to local
            if (!$delete_local && !empty($sizes)) {
                $this->download_cloudflare_sizes_to_local($sizes, $record->master_dir, $bucket_prefix, $upload_dir['basedir']);
            }
        } else {
            // WordPress generated sizes at upload time (or we need to generate them now)
            // Check if sizes exist in metadata
            if (!empty($metadata['sizes'])) {
                sqmedia()->log('cron: sizes path = upload_existing', 'debug', ['attachment_id' => $attachment_id]);
                // Sizes were generated by WordPress - register them, upload to bucket if cloud is on
                $sizes = $this->upload_existing_sizes($record, $metadata, $bucket_prefix, $upload_dir['basedir'], $cloud_enabled);
            } else {
                sqmedia()->log('cron: sizes path = generate_wordpress', 'debug', ['attachment_id' => $attachment_id]);
                // No sizes exist - generate them using WordPress, upload to bucket if cloud is on
                $sizes = $this->generate_sizes_wordpress($record, $metadata, $bucket_prefix, $upload_dir['basedir'], $cloud_enabled);
            }
        }

        sqmedia()->log('cron: sizes done', 'debug', [
            'attachment_id' => $attachment_id,
            'count' => count($sizes),
            'sizes' => array_keys($sizes),
        ]);

        // Guard against silent size-generation failures.
        //
        // Several silent-skip paths exist in the size pipeline: GD/Imagick
        // returning WP_Error on allocation, Cloudflare returning a transient
        // 5xx, missing local source files (race between WP's own size pass
        // and our cron pick-up), etc. Without this guard, the function would
        // continue to WebP / metadata update / 'success' and the record
        // would be marked `completed` even though nothing was persisted —
        // the Media Library Scanner would later flag the same files as
        // "Missing Sizes". Returning failure here puts the record back into
        // the queue (status='failed', retry_count incremented); cron will
        // retry up to max_retries times on subsequent ticks.
        $master_w = (int) ($metadata['width'] ?? 0);
        $master_h = (int) ($metadata['height'] ?? 0);
        $expected_size_count = 0;
        foreach (SQMedia_Settings::get_enabled_sizes() as $size_name => $size_config) {
            if ($this->should_generate_size(
                $master_w,
                $master_h,
                (int) ($size_config['width'] ?? 0),
                (int) ($size_config['height'] ?? 0),
                !empty($size_config['crop'])
            )) {
                $expected_size_count++;
            }
        }
        $persisted_size_count = 0;
        foreach ($sizes as $size_entry) {
            if (!empty($size_entry['local']) || !empty($size_entry['bucket'])) {
                $persisted_size_count++;
            }
        }
        if ($expected_size_count > 0 && $persisted_size_count === 0) {
            return [
                'success' => false,
                'error' => sprintf(
                    'No sizes persisted: %d expected, 0 produced. Likely a transient image-editor or CDN failure; cron will retry.',
                    $expected_size_count
                ),
            ];
        }

        // Step 4: Generate WebP
        if ($enable_webp) {
            $webp_sizes = $this->generate_webp($record, $metadata, $sizes, $bucket_prefix, $use_cloudflare, $upload_dir['basedir'], $cloud_enabled);

            // If storage is "both" and using Cloudflare, download WebP files to local
            if (!$delete_local && $use_cloudflare && !empty($webp_sizes)) {
                $this->download_cloudflare_sizes_to_local($webp_sizes, $record->master_dir, $bucket_prefix, $upload_dir['basedir']);
            }

            sqmedia()->log('cron: webp done', 'debug', [
                'attachment_id' => $attachment_id,
                'count' => count($webp_sizes),
                'webp_keys' => array_keys($webp_sizes),
            ]);

            // Guard against silent WebP-generation failures. If WebP is
            // enabled and the master MIME is in the supported list, we
            // expect at least one WebP output (the master at minimum).
            // Empty result means every WebP attempt silently failed; treat
            // that as a real failure so the record retries.
            $mime_type = get_post_mime_type($attachment_id);
            if ($this->should_generate_webp_for_mime((string) $mime_type) && empty($webp_sizes)) {
                return [
                    'success' => false,
                    'error' => 'WebP generation produced no output despite being enabled for this MIME type. Likely a transient failure; cron will retry.',
                ];
            }
        } else {
            sqmedia()->log('cron: webp skipped (disabled)', 'debug', ['attachment_id' => $attachment_id]);
        }

        // Step 5: Delete local files if configured
        if ($delete_local && $master_bucket) {
            $this->delete_local_files($file, $metadata, $master_dir, $upload_dir['basedir'], $sizes);
            $master_local = false;

            // Update sizes to reflect local deletion
            foreach ($sizes as $name => &$size) {
                $size['local'] = false;
            }
            foreach ($webp_sizes as $name => &$webp) {
                $webp['local'] = false;
            }
        }
        
        // Step 6: Update WordPress metadata (including WebP sizes)
        $this->update_wp_metadata($attachment_id, $metadata, $sizes, $webp_sizes);
        
        // Determine final storage location
        $storage_location = $this->determine_storage_location($master_local, $master_bucket);
        
        return [
            'success' => true,
            'storage_location' => $storage_location,
            'master_local' => $master_local,
            'master_bucket' => $master_bucket,
            'webp_generated' => !empty($webp_sizes),
            'original_image' => $metadata['original_image'] ?? null,
            'is_image' => true,
        ];
    }
    
    /**
     * Generate sizes via Cloudflare
     */
    private function generate_sizes_cloudflare(object $record, array $metadata, string $bucket_prefix): array {
        $sizes = [];
        $enabled_sizes = SQMedia_Settings::get_enabled_sizes();
        $cloudflare = sqmedia()->get_cloudflare_handler();
        $s3 = sqmedia()->get_storage_handler();
        
        // Get master dimensions for skip check
        $master_w = $metadata['width'] ?? 0;
        $master_h = $metadata['height'] ?? 0;
        
        // Use original_image basename for subsizes when present (scaled images).
        // Do NOT blindly strip '-scaled' — the filename could legitimately contain it.
        if (!empty($metadata['original_image'])) {
            $size_basename = pathinfo($metadata['original_image'], PATHINFO_FILENAME);
        } else {
            $size_basename = pathinfo($record->master_filename, PATHINFO_FILENAME);
        }
        $size_ext = pathinfo($record->master_filename, PATHINFO_EXTENSION);

        // Build the master file URL in bucket
        $source_url = SQMedia_Settings::build_bucket_url($record->master_path);

        // Log URLs for debugging

        foreach ($enabled_sizes as $size_name => $size_config) {
            $target_w = $size_config['width'] ?? 0;
            $target_h = $size_config['height'] ?? 0;
            // Normalize crop value
            $crop = !empty($size_config['crop']);

            // Skip if master is too small (would require upscaling)
            if (!$this->should_generate_size($master_w, $master_h, $target_w, $target_h, $crop)) {
                sqmedia()->log('cf-resize: skip (master too small)', 'debug', [
                    'size' => $size_name,
                    'target' => "{$target_w}x{$target_h}",
                    'master' => "{$master_w}x{$master_h}",
                ]);
                continue;
            }

            $result = $cloudflare->generate_size(
                $source_url,
                $target_w,
                $target_h,
                $crop
            );

            if ($result && !empty($result['content'])) {
                // CRITICAL: Use ACTUAL dimensions from Cloudflare for the filename
                // WordPress names files based on actual dimensions, not target dimensions
                // Example: A 1200x800 image resized to fit 300x300 becomes image-300x200.jpg, not image-300x300.jpg
                $actual_width = $result['width'];
                $actual_height = $result['height'];

                $filename = "{$size_basename}-{$actual_width}x{$actual_height}.{$size_ext}";

                $dest_path = $bucket_prefix . '/' . ($record->master_dir ? $record->master_dir . '/' : '') . $filename;

                $upload_ok = $s3->upload_content($result['content'], $dest_path, [
                    'content_type' => $result['content_type'] ?? 'image/jpeg',
                ]);

                sqmedia()->log('cf-resize: size generated', 'debug', [
                    'size' => $size_name,
                    'target' => "{$target_w}x{$target_h}",
                    'actual' => "{$actual_width}x{$actual_height}",
                    'bytes' => strlen($result['content']),
                    'upload_ok' => $upload_ok,
                    'upload_error' => $upload_ok ? null : $s3->get_last_error(),
                ]);

                if ($upload_ok) {
                    $sizes[$size_name] = [
                        'file' => $filename,
                        'width' => $actual_width,
                        'height' => $actual_height,
                        'mime_type' => $result['content_type'] ?? 'image/jpeg',
                        'local' => false,
                        'bucket' => true,
                    ];
                }
            } else {
                sqmedia()->log('cf-resize: size FAILED', 'warning', [
                    'size' => $size_name,
                    'target' => "{$target_w}x{$target_h}",
                    'source_url' => $source_url,
                    'cf_error' => $cloudflare->get_last_error(),
                ]);
            }
        }

        sqmedia()->log('cf-resize: summary', 'debug', [
            'requested' => count($enabled_sizes),
            'succeeded' => count($sizes),
        ]);

        return $sizes;
    }
    
    /**
     * Download Cloudflare-generated sizes to local storage
     * Used when storage is "both" to ensure files exist locally after Cloudflare generation
     * 
     * @param array &$sizes Array of sizes (passed by reference to update 'local' flags)
     */
    private function download_cloudflare_sizes_to_local(array &$sizes, string $master_dir, string $bucket_prefix, string $basedir): void {
        $s3 = sqmedia()->get_storage_handler();
        $downloaded_count = 0;
        
        foreach ($sizes as $size_name => &$size_data) {
            // Skip if already local
            if (!empty($size_data['local'])) {
                continue;
            }
            
            // Skip if not in bucket
            if (empty($size_data['bucket'])) {
                continue;
            }
            
            $filename = $size_data['file'] ?? '';
            if (empty($filename)) {
                continue;
            }
            
            $local_path = $basedir . '/' . ($master_dir ? $master_dir . '/' : '') . $filename;
            $bucket_path = $bucket_prefix . '/' . ($master_dir ? $master_dir . '/' : '') . $filename;
            
            // Ensure directory exists
            $local_dir = dirname($local_path);
            if (!file_exists($local_dir)) {
                wp_mkdir_p($local_dir);
            }
            
            // Download from bucket
            if ($s3->download_file($bucket_path, $local_path)) {
                $size_data['local'] = true;
                $downloaded_count++;
            } else {
            }
        }
        unset($size_data); // Break the reference
        
        if ($downloaded_count > 0) {
        }
    }
    
    /**
     * Upload existing local sizes to bucket
     */
    private function upload_existing_sizes(object $record, array $metadata, string $bucket_prefix, string $basedir, bool $cloud_enabled = true): array {
        $sizes = [];
        $s3 = $cloud_enabled ? sqmedia()->get_storage_handler() : null;
        $enabled_sizes = SQMedia_Settings::get_enabled_sizes();

        if (empty($metadata['sizes'])) {
            return $sizes;
        }

        foreach ($metadata['sizes'] as $size_name => $size_data) {
            // Skip disabled sizes
            if (!isset($enabled_sizes[$size_name])) {
                continue;
            }

            $local_path = $basedir . '/' . ($record->master_dir ? $record->master_dir . '/' : '') . $size_data['file'];
            $local_exists = file_exists($local_path);
            $bucket_exists = false;

            if ($local_exists && $cloud_enabled) {
                $bucket_path = $bucket_prefix . '/' . ($record->master_dir ? $record->master_dir . '/' : '') . $size_data['file'];

                if ($s3->upload_file($local_path, $bucket_path)) {
                    $bucket_exists = true;
                }
            }

            $sizes[$size_name] = [
                'file' => $size_data['file'],
                'width' => $size_data['width'],
                'height' => $size_data['height'],
                'mime_type' => $size_data['mime-type'] ?? '',
                'local' => $local_exists,
                'bucket' => $bucket_exists,
            ];
        }

        return $sizes;
    }
    
    /**
     * Generate sizes using WordPress image editor (when Cloudflare is disabled)
     * 
     * This is used when:
     * 1. Cloudflare is disabled in settings
     * 2. OR sizes weren't generated at upload time for some reason
     * 
     * @param object $record Database record
     * @param array $metadata WordPress attachment metadata
     * @param string $bucket_prefix Bucket prefix
     * @param string $basedir WordPress uploads basedir
     * @return array Generated sizes data
     */
    private function generate_sizes_wordpress(object $record, array $metadata, string $bucket_prefix, string $basedir, bool $cloud_enabled = true): array {
        $sizes = [];
        $enabled_sizes = SQMedia_Settings::get_enabled_sizes();
        $s3 = $cloud_enabled ? sqmedia()->get_storage_handler() : null;
        $attachment_id = $record->attachment_id;
        
        // Get master dimensions for skip check
        $master_w = $metadata['width'] ?? 0;
        $master_h = $metadata['height'] ?? 0;
        
        // Get the master file path
        $master_path = $basedir . '/' . $record->master_path;
        
        if (!file_exists($master_path)) {
            return $sizes;
        }
        
        
        // Use original_image basename for subsizes when present (scaled images).
        // Do NOT blindly strip '-scaled' — the filename could legitimately contain it.
        if (!empty($metadata['original_image'])) {
            $base_filename = pathinfo($metadata['original_image'], PATHINFO_FILENAME);
        } else {
            $base_filename = pathinfo($record->master_filename, PATHINFO_FILENAME);
        }
        $extension = pathinfo($record->master_filename, PATHINFO_EXTENSION);

        foreach ($enabled_sizes as $size_name => $size_config) {
            $target_w = $size_config['width'] ?? 0;
            $target_h = $size_config['height'] ?? 0;
            // Normalize crop value
            $crop = !empty($size_config['crop']);
            
            // Skip if master is too small (would require upscaling)
            if (!$this->should_generate_size($master_w, $master_h, $target_w, $target_h, $crop)) {
                continue;
            }
            
            
            // Create a fresh editor for each size
            $size_editor = wp_get_image_editor($master_path);
            
            if (is_wp_error($size_editor)) {
                continue;
            }
            
            // Resize the image
            $resized = $size_editor->resize($target_w, $target_h, $crop);
            
            if (is_wp_error($resized)) {
                continue;
            }
            
            // Get actual dimensions after resize
            $dimensions = $size_editor->get_size();
            $actual_width = $dimensions['width'];
            $actual_height = $dimensions['height'];
            
            // Generate filename with actual dimensions (WordPress convention)
            $size_filename = "{$base_filename}-{$actual_width}x{$actual_height}.{$extension}";
            $local_size_path = $basedir . '/' . ($record->master_dir ? $record->master_dir . '/' : '') . $size_filename;
            
            // Save the resized image — pass explicit mime type to prevent
            // WordPress output format filters from converting JPEG to WebP.
            $source_mime = wp_check_filetype($master_path)['type'] ?: 'image/jpeg';
            $saved = $size_editor->save($local_size_path, $source_mime);

            if (is_wp_error($saved)) {
                continue;
            }

            // Use the actual saved path/filename — WordPress may alter it
            $actual_file = $saved['file'] ?? $size_filename;
            $actual_path = $saved['path'] ?? $local_size_path;

            // Upload to bucket if cloud storage is enabled
            $bucket_exists = false;
            if ($cloud_enabled) {
                $bucket_path = $bucket_prefix . '/' . ($record->master_dir ? $record->master_dir . '/' : '') . $actual_file;
                if ($s3->upload_file($actual_path, $bucket_path)) {
                    $bucket_exists = true;
                }
            }

            $sizes[$size_name] = [
                'file' => $actual_file,
                'width' => $actual_width,
                'height' => $actual_height,
                'mime_type' => $saved['mime-type'] ?? $source_mime,
                'local' => file_exists($actual_path),
                'bucket' => $bucket_exists,
            ];
        }
        
        return $sizes;
    }
    
    /**
     * Generate WebP versions
     */
    private function generate_webp(
        object $record,
        array $metadata,
        array $sizes,
        string $bucket_prefix,
        bool $use_cloudflare = true,
        string $basedir = '',
        bool $cloud_enabled = true
    ): array {
        $webp = [];
        $quality = SQMedia_Settings::get_quality_value();
        $s3 = $cloud_enabled ? sqmedia()->get_storage_handler() : null;

        // Check if we should generate WebP for this image type
        $attachment_id = $record->attachment_id;
        $mime_type = get_post_mime_type($attachment_id);
        if (!$this->should_generate_webp_for_mime($mime_type)) {
            return $webp;
        }

        $dir_prefix = $record->master_dir ? $record->master_dir . '/' : '';

        // Master WebP
        $webp_filename = $this->get_webp_filename($record->master_filename);

        if ($use_cloudflare) {
            $cloudflare = sqmedia()->get_cloudflare_handler();
            $source_url = SQMedia_Settings::build_bucket_url($record->master_path);

            $result = $cloudflare->generate_webp($source_url, $quality);

            if ($result && !empty($result['content'])) {
                $dest_path = $bucket_prefix . '/' . $dir_prefix . $webp_filename;

                $upload_ok = $s3->upload_content($result['content'], $dest_path, ['content_type' => 'image/webp']);
                sqmedia()->log('cf-webp: master generated', 'debug', [
                    'webp_filename' => $webp_filename,
                    'bytes' => strlen($result['content']),
                    'upload_ok' => $upload_ok,
                ]);

                if ($upload_ok) {
                    $webp['master'] = [
                        'file' => $webp_filename,
                        'local' => false,
                        'bucket' => true,
                    ];
                }
            } else {
                sqmedia()->log('cf-webp: master FAILED', 'warning', [
                    'source_url' => $source_url,
                    'cf_error' => $cloudflare->get_last_error(),
                ]);
            }
        } else {
            // Local WebP generation via WordPress image editor
            $local_source = $basedir . '/' . $record->master_path;
            $local_dest = $basedir . '/' . $dir_prefix . $webp_filename;

            $generated = SQMedia_Image_Processor::generate_webp($local_source, $local_dest, $quality);

            sqmedia()->log('local-webp: master', 'debug', [
                'source' => $local_source,
                'dest' => $local_dest,
                'generated' => $generated,
            ]);

            if ($generated) {
                $bucket_exists = false;
                if ($cloud_enabled) {
                    $dest_path = $bucket_prefix . '/' . $dir_prefix . $webp_filename;
                    if ($s3->upload_file($local_dest, $dest_path)) {
                        $bucket_exists = true;
                    }
                }

                $webp['master'] = [
                    'file' => $webp_filename,
                    'local' => true,
                    'bucket' => $bucket_exists,
                ];
            }
        }

        // Size WebPs
        $enabled_sizes = SQMedia_Settings::get_enabled_sizes();
        foreach ($sizes as $size_name => $size_data) {
            if ($use_cloudflare) {
                // Generate from MASTER with dimensions via Cloudflare
                $cloudflare = $cloudflare ?? sqmedia()->get_cloudflare_handler();
                $source_url = SQMedia_Settings::build_bucket_url($record->master_path);

                $target_width = $size_data['width'];
                $target_height = $size_data['height'];
                $crop = !empty($enabled_sizes[$size_name]['crop']);

                $result = $cloudflare->generate_webp_size($source_url, $target_width, $target_height, $crop, $quality);

                if ($result && !empty($result['content'])) {
                    $actual_width = $result['width'];
                    $actual_height = $result['height'];

                    if (!empty($metadata['original_image'])) {
                        $base_filename = pathinfo($metadata['original_image'], PATHINFO_FILENAME);
                    } else {
                        $base_filename = pathinfo($record->master_filename, PATHINFO_FILENAME);
                    }
                    $size_webp_filename = "{$base_filename}-{$actual_width}x{$actual_height}.webp";

                    $dest_path = $bucket_prefix . '/' . $dir_prefix . $size_webp_filename;

                    $upload_ok = $s3->upload_content($result['content'], $dest_path, ['content_type' => 'image/webp']);
                    sqmedia()->log('cf-webp: size generated', 'debug', [
                        'size' => $size_name,
                        'target' => "{$target_width}x{$target_height}",
                        'actual' => "{$actual_width}x{$actual_height}",
                        'bytes' => strlen($result['content']),
                        'upload_ok' => $upload_ok,
                    ]);

                    if ($upload_ok) {
                        $webp[$size_name] = [
                            'file' => $size_webp_filename,
                            'width' => $actual_width,
                            'height' => $actual_height,
                            'local' => false,
                            'bucket' => true,
                        ];
                    }
                } else {
                    sqmedia()->log('cf-webp: size FAILED', 'warning', [
                        'size' => $size_name,
                        'target' => "{$target_width}x{$target_height}",
                        'cf_error' => $cloudflare->get_last_error(),
                    ]);
                }
            } else {
                // Local WebP generation from the already-resized JPEG
                $local_jpeg = $basedir . '/' . $dir_prefix . $size_data['file'];
                $size_webp_filename = $this->get_webp_filename($size_data['file']);
                $local_webp = $basedir . '/' . $dir_prefix . $size_webp_filename;

                $generated = SQMedia_Image_Processor::generate_webp($local_jpeg, $local_webp, $quality);

                if ($generated) {
                    $bucket_exists = false;
                    if ($cloud_enabled) {
                        $dest_path = $bucket_prefix . '/' . $dir_prefix . $size_webp_filename;
                        if ($s3->upload_file($local_webp, $dest_path)) {
                            $bucket_exists = true;
                        }
                    }

                    $webp[$size_name] = [
                        'file' => $size_webp_filename,
                        'width' => $size_data['width'],
                        'height' => $size_data['height'],
                        'local' => true,
                        'bucket' => $bucket_exists,
                    ];
                }
            }
        }

        return $webp;
    }
    
    /**
     * Check if should generate WebP for mime type
     */
    private function should_generate_webp_for_mime(string $mime_type): bool {
        switch ($mime_type) {
            case 'image/jpeg':
            case 'image/jpg':
                return true;
            case 'image/png':
            case 'image/gif':
                return true; // Could add settings for these
            default:
                return false;
        }
    }
    
    /**
     * Place the unscaled original at the configured local + bucket location.
     *
     * Reads `original_file_handling`:
     *   - `preserve`            → original stays alongside the master in the
     *                             main upload directory.
     *   - `move_to_originals`   → original lives under
     *                             `<uploads>/originals/<master_dir>/<file>`
     *                             locally and at the same relative path in
     *                             the bucket.
     *   - `discard` / unknown   → no-op.
     *
     * Idempotent: detects the current local location (main folder or
     * originals/ folder) and only moves when the file is not already at
     * the target. Always uploads to the bucket target when an S3 handler
     * is supplied (cloud_and_local or cloud_only).
     */
    private function place_original_file(
        string $basedir,
        string $bucket_prefix,
        string $master_dir,
        string $original_basename,
        $s3 = null
    ): void {
        $handling = SQMedia_Settings::get_original_file_handling();
        if ($handling !== 'preserve' && $handling !== 'move_to_originals') {
            return;
        }

        $rel_main      = ($master_dir ? $master_dir . '/' : '') . $original_basename;
        $rel_originals = 'originals/' . $rel_main;
        $rel_target    = ($handling === 'move_to_originals') ? $rel_originals : $rel_main;

        $local_main      = $basedir . '/' . $rel_main;
        $local_originals = $basedir . '/' . $rel_originals;
        $local_target    = $basedir . '/' . $rel_target;

        // Find current local location of the original.
        $current_local = null;
        if (file_exists($local_main)) {
            $current_local = $local_main;
        } elseif (file_exists($local_originals)) {
            $current_local = $local_originals;
        }

        // Move local file to the configured target location if needed.
        if ($current_local && $current_local !== $local_target && !file_exists($local_target)) {
            $target_dir = dirname($local_target);
            if (!file_exists($target_dir)) {
                wp_mkdir_p($target_dir);
            }
            // phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename
            if (@rename($current_local, $local_target)) {
                $current_local = $local_target;
            }
        }

        // Upload to bucket target if cloud is enabled and we have a local copy.
        if ($s3 && $current_local && file_exists($current_local)) {
            $bucket_target = $bucket_prefix . '/' . $rel_target;
            $s3->upload_file($current_local, $bucket_target);
        }
    }

    /**
     * Delete local files after successful bucket upload
     */
    private function delete_local_files(string $master_file, array $metadata, string $master_dir, string $basedir, array $generated_sizes = []): void {
        // Delete master
        if (file_exists($master_file)) {
            wp_delete_file($master_file);
        }

        // Delete sizes - check both metadata and generated sizes
        $sizes_to_delete = [];

        // From metadata (WordPress-generated sizes)
        if (!empty($metadata['sizes'])) {
            foreach ($metadata['sizes'] as $size_data) {
                if (!empty($size_data['file'])) {
                    $sizes_to_delete[$size_data['file']] = true;
                }
            }
        }

        // From generated sizes (Cloudflare-generated sizes)
        foreach ($generated_sizes as $size_data) {
            if (!empty($size_data['file'])) {
                $sizes_to_delete[$size_data['file']] = true;
            }
        }

        // Delete all size files
        foreach (array_keys($sizes_to_delete) as $filename) {
            $path = $basedir . '/' . ($master_dir ? $master_dir . '/' : '') . $filename;
            if (file_exists($path)) {
                wp_delete_file($path);
            }
        }

        // Delete original if exists. The original may live alongside the
        // master in the main folder (`preserve` mode) or under
        // originals/<master_dir>/ (`move_to_originals` mode) — check both.
        if (!empty($metadata['original_image'])) {
            $rel_main      = ($master_dir ? $master_dir . '/' : '') . $metadata['original_image'];
            $rel_originals = 'originals/' . $rel_main;
            foreach ([$rel_main, $rel_originals] as $rel) {
                $path = $basedir . '/' . $rel;
                if (file_exists($path)) {
                    wp_delete_file($path);
                    break;
                }
            }
        }

        // Delete WebP files
        $webp_master = $this->get_webp_filename($master_file);
        if (file_exists($webp_master)) {
            wp_delete_file($webp_master);
        }

        // Delete WebP sizes
        foreach (array_keys($sizes_to_delete) as $filename) {
            $webp_filename = preg_replace('/\.(jpg|jpeg|png|gif)$/i', '.webp', $filename);
            $webp_path = $basedir . '/' . ($master_dir ? $master_dir . '/' : '') . $webp_filename;
            if (file_exists($webp_path)) {
                wp_delete_file($webp_path);
            }
        }
    }
    
    /**
     * Maybe scale big image
     * 
     * If the image exceeds the threshold dimensions, scale it down.
     * Uses Cloudflare if enabled, otherwise falls back to WordPress.
     * The original file is preserved based on original_file_handling setting.
     * 
     * @param string $file Local file path
     * @param object $record Database record
     * @param array $metadata WordPress attachment metadata
     * @param string $bucket_prefix Bucket prefix
     * @param bool $use_cloudflare Whether Cloudflare is enabled
     * @return array|null Scaled result or null if no scaling needed
     */
    private function maybe_scale_big_image(string $file, object $record, array $metadata, string $bucket_prefix, bool $use_cloudflare, bool $cloud_enabled = true): ?array {
        // Check if scaling is enabled
        if (!SQMedia_Settings::is_big_image_scaling_enabled()) {
            return null;
        }

        $threshold_width = SQMedia_Settings::get_big_image_width();
        $threshold_height = SQMedia_Settings::get_big_image_height();

        // Get image dimensions
        $image_size = @wp_getimagesize($file);
        if (!$image_size) {
            return null;
        }

        list($width, $height) = $image_size;


        // Check if scaling is needed
        if ($width <= $threshold_width && $height <= $threshold_height) {
            return null;
        }


        $s3 = $cloud_enabled ? sqmedia()->get_storage_handler() : null;
        $path_info = pathinfo($file);
        $extension = !empty($path_info['extension']) ? $path_info['extension'] : 'jpg';
        $scaled_filename = $path_info['filename'] . '-scaled.' . $extension;
        $scaled_path = $path_info['dirname'] . '/' . $scaled_filename;
        $scaled_width = null;
        $scaled_height = null;
        
        if ($use_cloudflare) {
            // Upload original to bucket first for Cloudflare to access
            $original_bucket_path = $bucket_prefix . '/' . $record->master_path;
            
            if (!$s3->upload_file($file, $original_bucket_path)) {
                return null;
            }
            
            // Use Cloudflare to scale
            $cloudflare = sqmedia()->get_cloudflare_handler();
            $source_url = SQMedia_Settings::build_bucket_url($record->master_path);
            
            
            $result = $cloudflare->generate_size($source_url, $threshold_width, $threshold_height, false);
            
            if (!$result || empty($result['content'])) {
                // Clean up - delete the uploaded original since scaling failed
                $s3->delete_file($original_bucket_path);
                return null;
            }
            
            $scaled_width = $result['width'];
            $scaled_height = $result['height'];
            
            // Save scaled version locally
            if (file_put_contents($scaled_path, $result['content']) === false) {
                $s3->delete_file($original_bucket_path);
                return null;
            }
            
            
        } else {
            // Use WordPress image editor
            $editor = wp_get_image_editor($file);
            
            if (is_wp_error($editor)) {
                return null;
            }
            
            // Resize to threshold dimensions (maintain aspect ratio)
            $resized = $editor->resize($threshold_width, $threshold_height, false);
            
            if (is_wp_error($resized)) {
                return null;
            }
            
            // Get actual dimensions
            $dimensions = $editor->get_size();
            $scaled_width = $dimensions['width'];
            $scaled_height = $dimensions['height'];
            
            // Save the resized image (let WordPress detect mime type from extension)
            $saved = $editor->save($scaled_path);
            
            if (is_wp_error($saved)) {
                return null;
            }
            
            
            // Set original_bucket_path for original handling (it wasn't uploaded yet)
            $original_bucket_path = null;
        }
        
        
        // Mark the original_image in metadata for `preserve` and
        // `move_to_originals` modes. Final placement (local move + bucket
        // upload to the configured location) is handled by
        // place_original_file() in process_image() — this function only
        // produces the scaled local file and stages the CF source.
        $original_handling = SQMedia_Settings::get_original_file_handling();
        $original_image = null;
        if ($original_handling === 'preserve' || $original_handling === 'move_to_originals') {
            $original_image = $record->master_filename;
        }

        // CF source cleanup: when Cloudflare Image Resizing was used, the
        // original was uploaded to its main bucket path purely so the CF
        // worker could fetch it. Step 2 will re-upload to the configured
        // final location, so remove the staging copy here.
        if ($cloud_enabled && $use_cloudflare && $original_bucket_path) {
            $s3->delete_file($original_bucket_path);
        }
        
        // Update record info for scaled version
        $new_master_path = ($record->master_dir ? $record->master_dir . '/' : '') . $scaled_filename;
        
        // Create modified record object
        $new_record = clone $record;
        $new_record->master_path = $new_master_path;
        $new_record->master_filename = $scaled_filename;
        
        // Update database with new master path
        $this->db->update_record($record->attachment_id, [
            'master_path' => $new_master_path,
            'master_filename' => $scaled_filename,
        ]);
        
        // Update metadata dimensions and filesize
        $metadata['width'] = $scaled_width;
        $metadata['height'] = $scaled_height;
        $metadata['file'] = $new_master_path;
        
        // Update filesize to reflect the scaled file, not the original
        if (file_exists($scaled_path)) {
            $metadata['filesize'] = filesize($scaled_path);
        }
        
        // Determine original_location based on what actually happened
        $original_location = null;
        if ($original_image) {
            $original_location = ($original_handling === 'move_to_originals') ? 'originals' : 'alongside';
        }

        return [
            'file' => $scaled_path,
            'record' => $new_record,
            'metadata' => $metadata,
            'original_image' => $original_image,
            'original_location' => $original_location,
        ];
    }
    
    /**
     * Determine storage location based on file existence
     */
    private function determine_storage_location(bool $local, bool $bucket): string {
        if ($local && $bucket) {
            return 'both';
        } elseif ($bucket) {
            return 'bucket';
        } else {
            return 'local';
        }
    }
    
    /**
     * Get WebP filename from original filename
     */
    private function get_webp_filename(string $filename): string {
        $ext = pathinfo($filename, PATHINFO_EXTENSION);
        return str_replace('.' . $ext, '.webp', $filename);
    }
    
    /**
     * Update WordPress metadata with generated sizes
     *
     * WebP files are generated separately but NOT tracked in WP metadata.
     * WebP URLs are derived at runtime from regular sizes + the has_webp flag.
     */
    private function update_wp_metadata(int $attachment_id, array $metadata, array $sizes, array $webp_sizes = []): void {
        $changed = false;

        // Add regular sizes
        foreach ($sizes as $size_name => $size_data) {
            if (!isset($metadata['sizes'][$size_name])) {
                $metadata['sizes'][$size_name] = [
                    'file' => $size_data['file'],
                    'width' => $size_data['width'],
                    'height' => $size_data['height'],
                    'mime-type' => $size_data['mime_type'] ?? 'image/jpeg',
                ];
                $changed = true;
            }
        }

        if ($changed) {
            wp_update_attachment_metadata($attachment_id, $metadata);
        }
    }
    
    /**
     * Check if a size should be generated based on master dimensions
     * 
     * Returns false if master is too small (would require upscaling).
     * Uses same logic as Phase 1 scanner for consistency.
     * 
     * @param int $master_w Master file width
     * @param int $master_h Master file height
     * @param int $target_w Target size width (0 = unconstrained)
     * @param int $target_h Target size height (0 = unconstrained)
     * @param bool $crop Whether this is a cropped size
     * @return bool True if size should be generated
     */
    private function should_generate_size(int $master_w, int $master_h, int $target_w, int $target_h, bool $crop): bool {
        if ($master_w <= 0 || $master_h <= 0) {
            return false;
        }
        
        // Handle 0 values (unconstrained dimension)
        if ($target_w <= 0 && $target_h <= 0) {
            return false;
        }
        
        if ($crop) {
            // Cropped sizes require both dimensions and master must be >= target
            if ($target_w <= 0 || $target_h <= 0) {
                return false;
            }
            if ($master_w < $target_w || $master_h < $target_h) {
                return false; // Master too small to crop
            }
            return true;
        }
        
        // Proportional resize - use WordPress function
        $constrain_w = $target_w > 0 ? $target_w : 9999;
        $constrain_h = $target_h > 0 ? $target_h : 9999;
        
        list($dest_w, $dest_h) = wp_constrain_dimensions($master_w, $master_h, $constrain_w, $constrain_h);
        
        // If dimensions didn't change (or would increase), don't generate
        if ($dest_w >= $master_w && $dest_h >= $master_h) {
            return false;
        }
        
        return true;
    }

    /**
     * Fix stale image URLs in post_content after scaling.
     *
     * When big image scaling renames e.g. image.jpg → image-scaled.jpg,
     * any posts that already reference the old filename become stale.
     * This finds and replaces those references directly in the DB.
     */
    private function fix_post_content_after_scaling(int $attachment_id, array $metadata): void {
        if (empty($metadata['original_image']) || empty($metadata['file'])) {
            return;
        }

        $current_filename = wp_basename($metadata['file']);
        $original_filename = $metadata['original_image'];

        // No rename happened
        if ($original_filename === $current_filename) {
            return;
        }

        $dir = dirname($metadata['file']);
        $old_rel = ($dir && $dir !== '.' ? $dir . '/' : '') . $original_filename;
        $new_rel = $metadata['file'];

        global $wpdb;

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $affected = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT ID, post_content FROM {$wpdb->posts} WHERE post_content LIKE %s",
                '%' . $wpdb->esc_like($old_rel) . '%'
            )
        );

        foreach ($affected as $post) {
            $updated_content = str_replace($old_rel, $new_rel, $post->post_content);
            if ($updated_content !== $post->post_content) {
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                $wpdb->update($wpdb->posts, ['post_content' => $updated_content], ['ID' => $post->ID]);
                clean_post_cache($post->ID);
            }
        }
    }
}