<?php

/**
 * LCP Beacon Module
 *
 * Detects actual Largest Contentful Paint (LCP) elements using browser PerformanceObserver API.
 * Stores per-page LCP data to optimize fetchpriority and preload hints.
 *
 * @package ProRank\SEO\Modules\Performance
 * @since   2.0.0
 */

declare(strict_types=1);

namespace ProRank\SEO\Modules\Performance;

use ProRank\SEO\Core\Buffer;
use ProRank\SEO\Modules\BaseModule;

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


/**
 * LCP Beacon detection module
 */


class LCPBeaconModule extends BaseModule {

    /**
     * Required tier for this module
     */
    protected string $feature_tier = 'free';

    /**
     * Option name for storing LCP data
     */
    private const LCP_DATA_OPTION = 'prorank_lcp_beacon_data';

    /**
     * Only apply beacon fetchpriority to very early images in markup order.
     *
     * @var int
     */
    private const MAX_FETCHPRIORITY_IMAGE_INDEX = 4;

    /**
     * Minimum width for a standalone image LCP candidate.
     *
     * @var int
     */
    private const MIN_IMAGE_LCP_WIDTH = 320;

    /**
     * Minimum height for a standalone image LCP candidate.
     *
     * @var int
     */
    private const MIN_IMAGE_LCP_HEIGHT = 180;

    /**
     * Minimum area for a standalone image LCP candidate.
     *
     * @var int
     */
    private const MIN_IMAGE_LCP_AREA = 100000;

    /**
     * Per-request counter for wp_get_attachment_image_attributes calls.
     *
     * @var int
     */
    private int $attachment_image_index = 0;

    /**
     * Guard to avoid duplicate output buffers for LCP HTML enforcement.
     *
     * @var bool
     */
    private bool $lcp_html_buffer_started = false;

    /**
     * Constructor
     */
    public function __construct() {
        $this->slug = 'lcp_beacon';
        $this->name = 'LCP Detection';
        $this->description = 'Automatically detects and optimizes Largest Contentful Paint elements';
$this->parent_slug = 'performance';
    }

    /**
     * Initialize module hooks
     */
    public function init_hooks(): void {
        if (!$this->is_enabled()) {
            return;
        }

        // Don't run in admin
        if (is_admin()) {
            return;
        }

        // Inject beacon script on frontend
        add_action('wp_footer', [$this, 'inject_beacon_script'], 99);

        // Register REST endpoint for beacon data
        add_action('rest_api_init', [$this, 'register_rest_routes']);

        // Apply LCP optimizations based on collected data
        add_action('wp_head', [$this, 'apply_lcp_optimizations'], 1);
        add_filter('wp_get_loading_optimization_attributes', [$this, 'normalize_loading_optimization_attributes'], 999, 4);
        add_filter('wp_get_attachment_image_attributes', [$this, 'enforce_preloaded_attachment_priority'], 9999, 3);
        add_filter('get_avatar', [$this, 'normalize_avatar_html_priority'], 999, 6);
        add_action('template_redirect', [$this, 'start_lcp_html_buffer'], 9999);
    }

    /**
     * Avatars should never keep fetchpriority/high priority hints.
     *
     * @param string $avatar_html
     * @param mixed  $id_or_email
     * @param mixed  $size
     * @param string $default
     * @param string $alt
     * @param array  $args
     * @return string
     */
    public function normalize_avatar_html_priority(
        string $avatar_html,
        $id_or_email = null,
        $size = null,
        string $default = '',
        string $alt = '',
        array $args = []
    ): string {
        if ($avatar_html === '' || stripos($avatar_html, '<img') === false) {
            return $avatar_html;
        }

        $updated = preg_replace('/\s*fetchpriority\s*=\s*["\'][^"\']*["\']/i', '', $avatar_html);
        if (!is_string($updated) || $updated === '') {
            $updated = $avatar_html;
        }

        if (preg_match('/\bloading\s*=\s*["\']eager["\']/i', $updated) === 1) {
            $updated = preg_replace('/\bloading\s*=\s*["\']eager["\']/i', 'loading="lazy"', $updated, 1) ?: $updated;
        }

        if (preg_match('/\bdata-prorank-lcp\s*=\s*["\'][^"\']*["\']/i', $updated) === 1) {
            $updated = preg_replace('/\s*data-prorank-lcp\s*=\s*["\'][^"\']*["\']/i', '', $updated, 1) ?: $updated;
        }

        return $updated;
    }

    /**
     * Register REST routes for beacon data collection
     */
    public function register_rest_routes(): void {
        $route_args = [
            'methods' => 'POST',
            'callback' => [$this, 'handle_beacon_data'],
            'permission_callback' => '__return_true', // Public endpoint for frontend beacon
            'args' => [
                'url' => [
                    'required' => false,
                    'type' => 'string',
                    'sanitize_callback' => 'esc_url_raw',
                ],
                'lcp_element' => [
                    'required' => false,
                    // Don't specify type - we'll handle object/string ourselves
                ],
            ],
        ];

        // Register both variants to avoid host-level slash redirects converting
        // POST beacon requests into GET and causing noisy 404s.
        register_rest_route('prorank-seo/v1', '/lcp-beacon', $route_args);
        register_rest_route('prorank-seo/v1', '/lcp-beacon/', $route_args);
    }

    /**
     * Handle incoming LCP beacon data
     *
     * @param \WP_REST_Request $request
     * @return \WP_REST_Response
     */
    public function handle_beacon_data(\WP_REST_Request $request): \WP_REST_Response {
        // Basic rate limit (per IP, per minute)
        $ip = $_SERVER['REMOTE_ADDR'] ?? '';
        if (!empty($ip)) {
            $rl_key = 'prorank_lcp_beacon_rl_' . md5($ip);
            $count = (int) get_transient($rl_key);
            if ($count >= 60) {
                return new \WP_REST_Response(['success' => false, 'message' => 'Rate limited'], 429);
            }
            set_transient($rl_key, $count + 1, MINUTE_IN_SECONDS);
        }

        $url = $request->get_param('url');
        $lcp_element = $request->get_param('lcp_element');

        // Handle case where lcp_element might be a JSON string (from sendBeacon)
        if (is_string($lcp_element)) {
            $lcp_element = json_decode($lcp_element, true);
        }

        if (empty($url) || empty($lcp_element) || !is_array($lcp_element)) {
            return new \WP_REST_Response([
                'success' => true,
                'accepted' => false,
                'reason' => 'invalid_payload',
            ], 202);
        }

        $normalized_page_url = $this->normalize_site_url(esc_url_raw($url));
        if (empty($normalized_page_url)) {
            return new \WP_REST_Response([
                'success' => true,
                'accepted' => false,
                'reason' => 'invalid_url',
            ], 202);
        }

        // Get URL pattern (remove query strings and normalize)
        $url_pattern = $this->get_url_pattern($normalized_page_url);

        // Get existing LCP data
        $lcp_data = $this->get_lcp_data_store();

        $src = $this->normalize_site_asset_url($lcp_element['src'] ?? '');
        $background_image = $this->normalize_site_asset_url($lcp_element['backgroundImage'] ?? '');

        // Nothing usable to optimize
        if (empty($src) && empty($background_image)) {
            return new \WP_REST_Response([
                'success' => true,
                'accepted' => false,
                'reason' => 'no_image_url',
            ], 202);
        }

        $existing_entry = is_array($lcp_data[$url_pattern] ?? null) ? $lcp_data[$url_pattern] : [];
        $timestamp = time();
        $new_entry = [
            'selector' => substr(sanitize_text_field($lcp_element['selector'] ?? ''), 0, 200),
            'tag' => substr(sanitize_text_field($lcp_element['tag'] ?? ''), 0, 50),
            'src' => $src,
            'background_image' => $background_image,
            'width' => absint($lcp_element['width'] ?? 0),
            'height' => absint($lcp_element['height'] ?? 0),
            'load_time' => floatval($lcp_element['loadTime'] ?? 0),
        ];

        if (!$this->is_valid_lcp_entry($new_entry)) {
            return new \WP_REST_Response([
                'success' => true,
                'accepted' => false,
                'reason' => 'invalid_candidate',
            ], 202);
        }

        $write_cooldown = max(0, (int) apply_filters('prorank_lcp_beacon_write_cooldown', 6 * HOUR_IN_SECONDS));
        $last_updated = absint($existing_entry['updated'] ?? 0);
        $same_target = !empty($existing_entry)
            && (string) ($existing_entry['selector'] ?? '') === (string) $new_entry['selector']
            && (string) ($existing_entry['tag'] ?? '') === (string) $new_entry['tag']
            && (string) ($existing_entry['src'] ?? '') === (string) $new_entry['src']
            && (string) ($existing_entry['background_image'] ?? '') === (string) $new_entry['background_image'];

        if (
            $same_target
            && $write_cooldown > 0
            && $last_updated > 0
            && ($timestamp - $last_updated) < $write_cooldown
        ) {
            return new \WP_REST_Response([
                'success' => true,
                'accepted' => true,
                'skipped' => true,
                'reason' => 'cooldown',
            ], 202);
        }

        // Store/update LCP data for this URL pattern.
        $lcp_data[$url_pattern] = $new_entry + [
            'updated' => $timestamp,
            'samples' => ($existing_entry['samples'] ?? 0) + 1,
        ];

        $max_entries = max(50, (int) apply_filters('prorank_lcp_beacon_max_entries', 500));
        if (count($lcp_data) > $max_entries) {
            uasort($lcp_data, fn($a, $b) => $b['updated'] - $a['updated']);
            $lcp_data = array_slice($lcp_data, 0, $max_entries, true);
        }

        $this->persist_lcp_data_store($lcp_data);

        return new \WP_REST_Response(['success' => true], 200);
    }

    /**
     * Normalize and scope a page URL to this site.
     */
    private function normalize_site_url(string $url): string {
        $url = trim($url);
        if ($url === '') {
            return '';
        }

        $parsed = wp_parse_url($url);
        if ($parsed === false) {
            return '';
        }

        $site_host = wp_parse_url(home_url(), PHP_URL_HOST);
        $host = $parsed['host'] ?? '';

        if (!empty($host) && !empty($site_host) && strtolower($host) !== strtolower($site_host)) {
            return '';
        }

        if (empty($host)) {
            $path = $parsed['path'] ?? '';
            if ($path === '' || $path[0] !== '/') {
                return '';
            }
            return home_url($path);
        }

        return $url;
    }

    /**
     * Only allow asset URLs from this site (prevents beacon poisoning to force preloading 3rd parties).
     */
    private function normalize_site_asset_url($url): string {
        if (!is_string($url) || $url === '') {
            return '';
        }

        $url = esc_url_raw($url);
        if ($url === '') {
            return '';
        }

        $parsed = wp_parse_url($url);
        if ($parsed === false) {
            return '';
        }

        $site_host = wp_parse_url(home_url(), PHP_URL_HOST);
        $host = $parsed['host'] ?? '';

        if (!empty($host) && !empty($site_host) && strtolower($host) !== strtolower($site_host)) {
            return '';
        }

        if (empty($host)) {
            $path = $parsed['path'] ?? '';
            if ($path === '' || $path[0] !== '/') {
                return '';
            }
            return home_url($path);
        }

        return $url;
    }

    /**
     * Inject the LCP beacon script
     */
    public function inject_beacon_script(): void {
        // Only inject for non-logged-in users to get real visitor data
        // Or when learning mode is enabled
        if (is_user_logged_in() && !$this->get_setting('learn_from_admins', false)) {
            return;
        }

        // Use trailing slash form to avoid 301 redirects on strict-slash hosts.
        $rest_url = trailingslashit(rest_url('prorank-seo/v1/lcp-beacon'));
        ?>
        <script id="prorank-lcp-beacon">
        (function() {
            'use strict';

            // Only run if PerformanceObserver is supported
            if (!('PerformanceObserver' in window)) return;

            let lcpElement = null;
            let lcpEntry = null;
            let lcpUrl = '';

            // Create observer for LCP
            const observer = new PerformanceObserver((list) => {
                const entries = list.getEntries();
                const lastEntry = entries[entries.length - 1];

                if (lastEntry) {
                    lcpEntry = lastEntry;
                    lcpElement = lastEntry.element;
                    lcpUrl = lastEntry.url || '';
                }
            });

            try {
                observer.observe({ type: 'largest-contentful-paint', buffered: true });
            } catch (e) {
                // Browser doesn't support LCP observation
                return;
            }

            // Send data after page is fully loaded
            const sendBeacon = function() {
                observer.disconnect();

                if (!lcpElement) return;

                // Build unique selector for the element
                const selector = buildSelector(lcpElement);
                const tag = lcpElement.tagName.toLowerCase();

                // Prefer the URL from the PerformanceEntry (covers <img> and many background images)
                // and avoid triggering forced style/layout calculations.
                let src = lcpUrl || lcpElement.currentSrc || lcpElement.src || '';
                let backgroundImage = '';

                // For non-img elements, use entry URL as background-image when available.
                if (tag !== 'img' && lcpUrl) {
                    backgroundImage = lcpUrl;
                }

                // Avoid forced reflow: never read layout-dependent properties like offsetWidth/offsetHeight.
                let width = 0;
                let height = 0;
                if (tag === 'img') {
                    width = lcpElement.naturalWidth || 0;
                    height = lcpElement.naturalHeight || 0;
                } else if (tag === 'video') {
                    width = lcpElement.videoWidth || 0;
                    height = lcpElement.videoHeight || 0;
                }

                const data = {
                    url: window.location.href,
                    lcp_element: {
                        selector: selector,
                        tag: tag,
                        src: src,
                        backgroundImage: backgroundImage,
                        width: width,
                        height: height,
                        loadTime: lcpEntry ? lcpEntry.loadTime : 0
                    }
                };

                // Send via beacon API (non-blocking)
                // Use Blob to set Content-Type header (sendBeacon sends text/plain by default)
                const blob = new Blob([JSON.stringify(data)], {type: 'application/json'});
                if (navigator.sendBeacon) {
                    navigator.sendBeacon(
                        '<?php echo esc_js($rest_url); ?>',
                        blob
                    );
                } else {
                    // Fallback to fetch
                    fetch('<?php echo esc_js($rest_url); ?>', {
                        method: 'POST',
                        body: blob,
                        headers: { 'Content-Type': 'application/json' },
                        keepalive: true
                    }).catch(() => {});
                }
            };

            // Build a unique CSS selector for an element
            function buildSelector(el) {
                if (!el || el === document.body) return 'body';

                // If element has an ID, use it
                if (el.id) {
                    return '#' + CSS.escape(el.id);
                }

                // Build selector with classes and nth-child
                const parts = [];
                let current = el;

                while (current && current !== document.body && parts.length < 5) {
                    let selector = current.tagName.toLowerCase();

                    // Add classes (first 2 significant ones)
                    const classes = Array.from(current.classList)
                        .filter(c => c && !c.match(/^(wp-|elementor-|et-|fl-)/))
                        .slice(0, 2);

                    if (classes.length) {
                        selector += '.' + classes.map(c => CSS.escape(c)).join('.');
                    }

                    // Add nth-child for uniqueness
                    const parent = current.parentElement;
                    if (parent) {
                        const siblings = Array.from(parent.children).filter(
                            c => c.tagName === current.tagName
                        );
                        if (siblings.length > 1) {
                            const index = siblings.indexOf(current) + 1;
                            selector += ':nth-of-type(' + index + ')';
                        }
                    }

                    parts.unshift(selector);
                    current = current.parentElement;
                }

                return parts.join(' > ');
            }

            // Listen for page unload to send final data
            if ('onvisibilitychange' in document) {
                document.addEventListener('visibilitychange', function() {
                    if (document.visibilityState === 'hidden') {
                        sendBeacon();
                    }
                });
            }

            // Also send after load + small delay
            window.addEventListener('load', function() {
                if ('requestIdleCallback' in window) {
                    requestIdleCallback(sendBeacon, { timeout: 3000 });
                } else {
                    setTimeout(sendBeacon, 2500);
                }
            });
        })();
        </script>
        <?php
    }

    /**
     * Apply LCP optimizations based on collected data
     */
    public function apply_lcp_optimizations(): void {
        $lcp_data = $this->get_lcp_data_store();

        if (empty($lcp_data)) {
            return;
        }

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

        // Get current URL pattern
        $current_pattern = $this->get_url_pattern($this->get_current_url());

        // Find matching LCP data
        $lcp = $lcp_data[$current_pattern] ?? null;

        if (!$lcp) {
            return;
        }

        // Only apply if we have enough samples (reliability threshold)
        if (($lcp['samples'] ?? 0) < 3) {
            return;
        }

        // Determine which image URL to preload
        $image_url = '';

        // Prefer img src for img tags
        if ($lcp['tag'] === 'img' && !empty($lcp['src'])) {
            $image_url = $lcp['src'];
        }
        // Use background-image for other elements (div, section, etc.)
        elseif (!empty($lcp['background_image'])) {
            $image_url = $lcp['background_image'];
        }
        // Fallback to src if background_image is empty but src exists
        elseif (!empty($lcp['src'])) {
            $image_url = $lcp['src'];
        }

        // Add preload hint for LCP image (with deduplication)
        if (!empty($image_url)) {
            // Deduplicate: track already-preloaded URLs across all modules
            if (!isset($GLOBALS['prorank_preloaded_urls'])) {
                $GLOBALS['prorank_preloaded_urls'] = [];
            }
            $normalized_url = esc_url($image_url);
            if ($this->is_preload_url_already_registered($normalized_url)) {
                return; // Already preloaded by another module
            }
            $GLOBALS['prorank_preloaded_urls'][] = $normalized_url;

            // Keep fetchpriority before href so Chrome's preload scanner sees
            // the priority hint before it dispatches the image request.
            $preload = sprintf(
                '<link rel="preload" fetchpriority="high" as="image" href="%s">',
                $normalized_url
            );
            echo wp_kses(
                $preload,
                [
                    'link' => [
                        'rel' => [],
                        'as' => [],
                        'href' => [],
                        'fetchpriority' => [],
                    ],
                ]
            ) . "\n";
        }
    }

    /**
     * Beacon preload hints should only run when no richer LCP owner is active.
     *
     * Premium already has the optimizer module with stale-data checks,
     * builder-aware background handling, and viewport-aware preload logic.
     * The beacon module is intentionally simpler, so it must not compete with
     * the optimizer or ignore the shared preload toggle.
     *
     * @return bool
     */
    private function should_emit_beacon_preload_hint(): bool {
        $preload_enabled = $this->get_setting('lcp_preload_lcp', $this->get_setting('preload_lcp', true));
        if (!$preload_enabled) {
            return false;
        }

        if (class_exists('\\ProRank\\SEO\\Modules\\Performance\\LCPOptimizerModule')) {
            $optimizer_enabled = $this->get_setting('lcp_optimizer_enabled', false);
            if ($optimizer_enabled) {
                return false;
            }
        }

        return true;
    }

    /**
     * Add fetchpriority="high" to LCP images
     *
     * @param array    $attr       Image attributes
     * @param \WP_Post $attachment Attachment post
     * @param mixed    $size       Image size
     * @return array
     */
    public function add_fetchpriority(array $attr, \WP_Post $attachment, $size): array {
        // Only run on frontend
        if (is_admin()) {
            return $attr;
        }

        $this->attachment_image_index++;
        if ($this->attachment_image_index > self::MAX_FETCHPRIORITY_IMAGE_INDEX) {
            return $attr;
        }

        $lcp_data = $this->get_lcp_data_store();
        $current_pattern = $this->get_url_pattern($this->get_current_url());

        $lcp = $lcp_data[$current_pattern] ?? null;

        if (!$lcp || ($lcp['samples'] ?? 0) < 3) {
            return $attr;
        }

        // Check if this image matches LCP
        $src = $attr['src'] ?? '';
        $current_basename = $this->extract_image_basename((string) $src);
        $lcp_basename = $this->extract_image_basename((string) ($lcp['src'] ?? ''));

        if ($current_basename === '' || $lcp_basename === '') {
            return $attr;
        }

        // If another module already selected a preloaded image for this request,
        // do not let stale beacon history force a conflicting candidate.
        $preloaded_basename = $this->get_first_preloaded_image_basename();
        if ($preloaded_basename !== '' && strtolower($preloaded_basename) !== strtolower($lcp_basename)) {
            return $attr;
        }

        if (strtolower($lcp_basename) === strtolower($current_basename)) {
            $attr['fetchpriority'] = 'high';
            // Remove lazy loading from LCP images
            unset($attr['loading']);
        }

        return $attr;
    }

    /**
     * Normalize core loading-optimization attrs using request preloaded image.
     *
     * @param array  $loading_attrs
     * @param string $tag_name
     * @param array  $attr
     * @param string $context
     * @return array
     */
    public function normalize_loading_optimization_attributes(array $loading_attrs, string $tag_name, array $attr, string $context): array {
        unset($context);

        if ($tag_name !== 'img') {
            return $loading_attrs;
        }

        $preferred_basename = $this->get_first_preloaded_image_basename();
        if ($preferred_basename === '') {
            return $loading_attrs;
        }

        $current_basename = $this->extract_image_basename((string) ($attr['src'] ?? ''));
        if ($current_basename === '') {
            return $loading_attrs;
        }

        if (strtolower($current_basename) === strtolower($preferred_basename)) {
            $loading_attrs['fetchpriority'] = 'high';
            $loading_attrs['loading'] = false;
            return $loading_attrs;
        }

        if (isset($loading_attrs['fetchpriority']) && strtolower((string) $loading_attrs['fetchpriority']) === 'high') {
            $loading_attrs['fetchpriority'] = false;
        }

        return $loading_attrs;
    }

    /**
     * Enforce that only the request-selected preloaded image keeps high priority.
     *
     * @param array $attributes
     * @param mixed $attachment
     * @param mixed $size
     * @return array
     */
    public function enforce_preloaded_attachment_priority(array $attributes, $attachment, $size): array {
        unset($attachment, $size);

        $preferred_basename = $this->get_first_preloaded_image_basename();
        if ($preferred_basename === '') {
            return $attributes;
        }

        $current_basename = $this->extract_image_basename((string) ($attributes['src'] ?? ''));
        if ($current_basename === '') {
            return $attributes;
        }

        if (strtolower($current_basename) === strtolower($preferred_basename)) {
            $attributes['fetchpriority'] = 'high';
            if (($attributes['loading'] ?? '') === 'lazy') {
                $attributes['loading'] = 'eager';
            }
            return $attributes;
        }

        if (isset($attributes['fetchpriority']) && strtolower((string) $attributes['fetchpriority']) === 'high') {
            unset($attributes['fetchpriority']);
        }

        return $attributes;
    }

    /**
     * Start a final HTML output buffer to enforce eager loading on preloaded LCP image.
     *
     * @return void
     */
    public function start_lcp_html_buffer(): void {
        if ($this->lcp_html_buffer_started || is_admin()) {
            return;
        }

        if (function_exists('wp_doing_ajax') && wp_doing_ajax()) {
            return;
        }

        if (defined('REST_REQUEST') && REST_REQUEST) {
            return;
        }

        ob_start([$this, 'enforce_preloaded_lcp_image_in_html']);
        $this->lcp_html_buffer_started = true;
    }

    /**
     * Ensure preloaded LCP image tag is eager/high priority in final HTML.
     *
     * @param string $html
     * @return string
     */
    public function enforce_preloaded_lcp_image_in_html(string $html): string {
        if ($html === '' || stripos($html, '<img') === false || stripos($html, 'rel="preload"') === false) {
            return $html;
        }

        $original_html = $html;

        $target_url = $this->extract_preloaded_image_url_from_html($html);
        $target_path = $this->normalize_asset_path_for_compare($target_url);
        if ($target_path === '') {
            return $html;
        }

        $updated = preg_replace_callback(
            '/<img\b[^>]*>/i',
            function (array $matches) use ($target_path): string {
                $img = $matches[0] ?? '';
                if (!is_string($img) || $img === '') {
                    return is_string($img) ? $img : '';
                }

                if (preg_match('/\bsrc\s*=\s*["\']([^"\']+)["\']/i', $img, $src_match) !== 1) {
                    return $img;
                }

                $src_path = $this->normalize_asset_path_for_compare((string) ($src_match[1] ?? ''));
                if ($src_path === '' || $src_path !== $target_path) {
                    if (preg_match('/\bfetchpriority\s*=\s*["\']high["\']/i', $img) === 1) {
                        $img = preg_replace('/\s*fetchpriority\s*=\s*["\']high["\']/i', '', $img, 1) ?: $img;
                    }
                    return $img;
                }

                if (preg_match('/\bloading\s*=/', $img) === 1) {
                    $img = preg_replace('/\bloading\s*=\s*["\'][^"\']*["\']/i', 'loading="eager"', $img, 1) ?: $img;
                } else {
                    $img = preg_replace('/<img\b/i', '<img loading="eager"', $img, 1) ?: $img;
                }

                if (preg_match('/\bfetchpriority\s*=/', $img) === 1) {
                    $img = preg_replace('/\bfetchpriority\s*=\s*["\'][^"\']*["\']/i', 'fetchpriority="high"', $img, 1) ?: $img;
                } else {
                    $img = preg_replace('/<img\b/i', '<img fetchpriority="high"', $img, 1) ?: $img;
                }

                return $img;
            },
            $html
        );

        $updated_html = is_string($updated) ? $updated : $html;

        return Buffer::guard_structural_containers($original_html, $updated_html, 'lcp_beacon');
    }

    /**
     * Extract first preloaded image URL from HTML head markup.
     *
     * @param string $html
     * @return string
     */
    private function extract_preloaded_image_url_from_html(string $html): string {
        if (preg_match('/<link\b(?=[^>]*\brel\s*=\s*["\']preload["\'])(?=[^>]*\bas\s*=\s*["\']image["\'])(?=[^>]*\bhref\s*=\s*["\']([^"\']+)["\'])[^>]*>/i', $html, $m) === 1) {
            return trim((string) ($m[1] ?? ''));
        }

        return '';
    }

    /**
     * Normalize URL/path for stable image matching.
     *
     * @param string $url
     * @return string
     */
    private function normalize_asset_path_for_compare(string $url): string {
        $url = trim(html_entity_decode($url, ENT_QUOTES));
        if ($url === '') {
            return '';
        }

        $path = (string) (wp_parse_url($url, PHP_URL_PATH) ?? '');
        if ($path === '') {
            return '';
        }

        return strtolower($path);
    }

    /**
     * Extract filename from a URL/path while ignoring query strings.
     *
     * @param string $url
     * @return string
     */
    private function extract_image_basename(string $url): string {
        $url = trim($url);
        if ($url === '') {
            return '';
        }

        $path = (string) (wp_parse_url($url, PHP_URL_PATH) ?? '');
        if ($path === '') {
            $path = $url;
        }

        $name = basename($path);
        return is_string($name) ? trim($name) : '';
    }

    /**
     * Get first preloaded image basename emitted for current request, if any.
     *
     * @return string
     */
    private function get_first_preloaded_image_basename(): string {
        $preloaded = $GLOBALS['prorank_preloaded_urls'] ?? [];
        if (!is_array($preloaded)) {
            return '';
        }

        foreach ($preloaded as $entry) {
            if (!is_string($entry) || trim($entry) === '') {
                continue;
            }

            $candidate = trim((string) explode('|', $entry, 2)[0]);
            if (preg_match('/\.(avif|webp|jpe?g|png|gif|svg)(\?|$)/i', $candidate) !== 1) {
                continue;
            }

            $basename = $this->extract_image_basename($candidate);
            if ($basename !== '') {
                return $basename;
            }
        }

        return '';
    }

    /**
     * Determine whether another ProRank module already registered this preload URL.
     *
     * LCP Optimizer stores entries as "url|media" while the beacon historically
     * stored only the raw URL. Treat both formats as equivalent so the beacon
     * cannot emit a duplicate generic preload after media-specific preloads.
     *
     * @param string $url
     * @return bool
     */
    private function is_preload_url_already_registered(string $url): bool {
        $url = trim($url);
        if ($url === '') {
            return false;
        }

        $preloaded = $GLOBALS['prorank_preloaded_urls'] ?? [];
        if (!is_array($preloaded) || empty($preloaded)) {
            return false;
        }

        foreach ($preloaded as $entry) {
            if (!is_string($entry) || $entry === '') {
                continue;
            }

            if ($entry === $url || str_starts_with($entry, $url . '|')) {
                return true;
            }
        }

        return false;
    }

    /**
     * Get URL pattern for grouping similar pages
     *
     * @param string $url
     * @return string
     */
    private function get_url_pattern(string $url): string {
        $parsed = wp_parse_url($url);
        $path = $parsed['path'] ?? '/';

        // Normalize path
        $path = trim($path, '/');

        if (empty($path)) {
            return 'home';
        }

        // Remove numeric IDs from paths for pattern matching
        // e.g., /post/123/title -> /post/*/title
        $path = preg_replace('/\/\d+\//', '/*/', $path);
        $path = preg_replace('/\/\d+$/', '/*', $path);

        // Create pattern based on path structure
        $parts = explode('/', $path);
        $parts = array_slice($parts, 0, 3); // Limit depth

        return implode('/', $parts);
    }

    /**
     * Get current URL
     *
     * @return string
     */
    private function get_current_url(): string {
        $protocol = is_ssl() ? 'https' : 'http';
        $host = $_SERVER['HTTP_HOST'] ?? '';
        $uri = $_SERVER['REQUEST_URI'] ?? '';

        return $protocol . '://' . $host . $uri;
    }

    /**
     * Get collected LCP statistics
     *
     * @return array
     */
    public function get_lcp_stats(): array {
        $lcp_data = $this->get_lcp_data_store();

        $stats = [
            'total_patterns' => count($lcp_data),
            'patterns' => [],
        ];

        foreach ($lcp_data as $pattern => $data) {
            $stats['patterns'][$pattern] = [
                'selector' => $data['selector'] ?? '',
                'tag' => $data['tag'] ?? '',
                'samples' => $data['samples'] ?? 0,
                'avg_load_time' => $data['load_time'] ?? 0,
                'last_updated' => wp_date('Y-m-d H:i:s', $data['updated'] ?? 0),
            ];
        }

        return $stats;
    }

    /**
     * Clear all LCP data
     */
    public function clear_lcp_data(): void {
        delete_option(self::LCP_DATA_OPTION);
    }

    /**
     * Load persisted beacon LCP data.
     *
     * @return array<string, array>
     */
    private function get_lcp_data_store(): array {
        $beacon_data = get_option(self::LCP_DATA_OPTION, []);
        if (!is_array($beacon_data)) {
            $beacon_data = [];
        }

        $beacon_data = $this->filter_valid_lcp_entries($beacon_data);
        $optimizer_fallback = $this->filter_valid_lcp_entries($this->get_optimizer_lcp_fallback_store());

        if (empty($optimizer_fallback)) {
            return $beacon_data;
        }

        foreach ($optimizer_fallback as $pattern => $entry) {
            if (!isset($beacon_data[$pattern])) {
                $beacon_data[$pattern] = $entry;
            }
        }

        return $beacon_data;
    }

    /**
     * Keep only usable LCP entries. Small branding assets should never override
     * optimizer-learned hero/background candidates.
     *
     * @param array<string,mixed> $entries
     * @return array<string,array<string,mixed>>
     */
    private function filter_valid_lcp_entries(array $entries): array {
        $valid = [];

        foreach ($entries as $pattern => $entry) {
            if (!is_string($pattern) || !is_array($entry)) {
                continue;
            }

            if (!$this->is_valid_lcp_entry($entry)) {
                continue;
            }

            $valid[$pattern] = $entry;
        }

        return $valid;
    }

    /**
     * Validate whether a learned LCP entry is worth applying.
     *
     * Reject tiny inline/logo-style images so they cannot override genuine hero
     * background/image candidates learned by the optimizer fallback.
     *
     * @param array<string,mixed> $entry
     * @return bool
     */
    private function is_valid_lcp_entry(array $entry): bool {
        $src = isset($entry['src']) ? trim((string) $entry['src']) : '';
        $background = isset($entry['background_image']) ? trim((string) $entry['background_image']) : '';

        if ($src === '' && $background === '') {
            return false;
        }

        if ($this->looks_like_branding_asset($entry)) {
            return false;
        }

        if ($background !== '') {
            return true;
        }

        $tag = strtolower((string) ($entry['tag'] ?? ''));
        if ($tag !== 'img') {
            return true;
        }

        $width = absint($entry['width'] ?? 0);
        $height = absint($entry['height'] ?? 0);

        if ($width > 0 && $height > 0) {
            if (
                $width < self::MIN_IMAGE_LCP_WIDTH
                || $height < self::MIN_IMAGE_LCP_HEIGHT
                || ($width * $height) < self::MIN_IMAGE_LCP_AREA
            ) {
                return false;
            }
        }

        return true;
    }

    /**
     * Branding assets should never be treated as LCP candidates.
     *
     * @param array<string,mixed> $entry
     * @return bool
     */
    private function looks_like_branding_asset(array $entry): bool {
        $selector = strtolower((string) ($entry['selector'] ?? ''));
        $src = strtolower((string) ($entry['src'] ?? ''));
        $background = strtolower((string) ($entry['background_image'] ?? ''));
        $tag = strtolower((string) ($entry['tag'] ?? ''));

        $tokens = $selector . ' ' . $src . ' ' . $background . ' ' . $tag;
        foreach (['logo', 'site-icon', 'favicon', 'avatar', 'brandmark', 'wordmark'] as $needle) {
            if (strpos($tokens, $needle) !== false) {
                return true;
            }
        }

        return false;
    }

    /**
     * Fall back to optimizer-learned LCP data when beacon-specific samples are not available yet.
     *
     * @return array<string, array<string, mixed>>
     */
    private function get_optimizer_lcp_fallback_store(): array {
        $optimizer_data = get_option('prorank_lcp_optimizer_data', []);
        if (!is_array($optimizer_data) || empty($optimizer_data)) {
            return [];
        }

        $page_key = $this->get_optimizer_page_template_key();
        if ($page_key === '' || empty($optimizer_data[$page_key]) || !is_array($optimizer_data[$page_key])) {
            return [];
        }

        $entry = $optimizer_data[$page_key];
        $url = isset($entry['url']) ? esc_url_raw((string) $entry['url']) : '';
        if ($url === '') {
            return [];
        }

        $current_pattern = $this->get_url_pattern($this->get_current_url());
        if ($current_pattern === '') {
            return [];
        }

        $is_background = !empty($entry['is_background']);

        return [
            $current_pattern => [
                'selector' => substr(sanitize_text_field((string) ($entry['element_selector'] ?? '')), 0, 200),
                'tag' => $is_background ? 'div' : substr(sanitize_text_field((string) ($entry['element_tag'] ?? 'img')), 0, 50),
                'src' => $is_background ? '' : $url,
                'background_image' => $is_background ? $url : '',
                'width' => 0,
                'height' => 0,
                'load_time' => 0.0,
                'updated' => absint($entry['learned_at'] ?? time()),
                'samples' => max(3, (int) ($entry['sample_count'] ?? 1)),
            ],
        ];
    }

    /**
     * Resolve the optimizer data key for the current request.
     *
     * @return string
     */
    private function get_optimizer_page_template_key(): string {
        if (is_front_page()) {
            return 'front_page';
        }
        if (is_home()) {
            return 'blog_home';
        }
        if (is_singular('post')) {
            return 'single_post';
        }
        if (is_singular('page')) {
            $template = get_page_template_slug();
            return 'page_' . ($template ?: 'default');
        }
        if (is_singular()) {
            return 'single_' . get_post_type();
        }
        if (is_archive()) {
            if (is_category()) {
                return 'archive_category';
            }
            if (is_tag()) {
                return 'archive_tag';
            }
            if (is_author()) {
                return 'archive_author';
            }
            return 'archive_' . (get_post_type() ?: 'post');
        }
        if (is_search()) {
            return 'search';
        }
        if (is_404()) {
            return '404';
        }

        return '';
    }

    /**
     * Persist beacon LCP data.
     *
     * @param array<string, array> $data
     * @return void
     */
    private function persist_lcp_data_store(array $data): void {
        update_option(self::LCP_DATA_OPTION, $data, false);
    }
}
