<?php

/**
 * Centralized Output Buffer Handler
 *
 * Provides a single output buffer for all performance modules to hook into.
 * Based on WP Rocket's `rocket_buffer` and Perfmatters' buffer patterns.
 *
 * This ensures complete page coverage (headers, footers, sidebars) instead of
 * just content areas filtered via `the_content`.
 *
 * @package ProRank\SEO\Core
 * @since   2.0.0
 */

declare(strict_types=1);

namespace ProRank\SEO\Core;

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


/**
 * Buffer class for centralized output buffering
 */


class Buffer {

    /**
     * Whether buffer has been started
     *
     * @var bool
     */
    private static bool $started = false;

    /**
     * Request start time for performance tracking
     *
     * @var float
     */
    private static float $start_time;

    /**
     * Initialize the buffer system
     *
     * @return void
     */
    public static function init(): void {
        // Don't run in admin, AJAX, or cron contexts
        if (is_admin() || wp_doing_ajax() || wp_doing_cron()) {
            return;
        }

        // Don't run for REST API requests
        if (defined('REST_REQUEST') && REST_REQUEST) {
            return;
        }

        // Store request start time
        self::$start_time = microtime(true);

        // Start buffering on template_redirect (after WordPress has set up the query)
        add_action('template_redirect', [__CLASS__, 'start'], 1);
    }

    /**
     * Start output buffering
     *
     * @return void
     */
    public static function start(): void {
        // Prevent double-starting
        if (self::$started) {
            return;
        }

        // Don't buffer for logged-in users unless explicitly enabled
        if (is_user_logged_in()) {
            $settings = get_option('prorank_performance_settings', []);
            if (empty($settings['optimize_logged_in'])) {
                return;
            }
        }

        // Don't buffer previews or customizer
        if (is_preview() || is_customize_preview()) {
            return;
        }

        // Don't buffer feeds
        if (is_feed()) {
            return;
        }

        // Don't buffer 404 pages (optional - can be configured)
        if (is_404()) {
            $settings = get_option('prorank_performance_settings', []);
            if (empty($settings['optimize_404'])) {
                return;
            }
        }

        self::$started = true;
        ob_start([__CLASS__, 'process']);
    }

    /**
     * Process the buffered output
     *
     * This method applies all registered filters to the HTML output.
     * Filters should be added at specific priorities:
     *   - 5:  Early processing (e.g., remove comments)
     *   - 10: Critical CSS injection
     *   - 15: Font optimization (preload, font-display)
     *   - 20: JavaScript optimization (defer, delay)
     *   - 25: Lazy loading (images, iframes, videos)
     *   - 30: CLS prevention (add dimensions)
     *   - 35: HTML minification
     *   - 40: Final cleanup
     *
     * @param string $html The buffered HTML output
     * @return string Processed HTML
     */
    public static function process(string $html): string {
        // Quick validation - only process full HTML pages
        if (!self::is_valid_html($html)) {
            return $html;
        }

        // Allow other plugins to check if buffer is active
        if (!apply_filters('prorank_buffer_enabled', true)) {
            return $html;
        }

        /**
         * Main filter for HTML processing
         *
         * Performance modules should hook into this filter:
         *
         * @example
         * add_filter('prorank_output_buffer', [$this, 'process_html'], 25);
         *
         * @param string $html The full page HTML
         * @return string Modified HTML
         */
        $original_html = $html;
        $processed_html = apply_filters('prorank_output_buffer', $html);
        if (!is_string($processed_html) || $processed_html === '') {
            $processed_html = $original_html;
        }

        $html = self::guard_structural_containers($original_html, $processed_html, 'prorank_output_buffer');

        // Add debug comment if enabled
        if (self::is_debug_enabled()) {
            $html = self::add_debug_comment($html);
        }

        return $html;
    }

    /**
     * Guard protected structural containers from unsafe front-end mutations.
     *
     * Reverts to the original HTML when a buffer mutation collapses headers,
     * navs, or footers in a suspicious way.
     *
     * @param string $original_html Original HTML before mutation.
     * @param string $processed_html Mutated HTML after processing.
     * @param string $context Buffer/mutator name for logging.
     * @return string Safe HTML output.
     */
    public static function guard_structural_containers(string $original_html, string $processed_html, string $context = 'buffer'): string {
        if ($original_html === '' || $processed_html === '' || $original_html === $processed_html) {
            return $processed_html;
        }

        if (!self::is_structural_guard_enabled()) {
            return $processed_html;
        }

        if (!self::is_valid_html($original_html) || !self::is_valid_html($processed_html)) {
            return $processed_html;
        }

        $before = self::capture_structural_metrics($original_html);
        $after  = self::capture_structural_metrics($processed_html);

        if ($before === [] || $after === []) {
            return $processed_html;
        }

        if (!self::has_structural_regression($before, $after)) {
            return $processed_html;
        }

        if (function_exists('prorank_log')) {
            $before_json = function_exists('wp_json_encode') ? wp_json_encode($before) : json_encode($before);
            $after_json  = function_exists('wp_json_encode') ? wp_json_encode($after) : json_encode($after);
            prorank_log(sprintf(
                '[ProRank SEO] Structural guard reverted mutated HTML in %s | before=%s | after=%s',
                $context,
                is_string($before_json) ? $before_json : '{}',
                is_string($after_json) ? $after_json : '{}'
            ));
        }

        self::record_structural_guard_incident($context, $before, $after);

        return $original_html;
    }

    /**
     * Check if HTML is valid for processing
     *
     * @param string $html HTML content
     * @return bool
     */
    private static function is_valid_html(string $html): bool {
        // Skip empty content
        if (empty($html)) {
            return false;
        }

        // Must have HTML structure
        if (stripos($html, '<html') === false) {
            return false;
        }

        // Must have body closing tag (indicates full page)
        if (stripos($html, '</body>') === false) {
            return false;
        }

        // Skip if content looks like JSON
        if (trim($html)[0] === '{' || trim($html)[0] === '[') {
            return false;
        }

        // Skip if content looks like XML
        if (stripos($html, '<?xml') === 0) {
            return false;
        }

        return true;
    }

    /**
     * Check whether structural guard is enabled.
     *
     * @return bool
     */
    private static function is_structural_guard_enabled(): bool {
        $settings = self::get_advanced_settings();
        $enabled = !is_array($settings) || !array_key_exists('structural_guard_enabled', $settings)
            ? true
            : !empty($settings['structural_guard_enabled']);

        return (bool) apply_filters('prorank_buffer_structural_guard_enabled', $enabled);
    }

    /**
     * Capture lightweight metrics for protected structural regions.
     *
     * @param string $html Full HTML document.
     * @return array<string, array<string, int>>
     */
    private static function capture_structural_metrics(string $html): array {
        if (!class_exists('\DOMDocument')) {
            return [];
        }

        $previous = libxml_use_internal_errors(true);

        $dom = new \DOMDocument();
        $loaded = $dom->loadHTML('<?xml encoding="utf-8" ?>' . $html);
        if (!$loaded) {
            libxml_clear_errors();
            libxml_use_internal_errors($previous);
            return [];
        }

        $xpath = new \DOMXPath($dom);

        $metrics = [
            'header' => self::capture_region_metrics(
                $xpath,
                '//*[self::header or @id="thrive-header" or @id="masthead" or contains(concat(" ", normalize-space(@class), " "), " site-header ") or contains(concat(" ", normalize-space(@class), " "), " thrv_header ")]'
            ),
            'nav' => self::capture_region_metrics(
                $xpath,
                '//*[self::nav or @id="site-navigation" or contains(concat(" ", normalize-space(@class), " "), " main-navigation ") or contains(concat(" ", normalize-space(@class), " "), " header-menu ") or contains(concat(" ", normalize-space(@class), " "), " thrv_widget_menu ")]'
            ),
            'footer' => self::capture_region_metrics(
                $xpath,
                '//*[self::footer or @id="thrive-footer" or @id="colophon" or contains(concat(" ", normalize-space(@class), " "), " site-footer ") or contains(concat(" ", normalize-space(@class), " "), " thrv_footer ")]'
            ),
        ];

        libxml_clear_errors();
        libxml_use_internal_errors($previous);

        return $metrics;
    }

    /**
     * Capture counts for a structural region.
     *
     * @param \DOMXPath $xpath DOM XPath instance.
     * @param string    $query XPath query.
     * @return array<string, int>
     */
    private static function capture_region_metrics(\DOMXPath $xpath, string $query): array {
        $nodes = $xpath->query($query);
        if (!$nodes instanceof \DOMNodeList || $nodes->length === 0) {
            return [
                'containers'  => 0,
                'anchors'     => 0,
                'menu_items'  => 0,
                'text_length' => 0,
            ];
        }

        $metrics = [
            'containers'  => 0,
            'anchors'     => 0,
            'menu_items'  => 0,
            'text_length' => 0,
        ];

        foreach ($nodes as $node) {
            if (!$node instanceof \DOMElement) {
                continue;
            }

            $metrics['containers']++;
            $metrics['anchors'] += (int) $xpath->evaluate('count(.//a)', $node);
            $metrics['menu_items'] += (int) $xpath->evaluate('count(.//*[self::li or contains(concat(" ", normalize-space(@class), " "), " menu-item ")])', $node);

            $text = preg_replace('/\s+/u', ' ', trim((string) $node->textContent));
            $metrics['text_length'] += is_string($text) ? strlen($text) : 0;
        }

        return $metrics;
    }

    /**
     * Determine whether processed HTML regressed in protected regions.
     *
     * @param array<string, array<string, int>> $before Metrics before mutation.
     * @param array<string, array<string, int>> $after  Metrics after mutation.
     * @return bool
     */
    private static function has_structural_regression(array $before, array $after): bool {
        foreach (['header', 'nav', 'footer'] as $region) {
            $before_region = $before[$region] ?? [];
            $after_region  = $after[$region] ?? [];

            if (self::region_regressed($before_region, $after_region)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Check whether a single region regressed in a meaningful way.
     *
     * @param array<string, int> $before Metrics before mutation.
     * @param array<string, int> $after  Metrics after mutation.
     * @return bool
     */
    private static function region_regressed(array $before, array $after): bool {
        $before_containers = (int) ($before['containers'] ?? 0);
        $after_containers  = (int) ($after['containers'] ?? 0);
        if ($before_containers > 0 && $after_containers === 0) {
            return true;
        }

        $before_anchors = (int) ($before['anchors'] ?? 0);
        $after_anchors  = (int) ($after['anchors'] ?? 0);
        if (
            ($before_anchors >= 4 && $after_anchors === 0)
            || ($before_anchors >= 6 && $after_anchors <= (int) floor($before_anchors / 2))
        ) {
            return true;
        }

        $before_items = (int) ($before['menu_items'] ?? 0);
        $after_items  = (int) ($after['menu_items'] ?? 0);
        if (
            ($before_items >= 4 && $after_items === 0)
            || ($before_items >= 6 && $after_items <= (int) floor($before_items / 2))
        ) {
            return true;
        }

        $before_text = (int) ($before['text_length'] ?? 0);
        $after_text  = (int) ($after['text_length'] ?? 0);
        if (
            ($before_anchors >= 3 || $before_items >= 3)
            && $before_text >= 120
            && $after_text > 0
            && $after_text < (int) floor($before_text * 0.4)
        ) {
            return true;
        }

        return false;
    }

    /**
     * Check if debug mode is enabled
     *
     * @return bool
     */
    private static function is_debug_enabled(): bool {
        // Check for constant
        if (defined('PRORANK_DEBUG') && PRORANK_DEBUG) {
            return true;
        }

        // Check for setting
        $settings = self::get_advanced_settings();
        return !empty($settings['debug_mode']);
    }

    /**
     * Read advanced settings using the canonical option with legacy fallback.
     *
     * @return array<string,mixed>
     */
    private static function get_advanced_settings(): array {
        $settings = get_option('prorank_seo_advanced_settings', []);
        if (!is_array($settings) || $settings === []) {
            $legacy = get_option('prorank_advanced_settings', []);
            if (is_array($legacy) && $legacy !== []) {
                $settings = $legacy;
            }
        }

        return is_array($settings) ? $settings : [];
    }

    /**
     * Whether auto rollback is enabled.
     *
     * @return bool
     */
    private static function is_auto_rollback_enabled(): bool {
        $settings = self::get_advanced_settings();

        return !array_key_exists('performance_auto_rollback', $settings)
            || !empty($settings['performance_auto_rollback']);
    }

    /**
     * Record a structural guard incident and optionally arm automatic rollback.
     *
     * @param string                               $context
     * @param array<string, array<string, int>>    $before
     * @param array<string, array<string, int>>    $after
     * @return void
     */
    private static function record_structural_guard_incident(string $context, array $before, array $after): void {
        if (!self::is_auto_rollback_enabled()) {
            return;
        }

        $module_slug = self::map_structural_guard_context_to_module($context);
        if ($module_slug === '') {
            return;
        }

        $rollbacks = get_option('prorank_performance_rollbacks', []);
        if (!is_array($rollbacks)) {
            $rollbacks = [];
        }

        $now = time();
        $window = HOUR_IN_SECONDS;
        $threshold = 2;
        $cooldown = 6 * HOUR_IN_SECONDS;

        $entry = isset($rollbacks[$module_slug]) && is_array($rollbacks[$module_slug])
            ? $rollbacks[$module_slug]
            : [];

        if ((int) ($entry['last'] ?? 0) < ($now - $window)) {
            $entry['count'] = 0;
        }

        $entry['count'] = (int) ($entry['count'] ?? 0) + 1;
        $entry['last'] = $now;
        $entry['context'] = $context;
        $entry['before'] = $before;
        $entry['after'] = $after;

        if ($entry['count'] >= $threshold) {
            $entry['disabled_until'] = $now + $cooldown;
        }

        $rollbacks[$module_slug] = $entry;
        update_option('prorank_performance_rollbacks', $rollbacks, false);

        if (!empty($entry['disabled_until']) && function_exists('prorank_log')) {
            prorank_log(sprintf(
                '[ProRank SEO] Auto rollback armed for %s until %d after structural regressions.',
                $module_slug,
                (int) $entry['disabled_until']
            ));
        }
    }

    /**
     * Map a structural guard context to the related module slug.
     *
     * @param string $context
     * @return string
     */
    private static function map_structural_guard_context_to_module(string $context): string {
        $map = [
            'js_optimize' => 'js_opt',
            'font_optimization' => 'font_optimization',
            'cls_prevention' => 'css_opt',
            'lcp_beacon' => 'lcp_beacon',
            'lcp_optimizer' => 'lcp_optimizer',
        ];

        return $map[$context] ?? '';
    }

    /**
     * Add debug comment to HTML
     *
     * @param string $html HTML content
     * @return string HTML with debug comment
     */
    private static function add_debug_comment(string $html): string {
        $elapsed = round((microtime(true) - self::$start_time) * 1000, 2);
        $memory = round(memory_get_peak_usage() / 1024 / 1024, 2);

        $comment = sprintf(
            "\n<!-- ProRank SEO | Buffer processed in %sms | Memory: %sMB -->\n",
            $elapsed,
            $memory
        );

        // Insert before </body>
        $pos = stripos($html, '</body>');
        if ($pos !== false) {
            $html = substr_replace($html, $comment, $pos, 0);
        }

        return $html;
    }

    /**
     * Check if buffer has been started
     *
     * @return bool
     */
    public static function is_started(): bool {
        return self::$started;
    }

    /**
     * Get elapsed time since buffer start
     *
     * @return float Time in milliseconds
     */
    public static function get_elapsed_time(): float {
        if (!isset(self::$start_time)) {
            return 0.0;
        }
        return (microtime(true) - self::$start_time) * 1000;
    }
}
