<?php

/**
 * Lazy media helper
 *
 * Adds loading/decoding hints to images/iframes in post content.
 *
 * @package ProRank\SEO\Frontend
 */

declare(strict_types=1);

namespace ProRank\SEO\Frontend;

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




class LazyMedia {
    public function init(): void {
        if ( \ProRank\SEO\Plugin::is_elementor_runtime_request() ) {
            return;
        }

        add_filter( 'the_content', [ $this, 'inject_lazy_attrs' ], 15 );

        if ( $this->should_process_full_page_buffer() ) {
            add_action( 'template_redirect', [ $this, 'start_output_buffer' ], 1 );
        }
    }

    public function inject_lazy_attrs( string $content ): string {
        if ( \ProRank\SEO\Plugin::is_elementor_runtime_request() ) {
            return $content;
        }

        if ( is_admin() ) {
            return $content;
        }

        $settings = get_option('prorank_image_optimization_settings', []);
        if (!is_array($settings)) {
            $settings = [];
        }

        $lazy_images = $this->is_truthy_setting($settings['lazyload_images'] ?? null);
        $lazy_iframes = $this->is_truthy_setting($settings['lazyload_iframes'] ?? null);
        $lazy_videos = $this->is_truthy_setting($settings['lazyload_videos'] ?? null);

        if (!$lazy_images && !$lazy_iframes && !$lazy_videos) {
            return $content;
        }

        // Add loading/decoding to images
        if ($lazy_images) {
            $content = preg_replace_callback( '#<img[^>]+>#i', function( $matches ) {
                $tag = $matches[0];
                if ( stripos( $tag, 'loading=' ) === false ) {
                    $tag = preg_replace( '/<img/i', '<img loading="lazy"', $tag, 1 );
                }
                if ( stripos( $tag, 'decoding=' ) === false ) {
                    $tag = preg_replace( '/<img/i', '<img decoding="async"', $tag, 1 );
                }
                return $tag;
            }, $content );
        }

        // Add lazy to iframes
        if ($lazy_iframes || $lazy_videos) {
            $content = preg_replace_callback( '#<iframe[^>]+>#i', function( $matches ) use ($lazy_iframes, $lazy_videos) {
                $tag = $matches[0];

                // Normalize critical embeds that should render immediately.
                $tag = $this->normalize_critical_iframe_loading($tag);

                if (!$lazy_iframes && !$lazy_videos) {
                    return $tag;
                }
                if ($this->should_skip_iframe_lazy($tag, $lazy_iframes, $lazy_videos)) {
                    return $tag;
                }
                if (!$lazy_iframes && $lazy_videos) {
                    $is_video_embed = (stripos($tag, 'youtube.com') !== false)
                        || (stripos($tag, 'youtu.be') !== false)
                        || (stripos($tag, 'vimeo.com') !== false);
                    if (!$is_video_embed) {
                        return $tag;
                    }
                }

                // For map embeds saved with loading="eager", override to lazy by
                // default unless explicit no-lazy markers are present.
                if ($this->is_map_embed($tag) && stripos($tag, 'loading=') !== false) {
                    $tag = preg_replace('/loading\\s*=\\s*([\"\\\']).*?\\1/i', 'loading="lazy"', $tag, 1) ?? $tag;
                    return $tag;
                }

                if ( stripos( $tag, 'loading=' ) === false ) {
                    $tag = preg_replace( '/<iframe/i', '<iframe loading="lazy"', $tag, 1 );
                }
                return $tag;
            }, $content );
        }

        return $content;
    }

    /**
     * Start a full-page output buffer for builder layouts.
     *
     * This path is intentionally limited to iframe/video lazy behaviour. Image
     * lazy-loading stays on content/image hooks to avoid above-the-fold
     * regressions on builder-heavy pages.
     *
     * @return void
     */
    public function start_output_buffer(): void {
        if ( ! $this->is_frontend_output_context() || ! $this->should_process_full_page_buffer() ) {
            return;
        }

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

    /**
     * Process final frontend HTML for lazy iframe/video attributes.
     *
     * @param string $buffer
     * @return string
     */
    public function process_output_buffer( string $buffer ): string {
        if ( $buffer === '' || stripos( $buffer, '<iframe' ) === false ) {
            return $buffer;
        }

        $settings = get_option( 'prorank_image_optimization_settings', [] );
        if ( ! is_array( $settings ) ) {
            $settings = [];
        }

        $lazy_iframes = $this->is_truthy_setting( $settings['lazyload_iframes'] ?? null );
        $lazy_videos  = $this->is_truthy_setting( $settings['lazyload_videos'] ?? null );

        if ( ! $lazy_iframes && ! $lazy_videos ) {
            return $buffer;
        }

        return preg_replace_callback(
            '#<iframe[^>]+>#i',
            function ( $matches ) use ( $lazy_iframes, $lazy_videos ) {
                $tag = $matches[0];

                $tag = $this->normalize_critical_iframe_loading( $tag );

                if ( ! $lazy_iframes && ! $lazy_videos ) {
                    return $tag;
                }

                if ( $this->should_skip_iframe_lazy( $tag, $lazy_iframes, $lazy_videos ) ) {
                    return $tag;
                }

                if ( ! $lazy_iframes && $lazy_videos ) {
                    $is_video_embed = ( stripos( $tag, 'youtube.com' ) !== false )
                        || ( stripos( $tag, 'youtu.be' ) !== false )
                        || ( stripos( $tag, 'vimeo.com' ) !== false );
                    if ( ! $is_video_embed ) {
                        return $tag;
                    }
                }

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

                if ( stripos( $tag, 'loading=' ) === false ) {
                    return preg_replace( '/<iframe/i', '<iframe loading="lazy"', $tag, 1 ) ?? $tag;
                }

                return $tag;
            },
            $buffer
        ) ?? $buffer;
    }

    /**
     * Skip lazy-loading for critical iframe types that frequently break UX when delayed.
     *
     * @param string $tag
     * @param bool   $lazy_iframes
     * @param bool   $lazy_videos
     * @return bool
     */
    private function should_skip_iframe_lazy(string $tag, bool $lazy_iframes, bool $lazy_videos): bool {
        $lower = strtolower($tag);

        // Respect explicit markup intent.
        if (strpos($lower, 'data-no-lazyload') !== false || strpos($lower, 'no-lazyload') !== false) {
            return true;
        }

        $is_map_embed = $this->is_map_embed($tag);

        // Keep explicit eager for non-map iframes. Map embeds are usually below
        // the fold and benefit from lazy loading by default.
        if (strpos($lower, 'loading="eager"') !== false && !$is_map_embed) {
            return true;
        }

        // ReCAPTCHA/challenge frames should not be delayed.
        if (strpos($lower, 'recaptcha') !== false || strpos($lower, 'hcaptcha') !== false) {
            return true;
        }

        // If only "lazy videos" is enabled, keep non-video embeds untouched.
        if (!$lazy_iframes && $lazy_videos) {
            $is_video_embed = (strpos($lower, 'youtube.com') !== false)
                || (strpos($lower, 'youtu.be') !== false)
                || (strpos($lower, 'vimeo.com') !== false);
            if (!$is_video_embed) {
                return true;
            }
        }

        return false;
    }

    /**
     * Force eager loading for critical iframe providers if markup was saved as lazy.
     *
     * @param string $tag
     * @return string
     */
    private function normalize_critical_iframe_loading(string $tag): string {
        $lower = strtolower($tag);

        $critical_patterns = [
            '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;
    }

    /**
     * Detect known map/location iframe providers.
     *
     * @param string $tag
     * @return bool
     */
    private function is_map_embed(string $tag): bool {
        $lower = strtolower($tag);
        foreach ([
            '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',
        ] as $pattern) {
            if (strpos($lower, $pattern) !== false) {
                return true;
            }
        }

        return false;
    }

    /**
     * Restrict full-page buffering to real frontend HTML responses.
     *
     * @return bool
     */
    private function is_frontend_output_context(): bool {
        if ( \ProRank\SEO\Plugin::is_elementor_runtime_request() ) {
            return false;
        }

        if ( is_admin() || is_feed() || wp_doing_ajax() || wp_doing_cron() ) {
            return false;
        }

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

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

        return true;
    }

    /**
     * Only builder/outside-content iframe/video lazy behaviour needs the
     * full-page buffer.
     *
     * @return bool
     */
    private function should_process_full_page_buffer(): bool {
        $settings = get_option( 'prorank_image_optimization_settings', [] );
        if ( ! is_array( $settings ) ) {
            $settings = [];
        }

        return $this->is_truthy_setting( $settings['lazyload_iframes'] ?? null )
            || $this->is_truthy_setting( $settings['lazyload_videos'] ?? null );
    }

    /**
     * Normalize bool-like values from option storage.
     *
     * @param mixed $value Raw option value.
     * @return bool
     */
    private function is_truthy_setting($value): bool {
        if (is_bool($value)) {
            return $value;
        }

        if (is_int($value) || is_float($value)) {
            return ((int) $value) !== 0;
        }

        if (is_string($value)) {
            $normalized = strtolower(trim($value));
            if ($normalized === '' || in_array($normalized, ['0', 'false', 'no', 'off', 'disabled'], true)) {
                return false;
            }
            if (in_array($normalized, ['1', 'true', 'yes', 'on', 'enabled'], true)) {
                return true;
            }
        }

        return !empty($value);
    }
}
