<?php

/**
 * CLS Prevention Module
 *
 * Automatically prevents Cumulative Layout Shift issues using universal fixes
 * Works on ANY WordPress site regardless of theme
 *
 * @package ProRank\SEO\Modules\Performance
 * @since   1.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;
}


/**
 * CLSPreventionModule class
 */


class CLSPreventionModule extends BaseModule {

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

    /**
     * Constructor
     */
    public function __construct() {
        $this->slug = 'cls_prevention';
        $this->name = 'CLS Prevention';
        $this->description = 'Automatically prevent Cumulative Layout Shift (CLS) issues';
$this->parent_slug = 'performance';
    }

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

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

        if (\ProRank\SEO\Plugin::is_elementor_runtime_request()) {
            return;
        }

        // Inject CLS prevention CSS very early in head
        add_action('wp_head', [$this, 'safe_inject_cls_prevention_css'], 1);

        // Process HTML to fix images AND body classes
        add_action('template_redirect', [$this, 'safe_start_output_buffer'], 1);
    }

    /**
     * Fail-open wrapper for CLS prevention CSS output.
     *
     * @return void
     */
    public function safe_inject_cls_prevention_css(): void {
        try {
            $this->inject_cls_prevention_css();
        } catch (\Throwable $e) {
            $this->log_frontend_callback_error('inject_cls_prevention_css', $e);
        }
    }

    /**
     * Start the CLS output buffer with a fail-open callback.
     *
     * @return void
     */
    public function safe_start_output_buffer(): void {
        try {
            if (!$this->should_process_frontend_html()) {
                return;
            }

            ob_start([$this, 'safe_process_html']);
        } catch (\Throwable $e) {
            $this->log_frontend_callback_error('start_output_buffer', $e);
        }
    }

    /**
     * Fail-open output-buffer callback for CLS HTML processing.
     *
     * @param mixed $html
     * @return string
     */
    public function safe_process_html($html): string {
        $fallback = is_string($html) ? $html : '';

        try {
            return $this->process_html($fallback);
        } catch (\Throwable $e) {
            $this->log_frontend_callback_error('process_html', $e);
            return $fallback;
        }
    }

    /**
     * Log frontend optimization failures without taking the page down.
     *
     * @param string     $callback
     * @param \Throwable $e
     * @return void
     */
    private function log_frontend_callback_error(string $callback, \Throwable $e): void {
        if (function_exists('prorank_log')) {
            prorank_log('[ProRank CLSPreventionModule] ' . $callback . ' failed: ' . $e->getMessage());
        }
    }

    /**
     * Inject CLS prevention CSS
     *
     * @return void
     */
    public function inject_cls_prevention_css(): void {
        $css = $this->get_universal_css();
        if ($css === '') {
            return;
        }

        wp_register_style('prorank-cls-prevention', false, [], PRORANK_VERSION);
        wp_enqueue_style('prorank-cls-prevention');
        wp_add_inline_style('prorank-cls-prevention', prorank_sanitize_inline_css($css));
    }

    /**
     * Get universal CLS prevention CSS that works on ANY site
     *
     * @return string CSS
     */
    private function get_universal_css(): string {
        return '
/* ProRank CLS Prevention - Safe Universal Baseline */

/* Keep images/video responsive. */
img,
video,
object {
    max-width: 100%;
    height: auto;
}

/* Keep embeds responsive, but preserve their intrinsic or script-managed height. */
iframe,
embed {
    max-width: 100%;
}

/* Ensure aspect-ratio styles injected by HTML pass are respected. */
img[style*="aspect-ratio"] {
    display: block;
}

/* Avoid FOIT where browsers support it, without forcing global layout rules. */
html {
    font-size-adjust: from-font;
}';
    }

    /**
     * Start output buffer to process HTML
     *
     * @return void
     */
    public function start_output_buffer(): void {
        if (!$this->should_process_frontend_html()) {
            return;
        }

        ob_start([$this, 'process_html']);
    }

    /**
     * Process HTML to fix CLS issues
     *
     * @param string $html HTML content
     * @return string Modified HTML
     */
    public function process_html(string $html): string {
        if (empty($html)) {
            return $html;
        }

        $original_html = $html;

        // Only mutate real HTML documents. Avoid touching JSON/XML/other payloads.
        $lower = strtolower($html);
        if (strpos($lower, '<html') === false && strpos($lower, '<!doctype html') === false) {
            return $html;
        }

        // FIX 3: Process all img tags - remove lazy from above-fold, add aspect-ratio
        $html = preg_replace_callback(
            '/<img\s+[^>]*>/i',
            [$this, 'process_img_tag'],
            $html
        );

        // Normalize critical iframes (maps/challenges) to eager loading.
        // Many themes/page-builders persist these as loading="lazy" even above-the-fold.
        $html = preg_replace_callback(
            '/<iframe\s+[^>]*>/i',
            [$this, 'normalize_critical_iframe_tag'],
            $html
        );

        // FIX 4: Remove heavy WooCommerce block fonts on non-WooCommerce pages
        // WooCommerce injects large local fonts (e.g., Inter variable) via wp-fonts-local, even on pages that don't use WC blocks.
        if (
            $this->should_strip_woocommerce_block_fonts()
            && stripos($html, 'wp-fonts-local') !== false
            && stripos($html, '/wp-content/plugins/woocommerce/assets/fonts/') !== false
        ) {
            $html = $this->strip_woocommerce_block_font_faces($html);
        }

        return Buffer::guard_structural_containers($original_html, $html, 'cls_prevention');
    }

    /**
     * Guard output buffering to real frontend HTML requests only.
     *
     * @return bool
     */
    private function should_process_frontend_html(): bool {
        if (is_admin() || wp_doing_ajax() || wp_doing_cron()) {
            return false;
        }

        if (\ProRank\SEO\Plugin::is_elementor_runtime_request()) {
            return false;
        }

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

        if (function_exists('wp_is_json_request') && wp_is_json_request()) {
            return false;
        }

        if (is_feed() || is_trackback() || is_robots() || is_embed()) {
            return false;
        }

        $method = isset($_SERVER['REQUEST_METHOD']) ? strtoupper((string) $_SERVER['REQUEST_METHOD']) : 'GET';
        if (!in_array($method, ['GET', 'HEAD'], true)) {
            return false;
        }

        return true;
    }

    /**
     * Process individual img tag
     *
     * @param array $matches Regex matches
     * @return string Modified img tag
     */
    private function process_img_tag(array $matches): string {
        $img = $matches[0];
        // CLS prevention should only stabilize layout, not change loading strategy.
        $img = $this->add_aspect_ratio($img);

        return $img;
    }

    /**
     * Add aspect-ratio to img tag if it has width and height
     *
     * @param string $img Img tag
     * @return string Modified img tag
     */
    private function add_aspect_ratio(string $img): string {
        // Skip if already has aspect-ratio
        if (stripos($img, 'aspect-ratio') !== false) {
            return $img;
        }

        // Extract width and height
        $width = 0;
        $height = 0;

        if (preg_match('/\bwidth\s*=\s*["\']?(\d+)/i', $img, $w_match)) {
            $width = (int)$w_match[1];
        }
        if (preg_match('/\bheight\s*=\s*["\']?(\d+)/i', $img, $h_match)) {
            $height = (int)$h_match[1];
        }

        if ($width <= 0 || $height <= 0) {
            return $img;
        }

        $ratio = $width . '/' . $height;

        // Add or append to style
        if (preg_match('/style\s*=\s*["\']([^"\']*)["\']/', $img, $style_match)) {
            $new_style = rtrim($style_match[1], '; ') . ';aspect-ratio:' . $ratio . ';';
            return str_replace($style_match[0], 'style="' . $new_style . '"', $img);
        }

        // Add new style attribute
        return str_replace('<img', '<img style="aspect-ratio:' . $ratio . ';"', $img);
    }

    /**
     * Force eager loading for critical iframe providers.
     *
     * @param array $matches
     * @return string
     */
    private function normalize_critical_iframe_tag(array $matches): string {
        $tag = $matches[0] ?? '';
        if ($tag === '') {
            return $tag;
        }

        $lower = strtolower($tag);
        $critical_patterns = [
            'google.com/maps',
            'google.com/map',
            'maps.googleapis.com',
            'maps.google.',
            'maps-solutions',
            'storage.googleapis.com/maps-solutions',
            'openstreetmap.org',
            'mapbox.com',
            'bing.com/maps',
            'waze.com',
            'recaptcha',
            'hcaptcha',
        ];

        $is_critical = false;
        foreach ($critical_patterns as $pattern) {
            if (strpos($lower, $pattern) !== false) {
                $is_critical = true;
                break;
            }
        }

        if (!$is_critical) {
            return $tag;
        }

        if (stripos($tag, 'loading=') !== false) {
            return preg_replace('/loading\\s*=\\s*([\"\\\'])lazy\\1/i', 'loading="eager"', $tag) ?? $tag;
        }

        return preg_replace('/<iframe/i', '<iframe loading="eager"', $tag, 1) ?? $tag;
    }

    /**
     * Strip WooCommerce block fonts on non-WooCommerce pages.
     *
     * @return bool
     */
    private function should_strip_woocommerce_block_fonts(): bool {
        if (!class_exists('WooCommerce')) {
            return false;
        }

        foreach (['is_woocommerce', 'is_cart', 'is_checkout', 'is_account_page'] as $fn) {
            if (function_exists($fn) && $fn()) {
                return false;
            }
        }

        return true;
    }

    /**
     * Remove WooCommerce-injected @font-face rules from the wp-fonts-local <style> block.
     *
     * @param string $html
     * @return string
     */
    private function strip_woocommerce_block_font_faces(string $html): string {
        $result = preg_replace_callback(
            '/<style\\b([^>]*)\\bclass=[\"\\\']wp-fonts-local[\"\\\']([^>]*)>(.*?)<\\/style>/is',
            function (array $matches): string {
                $css = (string) ($matches[3] ?? '');
                if ($css === '' || stripos($css, '/wp-content/plugins/woocommerce/assets/fonts/') === false) {
                    return $matches[0];
                }

                $filtered = preg_replace(
                    '/@font-face\\{[^}]*\\/wp-content\\/plugins\\/woocommerce\\/assets\\/fonts\\/[^}]*\\}\\s*/i',
                    '',
                    $css
                );

                $filtered = trim((string) $filtered);
                if ($filtered === '') {
                    return '';
                }

                // Preserve the original <style> tag attributes.
                return '<style' . ($matches[1] ?? '') . 'class="wp-fonts-local"' . ($matches[2] ?? '') . '>' . $filtered . '</style>';
            },
            $html
        );

        return is_string($result) ? $result : $html;
    }
}
