<?php

/**
 * Content Visibility Module
 *
 * Applies CSS `content-visibility: auto` to user-configured below-fold
 * selectors so the browser can skip rendering them until they scroll into
 * view. Reduces render work on long pages without lazy-loading their DOM.
 *
 * @package ProRank\SEO\Modules\Performance
 * @since   1.4.0
 */

declare(strict_types=1);

namespace ProRank\SEO\Modules\Performance;

use ProRank\SEO\Modules\BaseModule;

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


/**
 * ContentVisibilityModule class
 *
 * Reads from prorank_asset_optimization_settings:
 *   content_visibility_enabled    (bool)   – master toggle
 *   content_visibility_selectors  (string) – newline-delimited CSS selectors
 *   content_visibility_size_hint  (string) – contain-intrinsic-size hint (default "auto 800px")
 *
 * Injects a single inline <style> on wp_head priority 1.
 */
class ContentVisibilityModule extends BaseModule {

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

    /**
     * Constructor
     */
    public function __construct() {
        $this->slug        = 'content_visibility';
        $this->name        = 'Below-Fold Render Skip';
        $this->description = 'Apply CSS content-visibility:auto to selected below-fold sections';
        $this->parent_slug = 'performance';
    }

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

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

        if (is_admin()) {
            return;
        }

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

        add_action('wp_head', [$this, 'inject_content_visibility_css'], 1);
    }

    /**
     * Inject the inline CSS rule covering the configured selectors.
     *
     * @return void
     */
    public function inject_content_visibility_css(): void {
        $selectors = $this->normalize_selectors($this->get_setting('content_visibility_selectors', ''));
        if (empty($selectors)) {
            return;
        }

        $size_hint = trim((string) $this->get_setting('content_visibility_size_hint', 'auto 800px'));
        if ($size_hint === '' || !$this->is_safe_size_hint($size_hint)) {
            $size_hint = 'auto 800px';
        }

        $selector_list = implode(', ', array_map([$this, 'escape_selector'], $selectors));
        if ($selector_list === '') {
            return;
        }

        $css = sprintf(
            '%s { content-visibility: auto; contain-intrinsic-size: %s; }',
            $selector_list,
            $size_hint
        );

        echo "<style id=\"prorank-content-visibility\">" . $css . "</style>\n";
    }

    /**
     * Whether the per-module feature toggle is on.
     *
     * @return bool
     */
    private function is_feature_enabled(): bool {
        $value = $this->get_setting('content_visibility_enabled', false);

        if (is_string($value)) {
            return in_array(strtolower($value), ['true', '1', 'yes', 'on'], true);
        }

        return (bool) $value;
    }

    /**
     * Parse the selector string into a deduped list, capped at a sane maximum.
     *
     * @param mixed $raw
     * @return array<int,string>
     */
    private function normalize_selectors($raw): array {
        if (is_array($raw)) {
            $items = $raw;
        } elseif (is_string($raw)) {
            $items = preg_split('/[\r\n,]+/', $raw) ?: [];
        } else {
            return [];
        }

        $out = [];
        foreach ($items as $item) {
            $item = trim((string) $item);
            if ($item === '') {
                continue;
            }

            // Strict whitelist of safe CSS selector chars to avoid injecting
            // arbitrary content into the <style> block. Anything that contains
            // braces, semicolons, slashes or angle brackets is dropped.
            if (!preg_match('/^[A-Za-z0-9 .#:>+~*\-_\[\]="\'(),]+$/', $item)) {
                continue;
            }

            $out[$item] = $item;
            if (count($out) >= 64) {
                break;
            }
        }

        return array_values($out);
    }

    /**
     * Escape a selector for inline-style embedding.
     *
     * @param string $selector
     * @return string
     */
    private function escape_selector(string $selector): string {
        return str_replace(['<', '>'], '', $selector);
    }

    /**
     * Validate the contain-intrinsic-size hint.
     *
     * Allowed forms: "auto", "auto <length>", "<length>", "<length> <length>".
     * Lengths must be a positive number followed by px/em/rem/vw/vh/%.
     *
     * @param string $hint
     * @return bool
     */
    private function is_safe_size_hint(string $hint): bool {
        return (bool) preg_match(
            '/^(auto\s+)?(\d+(?:\.\d+)?(px|em|rem|vw|vh|%))(\s+\d+(?:\.\d+)?(px|em|rem|vw|vh|%))?$|^auto$/i',
            $hint
        );
    }
}
