<?php
/**
 * File State Scanner - Phase 1 Handler
 *
 * Phase 1: Database Alignment
 * Checks WordPress postmeta against current settings and fixes metadata issues.
 *
 * Issue categories handled:
 * - CAT_CORRUPTED: Missing dimensions or unreadable metadata (fixable - file exists)
 * - CAT_CRITICAL_MISSING: Metadata issue but master file missing (unfixable)
 * - CAT_WRONG_DIMENSIONS: Size has wrong width/height
 * - CAT_MISSING_SIZES: Size should exist but no metadata entry
 * - CAT_DEPRECATED_SIZES: Size in metadata but not in current theme
 * - CAT_MISSING_WEBP: WebP enabled but missing -webp entries
 * - CAT_POST_CONTENT_REF: Stale file references in post_content
 *
 * @package StaticQ
 * @since 3.0.0
 */

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

class SQMedia_File_State_Scanner_Phase1 {
    
    /**
     * Reference to parent scanner
     */
    private SQMedia_File_State_Scanner $scanner;
    
    /**
     * Constructor
     */
    public function __construct(SQMedia_File_State_Scanner $scanner) {
        $this->scanner = $scanner;
    }

    /**
     * Check if master file exists (local or bucket)
     * Used to determine if corruption issues are fixable or critical
     *
     * @param string $rel_path Relative path to file (from _wp_attached_file)
     * @param array $context Scan context with upload_dir
     * @return bool True if file exists somewhere
     */
    private function master_file_exists(string $rel_path, array $context): bool {
        if (empty($rel_path)) {
            return false;
        }

        // Check local first (fast)
        $local_path = $context['upload_dir']['basedir'] . '/' . $rel_path;
        if (file_exists($local_path)) {
            return true;
        }

        // Check bucket if cloud storage is configured
        $s3 = sqmedia()->get_storage_handler();
        if ($s3 && $s3->is_ready()) {
            $bucket_path = SQMedia_Settings::get_current_prefix() . '/' . $rel_path;
            if ($s3->file_exists($bucket_path)) {
                return true;
            }
        }

        return false;
    }

    // =========================================================================
    // SCANNING
    // =========================================================================
    
    /**
     * Scan a single attachment for Phase 1 issues
     * 
     * @param int $attachment_id Attachment ID to scan
     * @param array $context Scan context with settings
     * @return array Scan results with issues found and size counts
     */
    public function scan_attachment(int $attachment_id, array $context): array {
        $issues = [];
        $record = SQMedia_Attachment_Record::find($attachment_id);
        
        // Track file counts: expected vs found in metadata
        $sizes_expected = 1; // Master is always expected
        $sizes_found = 0;
        
        if (!$record) {
            return [
                'success' => false, 
                'error' => 'Record not found', 
                'issues' => [],
                'sizes_expected' => 0,
                'sizes_found' => 0,
            ];
        }
        
        // Non-images have no sizes or WebP to validate - they're always "clean" in Phase 1
        if (!$record->is_image()) {
            return [
                'success' => true,
                'issues' => [],
                'sizes_expected' => 1, // Just master
                'sizes_found' => 1,    // Master exists (checked in Phase 2)
                'is_image' => false,
            ];
        }
        
        // Non-resizable images (SVG, ICO) - no sizes or WebP needed, just master
        if (!$record->supports_resizing()) {
            return [
                'success' => true,
                'issues' => [],
                'sizes_expected' => 1, // Just master
                'sizes_found' => 1,    // Master exists (checked in Phase 2)
                'is_image' => true,
                'supports_resizing' => false,
            ];
        }
        
        // Get WordPress metadata
        $wp_metadata = $record->get_wp_metadata();
        $attached_file = get_post_meta($attachment_id, '_wp_attached_file', true);

        if (empty($wp_metadata)) {
            // No metadata - check if file exists to determine if fixable
            $file_exists = $this->master_file_exists($attached_file, $context);

            if ($file_exists) {
                // File exists - metadata can be rebuilt
                $this->scanner->store_issue($context['scan_id'], SQMedia_File_State_Scanner::CAT_CORRUPTED, $attachment_id, [
                    'reason' => 'no_metadata',
                ]);
                return [
                    'success' => true,
                    'issues' => [SQMedia_File_State_Scanner::CAT_CORRUPTED],
                    'sizes_expected' => $sizes_expected,
                    'sizes_found' => 0,
                ];
            } else {
                // File missing - cannot fix, mark as critical
                $this->scanner->store_issue($context['scan_id'], SQMedia_File_State_Scanner::CAT_CRITICAL_MISSING, $attachment_id, [
                    'file' => $attached_file ?: 'unknown',
                    'filename' => $attached_file ? basename($attached_file) : 'unknown',
                    'reason' => 'no_metadata_file_missing',
                ]);
                return [
                    'success' => true,
                    'issues' => [SQMedia_File_State_Scanner::CAT_CRITICAL_MISSING],
                    'sizes_expected' => $sizes_expected,
                    'sizes_found' => 0,
                ];
            }
        }
        
        // Master found in metadata
        $sizes_found = 1;
        
        // =====================================================================
        // MASTER FILE METADATA CHECKS
        // =====================================================================

        // Check 1: file path exists and matches _wp_attached_file
        $metadata_file = $wp_metadata['file'] ?? '';

        if (empty($metadata_file)) {
            $this->scanner->store_issue($context['scan_id'], SQMedia_File_State_Scanner::CAT_CORRUPTED, $attachment_id, [
                'reason' => 'missing_file_path',
            ]);
            $issues[] = SQMedia_File_State_Scanner::CAT_CORRUPTED;
        } elseif (!empty($attached_file) && $metadata_file !== $attached_file) {
            $this->scanner->store_issue($context['scan_id'], SQMedia_File_State_Scanner::CAT_CORRUPTED, $attachment_id, [
                'reason' => 'file_path_mismatch',
                'metadata_file' => $metadata_file,
                'attached_file' => $attached_file,
            ]);
            $issues[] = SQMedia_File_State_Scanner::CAT_CORRUPTED;
        } elseif (!$this->is_valid_image_path($metadata_file)) {
            $this->scanner->store_issue($context['scan_id'], SQMedia_File_State_Scanner::CAT_CORRUPTED, $attachment_id, [
                'reason' => 'invalid_file_path',
                'file' => $metadata_file,
            ]);
            $issues[] = SQMedia_File_State_Scanner::CAT_CORRUPTED;
        }

        // Check 2: width and height exist and > 0
        $master_width = $wp_metadata['width'] ?? 0;
        $master_height = $wp_metadata['height'] ?? 0;

        // Check 3: filesize exists and > 0
        $master_filesize = $wp_metadata['filesize'] ?? 0;

        // For issues that require file access to fix, check if file exists
        $needs_file_access = ($master_width <= 0 || $master_height <= 0 || $master_filesize <= 0);

        if ($needs_file_access) {
            $file_exists = $this->master_file_exists($attached_file, $context);

            if (!$file_exists) {
                // File missing - cannot fix ANY issues, mark as critical and return early
                $this->scanner->store_issue($context['scan_id'], SQMedia_File_State_Scanner::CAT_CRITICAL_MISSING, $attachment_id, [
                    'size' => 'master',
                    'file' => $attached_file ?: $metadata_file,
                    'filename' => basename($attached_file ?: $metadata_file),
                    'reason' => 'master_file_missing',
                ]);
                return [
                    'success' => true,
                    'issues' => [SQMedia_File_State_Scanner::CAT_CRITICAL_MISSING],
                    'sizes_expected' => $sizes_expected,
                    'sizes_found' => 0,
                ];
            }

            // File exists - log the specific corruption issues
            if ($master_width <= 0 || $master_height <= 0) {
                $this->scanner->store_issue($context['scan_id'], SQMedia_File_State_Scanner::CAT_CORRUPTED, $attachment_id, [
                    'reason' => 'invalid_dimensions',
                    'width' => $master_width,
                    'height' => $master_height,
                ]);
                $issues[] = SQMedia_File_State_Scanner::CAT_CORRUPTED;
            }

            if ($master_filesize <= 0) {
                $this->scanner->store_issue($context['scan_id'], SQMedia_File_State_Scanner::CAT_CORRUPTED, $attachment_id, [
                    'reason' => 'invalid_filesize',
                    'filesize' => $master_filesize,
                ]);
                $issues[] = SQMedia_File_State_Scanner::CAT_CORRUPTED;
            }
        }
        
        // =====================================================================
        // SIZE ENTRIES CHECKS
        // =====================================================================
        
        // Get current sizes from metadata
        $current_sizes = $wp_metadata['sizes'] ?? [];

        // Separate WebP and regular sizes — all -webp entries are now deprecated
        $image_sizes = [];
        foreach ($current_sizes as $size_name => $size_data) {
            if (str_ends_with($size_name, '-webp')) {
                // Any -webp entries in metadata are deprecated (WebP is now derived at runtime)
                $this->scanner->store_issue($context['scan_id'], SQMedia_File_State_Scanner::CAT_DEPRECATED_SIZES, $attachment_id, [
                    'size' => $size_name,
                ]);
                $issues[] = SQMedia_File_State_Scanner::CAT_DEPRECATED_SIZES;
            } else {
                $image_sizes[$size_name] = $size_data;
            }
        }

        // Check each enabled theme size and count expected/found
        $size_results = $this->check_sizes_with_counts($attachment_id, $context, $master_width, $master_height, $image_sizes);
        $issues = array_merge($issues, $size_results['issues']);
        $sizes_expected += $size_results['expected'];
        $sizes_found += $size_results['found'];

        // Check for deprecated sizes (in metadata but not in theme, or not needed for this master)
        $issues = array_merge($issues, $this->check_deprecated_sizes($attachment_id, $context, $image_sizes, $master_width, $master_height));

        // Check for stale file references in post_content
        $issues = array_merge($issues, $this->check_post_content_refs($attachment_id, $context, $wp_metadata, $issues));

        // Scale-down alignment check: master exceeds current big image threshold
        if (SQMedia_Settings::is_big_image_scaling_enabled() && $master_width > 0 && $master_height > 0) {
            $threshold_w = SQMedia_Settings::get_big_image_threshold_width();
            $threshold_h = SQMedia_Settings::get_big_image_threshold_height();

            if ($master_width > $threshold_w || $master_height > $threshold_h) {
                $this->scanner->store_issue($context['scan_id'], SQMedia_File_State_Scanner::CAT_ORIGINAL_POLICY, $attachment_id, [
                    'action' => 'scale_down',
                    'size' => 'master',
                    'master_width' => $master_width,
                    'master_height' => $master_height,
                    'threshold_width' => $threshold_w,
                    'threshold_height' => $threshold_h,
                    'has_original' => !empty($wp_metadata['original_image']),
                ]);
                $issues[] = SQMedia_File_State_Scanner::CAT_ORIGINAL_POLICY;
            }
        }

        return [
            'success' => true,
            'issues' => array_unique($issues),
            'sizes_expected' => $sizes_expected,
            'sizes_found' => $sizes_found,
        ];
    }
    
    /**
     * Validate that a file path looks like a valid image path
     * 
     * @param string $path The file path to validate
     * @return bool True if path appears valid
     */
    private function is_valid_image_path(string $path): bool {
        // Must not be empty
        if (empty($path)) {
            return false;
        }
        
        // Must have a valid image extension
        $valid_extensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'tiff', 'tif'];
        $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
        if (!in_array($ext, $valid_extensions, true)) {
            return false;
        }
        
        // Must have a filename (not just extension)
        $filename = pathinfo($path, PATHINFO_FILENAME);
        if (empty($filename)) {
            return false;
        }
        
        // Must not contain invalid characters
        if (preg_match('/[<>"|?*]/', $path)) {
            return false;
        }
        
        return true;
    }
    
    /**
     * Check enabled sizes against metadata and return counts
     */
    private function check_sizes_with_counts(int $attachment_id, array $context, int $master_width, int $master_height, array $image_sizes): array {
        $issues = [];
        $expected = 0;
        $found = 0;
        
        // Can't validate dimensions if master dimensions are unknown/corrupted
        $can_validate_dimensions = ($master_width > 0 && $master_height > 0);
        
        foreach ($context['enabled_sizes'] as $size_name => $size_config) {
            $crop = !empty($size_config['crop']);
            
            // Calculate expected dimensions
            $dims = $this->scanner->calculate_expected_dimensions(
                $master_width,
                $master_height,
                $size_config['width'] ?? 0,
                $size_config['height'] ?? 0,
                $crop
            );
            
            // Skip if master is smaller than this size
            if ($dims === null) {
                continue;
            }
            
            // This size is expected for this attachment
            $expected++;
            
            if (!isset($image_sizes[$size_name])) {
                // Size should exist but doesn't
                $this->scanner->store_issue($context['scan_id'], SQMedia_File_State_Scanner::CAT_MISSING_SIZES, $attachment_id, [
                    'size' => $size_name,
                    'expected_width' => $dims['width'],
                    'expected_height' => $dims['height'],
                ]);
                $issues[] = SQMedia_File_State_Scanner::CAT_MISSING_SIZES;
            } else {
                // Size exists - count it as found
                $found++;
                
                // Only validate dimensions if we know the master dimensions
                // When master is corrupted (0×0), we assume existing sizes are correct
                if ($can_validate_dimensions) {
                    $current = $image_sizes[$size_name];
                    $current_w = $current['width'] ?? 0;
                    $current_h = $current['height'] ?? 0;
                    $diff_w = abs($current_w - $dims['width']);
                    $diff_h = abs($current_h - $dims['height']);
                    $dims_wrong = ($diff_w > 1 || $diff_h > 1);

                    // Also check if filename has wrong dimensions embedded
                    // e.g. file says "image-400x0.jpg" but width/height say 400x267
                    $file_wrong = false;
                    $current_file = $current['file'] ?? '';
                    if (!$dims_wrong && $current_file && preg_match('/-(\d+)x(\d+)\./', $current_file, $m)) {
                        $file_w = (int) $m[1];
                        $file_h = (int) $m[2];
                        $file_wrong = (abs($file_w - $dims['width']) > 1 || abs($file_h - $dims['height']) > 1);
                    }

                    if ($dims_wrong || $file_wrong) {
                        $this->scanner->store_issue($context['scan_id'], SQMedia_File_State_Scanner::CAT_WRONG_DIMENSIONS, $attachment_id, [
                            'size' => $size_name,
                            'current_width' => $current_w,
                            'current_height' => $current_h,
                            'expected_width' => $dims['width'],
                            'expected_height' => $dims['height'],
                        ]);
                        $issues[] = SQMedia_File_State_Scanner::CAT_WRONG_DIMENSIONS;
                    }
                }
            }
        }
        
        return ['issues' => $issues, 'expected' => $expected, 'found' => $found];
    }
    
    /**
     * Check for deprecated sizes (in metadata but not in theme, or not needed for this master)
     */
    private function check_deprecated_sizes(int $attachment_id, array $context, array $image_sizes, int $master_width, int $master_height): array {
        $issues = [];
        $ignored_sizes = SQMedia_Settings::get_ignored_sizes();

        foreach ($image_sizes as $size_name => $size_data) {
            // Skip if this is a custom preserved size
            if (in_array($size_name, $ignored_sizes, true)) {
                continue;
            }

            if (!isset($context['enabled_sizes'][$size_name])) {
                // Size name not registered in theme at all
                $this->scanner->store_issue($context['scan_id'], SQMedia_File_State_Scanner::CAT_DEPRECATED_SIZES, $attachment_id, [
                    'size' => $size_name,
                ]);
                $issues[] = SQMedia_File_State_Scanner::CAT_DEPRECATED_SIZES;
            } elseif ($master_width > 0 && $master_height > 0) {
                // Size name is valid, but check if the master actually needs this size.
                // If calculate_expected_dimensions returns null, the master is at or below
                // the size target — WordPress wouldn't generate this size. Flag as deprecated
                // (catches legacy upscaled sizes and redundant same-as-master sizes).
                $size_config = $context['enabled_sizes'][$size_name];
                $dims = $this->scanner->calculate_expected_dimensions(
                    $master_width, $master_height,
                    $size_config['width'] ?? 0,
                    $size_config['height'] ?? 0,
                    !empty($size_config['crop'])
                );
                if ($dims === null) {
                    $this->scanner->store_issue($context['scan_id'], SQMedia_File_State_Scanner::CAT_DEPRECATED_SIZES, $attachment_id, [
                        'size' => $size_name,
                    ]);
                    $issues[] = SQMedia_File_State_Scanner::CAT_DEPRECATED_SIZES;
                }
            }
        }

        return $issues;
    }
    
    /**
     * Check for stale file references in post_content
     *
     * When wrong_dimensions or deprecated_sizes issues exist, the affected files
     * may be hardcoded in wp_posts.post_content. This detects those references
     * so repair can update them.
     */
    private function check_post_content_refs(int $attachment_id, array $context, array $wp_metadata, array $existing_issues): array {
        // Only relevant when files will change or disappear
        $has_wrong = in_array(SQMedia_File_State_Scanner::CAT_WRONG_DIMENSIONS, $existing_issues, true);
        $has_deprecated = in_array(SQMedia_File_State_Scanner::CAT_DEPRECATED_SIZES, $existing_issues, true);

        if (!$has_wrong && !$has_deprecated) {
            return [];
        }

        $dir = dirname($wp_metadata['file'] ?? '');
        if ($dir === '.') {
            $dir = '';
        }
        $sizes = $wp_metadata['sizes'] ?? [];

        // Collect affected scan items for this attachment to get size-level details
        $scan_items = $this->scanner->get_db()->get_attachment_scan_items($context['scan_id'], $attachment_id);

        // Build list of files that will change or disappear
        $affected_files = [];
        foreach ($scan_items as $item) {
            $details_arr = $item->details ? json_decode($item->details, true) : [];
            $items_list = $details_arr['items'] ?? [$details_arr];

            foreach ($items_list as $details) {
                $size_name = $details['size'] ?? null;
                if (!$size_name || !isset($sizes[$size_name])) {
                    continue;
                }

                $current_file = $sizes[$size_name]['file'] ?? '';
                if (empty($current_file)) {
                    continue;
                }

                $rel_path = ($dir !== '' ? $dir . '/' : '') . $current_file;

                if ($item->action_type === SQMedia_File_State_Scanner::CAT_WRONG_DIMENSIONS) {
                    $affected_files[] = [
                        'size' => $size_name,
                        'file' => $rel_path,
                        'old_width' => (int) ($details['current_width'] ?? 0),
                        'old_height' => (int) ($details['current_height'] ?? 0),
                        'source' => 'wrong_dimensions',
                        'replacement_size' => $size_name, // Same size, new dimensions
                    ];
                } elseif ($item->action_type === SQMedia_File_State_Scanner::CAT_DEPRECATED_SIZES) {
                    $current_w = (int) ($sizes[$size_name]['width'] ?? 0);
                    $current_h = (int) ($sizes[$size_name]['height'] ?? 0);

                    // Find closest larger enabled size as replacement
                    $replacement = 'full';
                    $best_area = PHP_INT_MAX;
                    $master_w = (int) ($wp_metadata['width'] ?? 0);
                    $master_h = (int) ($wp_metadata['height'] ?? 0);

                    foreach ($context['enabled_sizes'] as $es_name => $es_config) {
                        $dims = $this->scanner->calculate_expected_dimensions(
                            $master_w, $master_h,
                            $es_config['width'] ?? 0, $es_config['height'] ?? 0,
                            !empty($es_config['crop'])
                        );
                        if (!$dims) {
                            continue;
                        }
                        if ($dims['width'] >= $current_w && $dims['height'] >= $current_h) {
                            $area = $dims['width'] * $dims['height'];
                            if ($area < $best_area) {
                                $best_area = $area;
                                $replacement = $es_name;
                            }
                        }
                    }

                    $affected_files[] = [
                        'size' => $size_name,
                        'file' => $rel_path,
                        'old_width' => $current_w,
                        'old_height' => $current_h,
                        'source' => 'deprecated_sizes',
                        'replacement_size' => $replacement,
                    ];
                }
            }
        }

        if (empty($affected_files)) {
            return [];
        }

        // Build one SQL query with OR conditions for all affected files
        global $wpdb;
        $conditions = [];
        foreach ($affected_files as $af) {
            // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnquotedComplexPlaceholder
            $conditions[] = $wpdb->prepare('post_content LIKE %s', '%' . $wpdb->esc_like($af['file']) . '%');
        }

        // Check which files are actually referenced in post_content
        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
        $referenced = [];
        foreach ($affected_files as $i => $af) {
            $found = $wpdb->get_var(
                // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
                "SELECT ID FROM {$wpdb->posts} WHERE " . $conditions[$i] . ' LIMIT 1'
            );
            if ($found) {
                $referenced[] = $af;
            }
        }
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared

        if (empty($referenced)) {
            return [];
        }

        // Store each referenced item individually (add_scan_item handles aggregation)
        foreach ($referenced as $ref) {
            $this->scanner->store_issue($context['scan_id'], SQMedia_File_State_Scanner::CAT_POST_CONTENT_REF, $attachment_id, $ref);
        }

        return [SQMedia_File_State_Scanner::CAT_POST_CONTENT_REF];
    }

    // =========================================================================
    // TARGET SIZE COMPUTATION (for unified scan)
    // =========================================================================

    /**
     * Compute what sizes SHOULD exist based on current settings
     *
     * Bridges Phase 1 output to Phase 2 input: returns expected filenames
     * and dimensions so Phase 2 can check file existence against the target
     * state rather than current metadata.
     *
     * @param int $attachment_id Attachment ID
     * @param array $context Scan context with enabled_sizes
     * @return array [size_name => ['file' => ..., 'width' => ..., 'height' => ...], ...]
     *               Empty array for non-images, non-resizable, or corrupted (unknown dimensions)
     */
    public function compute_target_sizes(int $attachment_id, array $context): array {
        $record = SQMedia_Attachment_Record::find($attachment_id);

        if (!$record) {
            return [];
        }

        // Non-images (PDFs, etc.): return existing metadata sizes (preview thumbnails)
        if (!$record->is_image() || !$record->supports_resizing()) {
            $wp_metadata = $record->get_wp_metadata();
            $sizes = $wp_metadata['sizes'] ?? [];
            $target_sizes = [];
            foreach ($sizes as $size_name => $size_data) {
                if (!empty($size_data['file'])) {
                    $target_sizes[$size_name] = [
                        'file' => $size_data['file'],
                        'width' => $size_data['width'] ?? 0,
                        'height' => $size_data['height'] ?? 0,
                    ];
                }
            }
            return $target_sizes;
        }

        $wp_metadata = $record->get_wp_metadata();

        if (empty($wp_metadata)) {
            return [];
        }

        $master_width = $wp_metadata['width'] ?? 0;
        $master_height = $wp_metadata['height'] ?? 0;

        // Can't compute target sizes without master dimensions
        if ($master_width <= 0 || $master_height <= 0) {
            return [];
        }

        $master_file = get_post_meta($attachment_id, '_wp_attached_file', true);

        // Use original_image basename if present (for scaled images)
        if (!empty($wp_metadata['original_image'])) {
            $file_basename = pathinfo($wp_metadata['original_image'], PATHINFO_FILENAME);
        } else {
            $file_basename = pathinfo(basename($master_file), PATHINFO_FILENAME);
        }
        $file_ext = pathinfo(basename($master_file), PATHINFO_EXTENSION);

        $target_sizes = [];
        $metadata_sizes = $wp_metadata['sizes'] ?? [];
        $ignored_sizes = SQMedia_Settings::get_ignored_sizes();

        foreach ($context['enabled_sizes'] as $size_name => $size_config) {
            // Skip sizes the user marked as ignored in scanner settings
            if (in_array($size_name, $ignored_sizes, true)) {
                continue;
            }

            $crop = !empty($size_config['crop']);

            $dims = $this->scanner->calculate_expected_dimensions(
                $master_width,
                $master_height,
                $size_config['width'] ?? 0,
                $size_config['height'] ?? 0,
                $crop
            );

            // Skip if master is smaller than this size
            if ($dims === null) {
                continue;
            }

            // If this size exists in metadata with matching dimensions, use the metadata filename.
            // WordPress may use a different filename due to collision avoidance (e.g., image-1-150x150.jpg)
            $meta = $metadata_sizes[$size_name] ?? null;
            $use_meta_file = false;
            if ($meta
                && !empty($meta['file'])
                && abs(($meta['width'] ?? 0) - $dims['width']) <= 1
                && abs(($meta['height'] ?? 0) - $dims['height']) <= 1
            ) {
                // Also verify the filename's embedded dimensions match expected dims.
                // Old systems may have stored wrong filenames like "image-400x0.jpg"
                // with correct width/height values.
                $use_meta_file = true;
                if (preg_match('/-(\d+)x(\d+)\./', $meta['file'], $m)) {
                    $file_w = (int) $m[1];
                    $file_h = (int) $m[2];
                    if (abs($file_w - $dims['width']) > 1 || abs($file_h - $dims['height']) > 1) {
                        $use_meta_file = false;
                    }
                }
            }

            if ($use_meta_file) {
                $file = $meta['file'];
                // Use metadata dimensions since those match the actual generated file
                $actual_w = (int) ($meta['width'] ?? $dims['width']);
                $actual_h = (int) ($meta['height'] ?? $dims['height']);
            } else {
                $file = "{$file_basename}-{$dims['width']}x{$dims['height']}.{$file_ext}";
                $actual_w = $dims['width'];
                $actual_h = $dims['height'];
            }

            $target_sizes[$size_name] = [
                'file' => $file,
                'width' => $actual_w,
                'height' => $actual_h,
            ];
        }

        return $target_sizes;
    }

    // =========================================================================
    // FIXING
    // =========================================================================
    
    /**
     * Fix all Phase 1 issues from a scan
     */
    public function fix_all(string $scan_id, object $scan): array {
        $db = $this->scanner->get_db();
        
        // Get all unique attachment IDs with issues
        global $wpdb;
        $table = SQMedia_Database::get_table(SQMedia_Database::TABLE_SCAN_ITEMS);
        
        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        $attachment_ids = $wpdb->get_col($wpdb->prepare(
            "SELECT DISTINCT attachment_id FROM {$table} WHERE scan_id = %s ORDER BY attachment_id ASC",
            $scan_id
        ));
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter

        if (empty($attachment_ids)) {
            $db->update_scan($scan_id, ['status' => 'completed']);
            return ['success' => true, 'message' => __('No issues to fix.', 'staticq-media'), 'fixed' => 0, 'total' => 0, 'failed' => 0];
        }
        
        $total = count($attachment_ids);
        $fixed = 0;
        $failed = 0;
        $errors = [];
        
        foreach ($attachment_ids as $attachment_id) {
            $result = $this->fix_attachment((int) $attachment_id, $scan_id);
            
            if ($result['success']) {
                $fixed++;
            } else {
                $failed++;
                if (!empty($result['error'])) {
                    $errors[] = sprintf('#%d: %s', $attachment_id, $result['error']);
                }
            }
        }
        
        // Update scan results
        $results = $scan->results ?? [];
        $results['fix_completed'] = [
            'total' => $total,
            'fixed' => $fixed,
            'failed' => $failed,
        ];
        
        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        $remaining = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$table} WHERE scan_id = %s",
            $scan_id
        ));
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        
        $db->update_scan($scan_id, [
            'status' => 'completed',
            'results' => $results,
        ]);
        
        return [
            'success' => true,
            'total' => $total,
            'fixed' => $fixed,
            'failed' => $failed,
            'errors' => array_slice($errors, 0, 10),
            'remaining' => $remaining,
        ];
    }
    
    /**
     * Fix a single attachment's Phase 1 issues
     */
    public function fix_attachment(int $attachment_id, string $scan_id): array {
        global $wpdb;
        $db = $this->scanner->get_db();
        $record = SQMedia_Attachment_Record::find($attachment_id);
        
        if (!$record) {
            return ['success' => false, 'fixed' => 0, 'errors' => [__('Attachment not found.', 'staticq-media')]];
        }

        // Get issues for this attachment
        $issues = $db->get_attachment_scan_items($scan_id, $attachment_id);

        if (empty($issues)) {
            // Issues already resolved between scan and fix — nothing to do
            return ['success' => true, 'fixed' => 0, 'errors' => []];
        }
        
        // Sort issues by priority
        $priority = [
            SQMedia_File_State_Scanner::CAT_CORRUPTED => 1,
            SQMedia_File_State_Scanner::CAT_MISSING_SIZES => 2,
            SQMedia_File_State_Scanner::CAT_WRONG_DIMENSIONS => 3,
            SQMedia_File_State_Scanner::CAT_DEPRECATED_SIZES => 4,
            SQMedia_File_State_Scanner::CAT_MISSING_WEBP => 5,
            SQMedia_File_State_Scanner::CAT_POST_CONTENT_REF => 6,
        ];
        
        usort($issues, function($a, $b) use ($priority) {
            $pa = $priority[$a->action_type] ?? 99;
            $pb = $priority[$b->action_type] ?? 99;
            return $pa - $pb;
        });
        
        // Get current metadata
        $metadata = get_post_meta($attachment_id, '_wp_attachment_metadata', true);
        if (!is_array($metadata)) {
            $metadata = [];
        }
        
        if (!isset($metadata['sizes'])) {
            $metadata['sizes'] = [];
        }
        
        $master_file = get_post_meta($attachment_id, '_wp_attached_file', true);
        
        // Use original_image basename if present (for scaled images)
        // This ensures subsizes match WordPress's naming convention where subsizes
        // are named after the original file, not the -scaled version
        if (!empty($metadata['original_image'])) {
            $file_basename = pathinfo($metadata['original_image'], PATHINFO_FILENAME);
        } else {
            $file_basename = pathinfo(basename($master_file), PATHINFO_FILENAME);
        }
        $file_ext = pathinfo(basename($master_file), PATHINFO_EXTENSION);
        
        $master_width = $metadata['width'] ?? 0;
        $master_height = $metadata['height'] ?? 0;
        $master_filesize = $metadata['filesize'] ?? 0;
        $temp_master_path = null; // Track if we downloaded master temporarily
        $upload_dir = wp_upload_dir();
        $local_path = null;
        $file_accessed = false; // Track if we've accessed the actual file
        
        // =====================================================================
        // FIX FILE PATH FIRST (needed for subsequent fixes)
        // =====================================================================
        
        // Ensure metadata['file'] matches _wp_attached_file
        if (!empty($master_file)) {
            $metadata['file'] = $master_file;
        }
        
        // =====================================================================
        // GET FILE ACCESS IF NEEDED (for dimensions or filesize)
        // =====================================================================
        
        // Determine if we need to access the actual file
        $needs_dimensions = ($master_width <= 0 || $master_height <= 0);
        $needs_filesize = ($master_filesize <= 0);
        
        if ($needs_dimensions || $needs_filesize) {
            $local_path = $upload_dir['basedir'] . '/' . $master_file;
            
            if (file_exists($local_path)) {
                $file_accessed = true;
                
                // Read dimensions if needed
                if ($needs_dimensions) {
                    $image_size = wp_getimagesize($local_path);
                    if ($image_size) {
                        $master_width = $image_size[0];
                        $master_height = $image_size[1];
                        $metadata['width'] = $master_width;
                        $metadata['height'] = $master_height;
                    }
                }
                
                // Read filesize if needed
                if ($needs_filesize) {
                    $actual_filesize = filesize($local_path);
                    if ($actual_filesize > 0) {
                        $master_filesize = $actual_filesize;
                        $metadata['filesize'] = $master_filesize;
                    }
                }
            } else {
                // Try to download from bucket temporarily
                $s3 = sqmedia()->get_storage_handler();
                if ($s3 && $s3->is_ready()) {
                    $bucket_path = SQMedia_Settings::get_current_prefix() . '/' . $master_file;
                    $temp_master_path = $upload_dir['basedir'] . '/staticq-temp-' . $attachment_id . '-' . basename($master_file);
                    
                    if ($s3->download_file($bucket_path, $temp_master_path)) {
                        $file_accessed = true;
                        $local_path = $temp_master_path;
                        
                        // Read dimensions if needed
                        if ($needs_dimensions) {
                            $image_size = wp_getimagesize($temp_master_path);
                            if ($image_size) {
                                $master_width = $image_size[0];
                                $master_height = $image_size[1];
                                $metadata['width'] = $master_width;
                                $metadata['height'] = $master_height;
                            }
                        }
                        
                        // Read filesize if needed
                        if ($needs_filesize) {
                            $actual_filesize = filesize($temp_master_path);
                            if ($actual_filesize > 0) {
                                $master_filesize = $actual_filesize;
                                $metadata['filesize'] = $master_filesize;
                            }
                        }
                        
                        // Clean up temp file immediately if not needed further
                        // Note: keeping for now in case CAT_MISSING_SIZES needs regeneration
                    }
                }
            }
        }
        
        $fixed_count = 0;
        $errors = [];
        $fixed_action_types = []; // Track which action types had successful fixes
        $enabled_sizes = SQMedia_Settings::get_enabled_sizes();
        
        foreach ($issues as $issue) {
            $action_type = $issue->action_type;
            $raw_details = $issue->details ? json_decode($issue->details, true) : [];
            
            // Handle new aggregated format (items array) or old single-item format
            $items_list = $raw_details['items'] ?? [$raw_details];
            
            foreach ($items_list as $details) {
                $size_name = $details['size'] ?? null;
            
                try {
                    switch ($action_type) {
                        case SQMedia_File_State_Scanner::CAT_CORRUPTED:
                            // Handle different corruption reasons
                            $reason = $details['reason'] ?? 'unknown';
                            
                            switch ($reason) {
                                case 'no_metadata':
                                    // If we have dimensions and filesize now, metadata was rebuilt
                                    if ($master_width > 0 && $master_height > 0 && $master_filesize > 0) {
                                        $fixed_count++;
                                        $fixed_action_types[$action_type] = true;
                                    } else {
                                        $errors[] = __('Could not rebuild metadata from master file.', 'staticq-media');
                                    }
                                    break;

                                case 'missing_file_path':
                                case 'file_path_mismatch':
                                case 'invalid_file_path':
                                    // File path was already fixed above (metadata['file'] = master_file)
                                    if (!empty($metadata['file'])) {
                                        $fixed_count++;
                                        $fixed_action_types[$action_type] = true;
                                    } else {
                                        $errors[] = __('Could not fix file path - _wp_attached_file is empty.', 'staticq-media');
                                    }
                                    break;

                                case 'invalid_dimensions':
                                    // Dimensions were fixed above if possible
                                    if ($master_width > 0 && $master_height > 0) {
                                        $fixed_count++;
                                        $fixed_action_types[$action_type] = true;
                                    } else {
                                        $errors[] = $file_accessed
                                            ? __('File appears to be corrupt or not a valid image — could not read dimensions.', 'staticq-media')
                                            : __('Could not download master file to read dimensions.', 'staticq-media');
                                    }
                                    break;

                                case 'invalid_filesize':
                                    // Filesize was fixed above if possible
                                    if ($master_filesize > 0) {
                                        $fixed_count++;
                                        $fixed_action_types[$action_type] = true;
                                    } else {
                                        $errors[] = __('Could not determine master file size.', 'staticq-media');
                                    }
                                    break;

                                default:
                                    // Unknown reason - try generic validation
                                    if ($master_width > 0 && $master_height > 0) {
                                        $fixed_count++;
                                        $fixed_action_types[$action_type] = true;
                                    } else {
                                        /* translators: %s: corruption reason string */
                                        $errors[] = sprintf(__('Unknown corruption reason: %s', 'staticq-media'), $reason);
                                    }
                            }
                            break;
                        
                    case SQMedia_File_State_Scanner::CAT_MISSING_SIZES:
                        if ($size_name && isset($enabled_sizes[$size_name])) {
                            $size_config = $enabled_sizes[$size_name];
                            $crop = !empty($size_config['crop']);
                            $dims = $this->scanner->calculate_expected_dimensions(
                                $master_width, $master_height,
                                $size_config['width'] ?? 0,
                                $size_config['height'] ?? 0,
                                $crop
                            );

                            if ($dims) {
                                $size_filename = "{$file_basename}-{$dims['width']}x{$dims['height']}.{$file_ext}";

                                $metadata['sizes'][$size_name] = [
                                    'file' => $size_filename,
                                    'width' => $dims['width'],
                                    'height' => $dims['height'],
                                    'mime-type' => $metadata['mime-type'] ?? 'image/jpeg',
                                ];
                                $fixed_count++;
                                $fixed_action_types[$action_type] = true;
                            }
                        }
                        break;
                        
                    case SQMedia_File_State_Scanner::CAT_WRONG_DIMENSIONS:
                        if ($size_name && isset($metadata['sizes'][$size_name]) && isset($enabled_sizes[$size_name])) {
                            $size_config = $enabled_sizes[$size_name];
                            $crop = !empty($size_config['crop']);
                            $dims = $this->scanner->calculate_expected_dimensions(
                                $master_width, $master_height,
                                $size_config['width'] ?? 0,
                                $size_config['height'] ?? 0,
                                $crop
                            );

                            if ($dims) {
                                $size_filename = "{$file_basename}-{$dims['width']}x{$dims['height']}.{$file_ext}";

                                $metadata['sizes'][$size_name]['file'] = $size_filename;
                                $metadata['sizes'][$size_name]['width'] = $dims['width'];
                                $metadata['sizes'][$size_name]['height'] = $dims['height'];
                                $fixed_count++;
                                $fixed_action_types[$action_type] = true;
                            }
                        }
                        break;
                        
                    case SQMedia_File_State_Scanner::CAT_DEPRECATED_SIZES:
                        if ($size_name && isset($metadata['sizes'][$size_name])) {
                            unset($metadata['sizes'][$size_name]);
                            $fixed_count++;
                            $fixed_action_types[$action_type] = true;
                        }
                        break;

                    case SQMedia_File_State_Scanner::CAT_POST_CONTENT_REF:
                        $old_path = $details['file'] ?? '';
                        $source = $details['source'] ?? '';
                        $ref_size = $details['size'] ?? '';
                        $dir = dirname($metadata['file'] ?? '');
                        if ($dir === '.') {
                            $dir = '';
                        }

                        if (empty($old_path)) {
                            break;
                        }

                        $new_file = '';
                        $new_slug = $ref_size;

                        if ($source === 'wrong_dimensions' && isset($metadata['sizes'][$ref_size])) {
                            // Size still exists with new dimensions
                            $new_file = $metadata['sizes'][$ref_size]['file'];
                        } elseif ($source === 'deprecated_sizes') {
                            // Size was removed — find closest larger size
                            $target_w = (int) ($details['old_width'] ?? 0);
                            $target_h = (int) ($details['old_height'] ?? 0);
                            $best_match = null;
                            $best_area = PHP_INT_MAX;

                            foreach ($metadata['sizes'] as $s_name => $s_data) {
                                $sw = (int) ($s_data['width'] ?? 0);
                                $sh = (int) ($s_data['height'] ?? 0);
                                if ($sw >= $target_w && $sh >= $target_h) {
                                    $area = $sw * $sh;
                                    if ($area < $best_area) {
                                        $best_area = $area;
                                        $best_match = $s_name;
                                    }
                                }
                            }

                            if ($best_match) {
                                $new_file = $metadata['sizes'][$best_match]['file'];
                                $new_slug = $best_match;
                            } else {
                                // Fallback to master
                                $new_file = basename($metadata['file'] ?? $master_file);
                                $new_slug = 'full';
                            }
                        }

                        if (empty($new_file)) {
                            break;
                        }

                        $new_path = ($dir !== '' ? $dir . '/' : '') . $new_file;

                        // Fetch affected posts and update content
                        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                        $affected_posts = $wpdb->get_results(
                            $wpdb->prepare(
                                "SELECT ID, post_content FROM {$wpdb->posts} WHERE post_content LIKE %s",
                                '%' . $wpdb->esc_like($old_path) . '%'
                            )
                        );
                        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

                        foreach ($affected_posts as $post) {
                            $content = $post->post_content;

                            // Replace file path (works for both classic and block editor)
                            $content = str_replace($old_path, $new_path, $content);

                            // Update sizeSlug in Gutenberg block comments if slug changed
                            if ($new_slug !== $ref_size) {
                                $content = preg_replace(
                                    '/("id":' . $attachment_id . '[^}]*"sizeSlug":")' . preg_quote($ref_size, '/') . '(")/U',
                                    '${1}' . $new_slug . '${2}',
                                    $content
                                );
                            }

                            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                            $wpdb->update($wpdb->posts, ['post_content' => $content], ['ID' => $post->ID]);
                            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                            clean_post_cache($post->ID);
                        }

                        $fixed_count++;
                        $fixed_action_types[$action_type] = true;
                        break;

                }

                } catch (Exception $e) {
                    $errors[] = sprintf('%s: %s', $size_name ?? $action_type, $e->getMessage());
                }
            } // end foreach items_list
        } // end foreach issues
        
        // Clean up temp file if still exists
        if ($temp_master_path && file_exists($temp_master_path)) {
            wp_delete_file($temp_master_path);
        }
        
        // Save updated metadata
        if ($fixed_count > 0) {
            $updated = update_post_meta($attachment_id, '_wp_attachment_metadata', $metadata);
            if (!$updated) {
                $check = get_post_meta($attachment_id, '_wp_attachment_metadata', true);
                if ($check !== $metadata) {
                    return ['success' => false, 'fixed' => 0, 'errors' => [__('Failed to save metadata.', 'staticq-media')]];
                }
            }
            
            // Remove scan items for action types that had successful fixes
            foreach (array_keys($fixed_action_types) as $action_type) {
                $db->remove_scan_item($scan_id, $action_type, $attachment_id);
            }
        }
        
        return [
            'success' => empty($errors),
            'fixed' => $fixed_count,
            'errors' => $errors,
            'attachment_id' => $attachment_id,
        ];
    }
}