<?php
/**
 * Post Content Analyzer
 *
 * Stateless helper that extracts and validates image references from a single
 * post's content. Separated from the scanner for clarity and testability.
 *
 * @package StaticQ
 * @since 3.3.0
 */

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

class SQMedia_Post_Content_Analyzer {

    /**
     * Analyze a post's content for image issues
     *
     * @param int    $post_id The post ID
     * @param string $content The post_content
     * @param array  $context Shared context from the scanner
     * @return array Array of issues found: [ ['category' => '...', 'details' => [...]], ... ]
     */
    public function analyze(int $post_id, string $content, array $context): array {
        $issues = [];

        // Extract all image references from the content
        $references = $this->extract_image_references($content);

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

        foreach ($references as $ref) {
            $ref_issues = $this->validate_reference($ref, $post_id, $context);
            foreach ($ref_issues as $issue) {
                $issues[] = $issue;
            }
        }

        return $issues;
    }

    // =========================================================================
    // EXTRACTION
    // =========================================================================

    /**
     * Extract all image references from post content
     *
     * Parses <img> src/srcset, <a href> wrappers, and Gutenberg block comments.
     *
     * @return array Array of image reference arrays
     */
    private function extract_image_references(string $content): array {
        $references = [];
        $seen_urls = [];

        // Build a map of Gutenberg block IDs and sizeSlug for cross-referencing
        $block_map = $this->extract_gutenberg_blocks($content);

        // Extract <img> tags
        if (preg_match_all('/<img\b[^>]+>/i', $content, $img_matches)) {
            foreach ($img_matches[0] as $img_tag) {
                $src = $this->get_attribute($img_tag, 'src');
                if (empty($src)) {
                    continue;
                }

                // Get wp-image-{ID} class
                $wp_image_id = null;
                $class = $this->get_attribute($img_tag, 'class');
                if ($class && preg_match('/wp-image-(\d+)/', $class, $m)) {
                    $wp_image_id = (int) $m[1];
                }

                // Get Gutenberg block data for this image (if available)
                $size_slug = null;
                $block_id = null;
                if ($wp_image_id && isset($block_map[$wp_image_id])) {
                    $size_slug = $block_map[$wp_image_id]['sizeSlug'] ?? null;
                    $block_id = $wp_image_id;
                }

                // Main src
                if (!isset($seen_urls[$src])) {
                    $seen_urls[$src] = true;
                    $references[] = [
                        'url' => $src,
                        'wp_image_id' => $wp_image_id,
                        'block_id' => $block_id,
                        'size_slug' => $size_slug,
                        'source' => 'img_src',
                    ];
                }

                // srcset URLs
                $srcset = $this->get_attribute($img_tag, 'srcset');
                if ($srcset) {
                    $srcset_urls = $this->parse_srcset($srcset);
                    foreach ($srcset_urls as $srcset_url) {
                        if (!isset($seen_urls[$srcset_url])) {
                            $seen_urls[$srcset_url] = true;
                            $references[] = [
                                'url' => $srcset_url,
                                'wp_image_id' => $wp_image_id,
                                'block_id' => $block_id,
                                'size_slug' => null, // srcset entries don't have a specific slug
                                'source' => 'img_srcset',
                            ];
                        }
                    }
                }
            }
        }

        // Extract <a href> that wrap images and point to upload files
        if (preg_match_all('#<a\b[^>]+href=["\']([^"\']+)["\'][^>]*>\s*<img\b#i', $content, $a_matches)) {
            foreach ($a_matches[1] as $href) {
                if (strpos($href, '/wp-content/uploads/') !== false && !isset($seen_urls[$href])) {
                    $seen_urls[$href] = true;
                    $references[] = [
                        'url' => $href,
                        'wp_image_id' => null,
                        'block_id' => null,
                        'size_slug' => null,
                        'source' => 'a_href',
                    ];
                }
            }
        }

        return $references;
    }

    /**
     * Extract Gutenberg block comment data
     *
     * @return array Map of attachment ID => ['sizeSlug' => '...']
     */
    private function extract_gutenberg_blocks(string $content): array {
        $map = [];

        if (preg_match_all('/<!-- wp:image\s+(\{[^}]+\})\s*-->/s', $content, $matches)) {
            foreach ($matches[1] as $json_str) {
                $data = json_decode($json_str, true);
                if (!$data || empty($data['id'])) {
                    continue;
                }
                $map[(int) $data['id']] = [
                    'sizeSlug' => $data['sizeSlug'] ?? null,
                ];
            }
        }

        return $map;
    }

    /**
     * Get an HTML attribute value from a tag string
     */
    private function get_attribute(string $tag, string $attr): ?string {
        if (preg_match('/\b' . preg_quote($attr, '/') . '=["\']([^"\']*)["\']/', $tag, $m)) {
            return $m[1];
        }
        return null;
    }

    /**
     * Parse srcset attribute into individual URLs
     */
    private function parse_srcset(string $srcset): array {
        $urls = [];
        $entries = explode(',', $srcset);
        foreach ($entries as $entry) {
            $parts = preg_split('/\s+/', trim($entry));
            if (!empty($parts[0])) {
                $urls[] = $parts[0];
            }
        }
        return $urls;
    }

    // =========================================================================
    // VALIDATION
    // =========================================================================

    /**
     * Validate a single image reference
     *
     * @return array Issues found for this reference
     */
    private function validate_reference(array $ref, int $post_id, array $context): array {
        $url = $ref['url'];

        // Skip data URIs and empty URLs
        if (empty($url) || strpos($url, 'data:') === 0) {
            return [];
        }

        // Skip URLs that don't look like images
        if (!$this->is_image_url($url)) {
            return [];
        }

        // Determine if this URL belongs to our site
        $classification = $this->classify_url($url, $context);

        if ($classification === 'external') {
            return []; // Genuinely external image, skip
        }

        // For domain issues, flag and convert to local for further checks
        $local_url = $url;
        if ($classification === 'cdn_domain' || $classification === 'foreign_domain') {
            $local_url = $this->convert_to_local($url, $context);
            return [[
                'category' => SQMedia_Post_Content_Scanner::CAT_WRONG_DOMAIN,
                'details' => [
                    'url' => $url,
                    'local_url' => $local_url,
                    'domain' => wp_parse_url($url, PHP_URL_HOST),
                    'source' => $ref['source'],
                ],
            ]];
        }

        // Resolve attachment ID
        $attachment_id = $this->resolve_attachment_id($ref, $local_url, $context);

        if (!$attachment_id) {
            // Could not resolve to any attachment
            return [[
                'category' => SQMedia_Post_Content_Scanner::CAT_BROKEN_URL,
                'details' => [
                    'url' => $url,
                    'source' => $ref['source'],
                ],
            ]];
        }

        // Check if the attachment still exists
        $post_status = get_post_status($attachment_id);
        if (!$post_status || get_post_type($attachment_id) !== 'attachment') {
            return [[
                'category' => SQMedia_Post_Content_Scanner::CAT_DELETED_ATTACHMENT,
                'details' => [
                    'url' => $url,
                    'attachment_id' => $attachment_id,
                    'source' => $ref['source'],
                ],
            ]];
        }

        // Check if the referenced file/size is still valid in metadata
        return $this->validate_file_in_metadata($ref, $local_url, $attachment_id, $context);
    }

    /**
     * Classify a URL: 'local', 'cdn_domain', 'foreign_domain', or 'external'
     */
    private function classify_url(string $url, array $context): string {
        $host = wp_parse_url($url, PHP_URL_HOST);
        $path = wp_parse_url($url, PHP_URL_PATH);

        if (!$host || !$path) {
            // Relative URL — treat as local
            return 'local';
        }

        // Check if it's the site's domain
        if (strcasecmp($host, $context['site_host']) === 0) {
            return 'local';
        }

        // Check if it's the configured CDN domain
        if (!empty($context['cdn_host']) && strcasecmp($host, $context['cdn_host']) === 0) {
            return 'cdn_domain';
        }

        // Check if it has an uploads-like path (this site but different domain)
        if (strpos($path, '/wp-content/uploads/') !== false) {
            return 'foreign_domain';
        }

        // Check for date-structured paths that look like WP uploads
        if (preg_match('#/\d{4}/\d{2}/#', $path)) {
            // Could be a foreign CDN with rewritten paths — check more
            $bucket_prefix = $context['bucket_prefix'] ?? '';
            if (!empty($bucket_prefix) && strpos($path, '/' . $bucket_prefix . '/') !== false) {
                return 'foreign_domain';
            }
        }

        return 'external';
    }

    /**
     * Convert a non-local URL to a local WordPress URL
     */
    private function convert_to_local(string $url, array $context): string {
        $path = wp_parse_url($url, PHP_URL_PATH);

        // Try to extract the relative path from /wp-content/uploads/
        $marker = '/wp-content/uploads/';
        $pos = strpos($path, $marker);
        if ($pos !== false) {
            $relative = substr($path, $pos + strlen($marker));
            return $context['upload_baseurl'] . '/' . $relative;
        }

        // Try bucket prefix path
        $bucket_prefix = $context['bucket_prefix'] ?? '';
        if (!empty($bucket_prefix)) {
            $prefix_marker = '/' . $bucket_prefix . '/';
            $pos = strpos($path, $prefix_marker);
            if ($pos !== false) {
                $relative = substr($path, $pos + strlen($prefix_marker));
                return $context['upload_baseurl'] . '/' . $relative;
            }
        }

        // Fallback: just swap the domain
        $local_host = wp_parse_url($context['upload_baseurl'], PHP_URL_HOST);
        $current_host = wp_parse_url($url, PHP_URL_HOST);
        if ($current_host && $local_host) {
            $scheme = wp_parse_url($context['upload_baseurl'], PHP_URL_SCHEME) ?: 'https';
            return str_replace($current_host, $local_host, preg_replace('#^https?://#', $scheme . '://', $url));
        }

        return $url;
    }

    /**
     * Resolve attachment ID from an image reference
     */
    private function resolve_attachment_id(array $ref, string $local_url, array $context): int {
        // 1. Use wp-image-{ID} class if available
        if (!empty($ref['wp_image_id'])) {
            return $ref['wp_image_id'];
        }

        // 2. Strip -WIDTHxHEIGHT suffix and try first (most URLs with dimensions are
        //    sub-sizes, so the stripped version is more likely to match _wp_attached_file)
        $stripped_url = preg_replace('/-\d+x\d+(\.[a-z]+)$/i', '$1', $local_url);
        $id = attachment_url_to_postid($stripped_url);
        if ($id > 0) {
            return $id;
        }

        // 3. Try raw URL (covers files literally named with dimensions, or URLs without them)
        if ($stripped_url !== $local_url) {
            $id = attachment_url_to_postid($local_url);
            if ($id > 0) {
                return $id;
            }
        }

        // 4. Fallback: search serialized _wp_attachment_metadata
        // Uses YYYY/MM path check to prevent cross-folder false matches.
        // Uses " prefix/suffix from serialized format to ensure exact field value match.
        return $this->lookup_by_metadata($local_url, $context);
    }

    /**
     * Search _wp_attachment_metadata for a filename (sub-sizes and original_image)
     *
     * Searches serialized metadata for the filename as an exact field value,
     * with YYYY/MM path verification to prevent cross-folder false positives.
     *
     * 4a. Try raw filename (finds sub-sizes stored in sizes.*.file)
     * 4b. Try dimension-stripped filename (finds original_image)
     */
    private function lookup_by_metadata(string $local_url, array $context): int {
        global $wpdb;

        $upload_baseurl = $context['upload_baseurl'];
        if (strpos($local_url, $upload_baseurl) !== 0) {
            return 0;
        }

        $relative = substr($local_url, strlen($upload_baseurl) + 1);
        $dir = dirname($relative); // e.g., "2024/05"
        $basename = wp_basename($relative); // e.g., "653087-1600x0.jpg"

        if ($dir === '.' || empty($dir)) {
            return 0; // No YYYY/MM path — can't safely disambiguate
        }

        // 4a. Search for the raw filename in metadata (finds sub-sizes)
        $id = $this->search_metadata_for_filename($basename, $dir);
        if ($id > 0) {
            return $id;
        }

        // 4b. Strip dimensions and search again (finds original_image)
        $stripped_basename = preg_replace('/-\d+x\d+(\.[a-z]+)$/i', '$1', $basename);
        if ($stripped_basename !== $basename) {
            $id = $this->search_metadata_for_filename($stripped_basename, $dir);
            if ($id > 0) {
                return $id;
            }
        }

        return 0;
    }

    /**
     * Search _wp_attachment_metadata for a specific filename in a specific YYYY/MM dir
     *
     * The " quotes in serialized PHP data ensure we match exact field values,
     * not substrings inside longer strings (e.g., captions).
     */
    private function search_metadata_for_filename(string $filename, string $dir): int {
        global $wpdb;

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $id = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT post_id FROM {$wpdb->postmeta}
             WHERE meta_key = '_wp_attachment_metadata'
             AND meta_value LIKE %s
             AND meta_value LIKE %s
             LIMIT 1",
            '%' . $wpdb->esc_like('"' . $filename . '"') . '%',
            '%' . $wpdb->esc_like($dir) . '%'
        ));

        return max(0, $id);
    }

    /**
     * Validate that the referenced file exists in the attachment's metadata
     *
     * @return array Issues found (empty if file is valid)
     */
    private function validate_file_in_metadata(array $ref, string $local_url, int $attachment_id, array $context): array {
        $metadata = wp_get_attachment_metadata($attachment_id);
        if (!is_array($metadata) || empty($metadata['file'])) {
            return []; // No metadata — can't validate, skip
        }

        // Extract the filename from the URL
        $url_filename = wp_basename($local_url);
        $master_filename = wp_basename($metadata['file']);

        // Check if URL points to the master file
        if ($url_filename === $master_filename) {
            return []; // Points to master, all good
        }

        // Check if URL points to a known size
        $sizes = $metadata['sizes'] ?? [];
        foreach ($sizes as $size_name => $size_data) {
            if (($size_data['file'] ?? '') === $url_filename) {
                return []; // Points to a valid size
            }
        }

        // Check if URL points to the pre-scaled original_image
        // This is fixable: replace with the current master file
        $original_image = $metadata['original_image'] ?? '';
        if ($original_image === $url_filename) {
            return [[
                'category' => SQMedia_Post_Content_Scanner::CAT_STALE_SIZE,
                'details' => [
                    'url' => $ref['url'],
                    'attachment_id' => $attachment_id,
                    'referenced_file' => $url_filename,
                    'referenced_size' => null,
                    'old_width' => 0,
                    'old_height' => 0,
                    'replacement_file' => $master_filename,
                    'replacement_size' => 'full',
                    'source' => $ref['source'],
                ],
            ]];
        }

        // File not found in metadata — this is a stale size reference
        // Try to determine what size it was (extract dimensions from filename)
        $old_size = null;
        $old_width = 0;
        $old_height = 0;

        if (preg_match('/-(\d+)x(\d+)\.[a-z]+$/i', $url_filename, $dim_match)) {
            $old_width = (int) $dim_match[1];
            $old_height = (int) $dim_match[2];

            // Try to match by size_slug from Gutenberg block
            if (!empty($ref['size_slug'])) {
                $old_size = $ref['size_slug'];
            }
        }

        // Compute what the fix would be
        $replacement = $this->find_replacement($old_size, $old_width, $old_height, $metadata);

        return [[
            'category' => SQMedia_Post_Content_Scanner::CAT_STALE_SIZE,
            'details' => [
                'url' => $ref['url'],
                'attachment_id' => $attachment_id,
                'referenced_file' => $url_filename,
                'referenced_size' => $old_size,
                'old_width' => $old_width,
                'old_height' => $old_height,
                'replacement_file' => $replacement['file'],
                'replacement_size' => $replacement['size_label'],
                'source' => $ref['source'],
            ],
        ]];
    }

    // =========================================================================
    // FIX HELPERS (also used by the scanner's fix logic)
    // =========================================================================

    /**
     * Find a replacement file for a stale size reference
     *
     * Priority:
     * 1. If size label still exists in metadata, use its current file
     * 2. Find closest size going UP (never downscale)
     * 3. Fallback to master file
     *
     * @return array ['file' => filename, 'size_label' => label or 'full']
     */
    public function find_replacement(?string $old_size, int $old_width, int $old_height, array $metadata): array {
        $sizes = $metadata['sizes'] ?? [];
        $master_file = wp_basename($metadata['file'] ?? '');

        // 1. If the size label still exists in metadata, use the current file for that size
        if ($old_size && isset($sizes[$old_size])) {
            return [
                'file' => $sizes[$old_size]['file'],
                'size_label' => $old_size,
            ];
        }

        // 2. Find closest size going UP
        if ($old_width > 0 && $old_height > 0) {
            $best_match = null;
            $best_area = PHP_INT_MAX;

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

            if ($best_match) {
                return [
                    'file' => $sizes[$best_match]['file'],
                    'size_label' => $best_match,
                ];
            }
        }

        // 3. Fallback to master
        return [
            'file' => $master_file,
            'size_label' => 'full',
        ];
    }

    /**
     * Check if a URL looks like an image
     */
    private function is_image_url(string $url): bool {
        $path = wp_parse_url($url, PHP_URL_PATH);
        if (!$path) {
            return false;
        }
        $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
        return in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico', 'tiff', 'avif'], true);
    }
}
