<?php
/**
 * Safe enqueue helpers for dynamic CSS/JS (Plugin Review: no raw style/script echoes).
 *
 * @package AuraBuilder
 */

declare(strict_types=1);

namespace AuraBuilder\Support;

final class Assets {

	/** Whether SSL asset forcing filter is attached. */
	private static bool $https_filter = false;

	/**
	 * Plugin asset URL with correct scheme (fixes http:// CSS blocked on https:// sites).
	 */
	public static function url( string $relative = '' ): string {
		$base = defined( 'AURA_BUILDER_URL' ) ? AURA_BUILDER_URL : plugin_dir_url( dirname( __DIR__ ) . '/aura-builder.php' );
		$url  = $relative !== '' ? $base . ltrim( $relative, '/' ) : $base;
		return self::force_https_url( $url );
	}

	/**
	 * Force https when the public site is served over SSL / reverse proxy.
	 */
	public static function force_https_url( string $url ): string {
		$need_https = is_ssl()
			|| ( ! empty( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https' ) // phpcs:ignore
			|| ( ! empty( $_SERVER['HTTP_X_FORWARDED_SSL'] ) && $_SERVER['HTTP_X_FORWARDED_SSL'] === 'on' ) // phpcs:ignore
			|| str_starts_with( (string) home_url( '/' ), 'https://' )
			|| str_starts_with( (string) site_url( '/' ), 'https://' );
		if ( $need_https ) {
			$url = set_url_scheme( $url, 'https' );
		}
		return $url;
	}

	/** Attach filters so every aura-builder style/script uses https on SSL sites. */
	public static function force_https_assets(): void {
		if ( self::$https_filter ) {
			return;
		}
		self::$https_filter = true;
		$fix = static function ( string $src, string $handle ): string {
			if ( $src === '' ) {
				return $src;
			}
			if (
				str_contains( $handle, 'aura' )
				|| str_contains( $handle, 'aptb' )
				|| str_contains( $src, 'aura-builder' )
				|| str_contains( $src, 'auranet' )
			) {
				return self::force_https_url( $src );
			}
			return $src;
		};
		add_filter( 'style_loader_src', $fix, 20, 2 );
		add_filter( 'script_loader_src', $fix, 20, 2 );
	}

	/**
	 * Register (once) a handle with no file and add inline CSS.
	 * Only plugin-generated CSS (StyleEngine tokens) should flow here — never user paste.
	 */
	public static function inline_style( string $handle, string $css, array $deps = array() ): void {
		$css = self::safe_css( $css );
		if ( $css === '' ) {
			return;
		}
		if ( ! wp_style_is( $handle, 'registered' ) && ! wp_style_is( $handle, 'enqueued' ) ) {
			wp_register_style( $handle, false, $deps, defined( 'AURA_BUILDER_VERSION' ) ? AURA_BUILDER_VERSION : null );
		}
		wp_enqueue_style( $handle );
		// Late escape: strip tags; CSS is generated from sanitized design tokens only.
		wp_add_inline_style( $handle, wp_strip_all_tags( $css ) );
	}

	/**
	 * Add inline JS after an existing (or empty) script handle.
	 * Only plugin-generated JS (e.g. wp_json_encode of sanitized data) should flow here.
	 */
	public static function inline_script( string $handle, string $js, string $position = 'after', array $deps = array() ): void {
		$js = self::safe_js( $js );
		if ( $js === '' ) {
			return;
		}
		if ( ! wp_script_is( $handle, 'registered' ) && ! wp_script_is( $handle, 'enqueued' ) ) {
			wp_register_script( $handle, false, $deps, defined( 'AURA_BUILDER_VERSION' ) ? AURA_BUILDER_VERSION : null, true );
		}
		wp_enqueue_script( $handle );
		wp_add_inline_script( $handle, $js, $position );
	}

	/**
	 * Sanitize CSS produced by the StyleEngine (never raw user paste).
	 */
	public static function safe_css( string $css ): string {
		$css = trim( wp_check_invalid_utf8( $css ) );
		if ( $css === '' ) {
			return '';
		}
		// No HTML/script breakout; no expression().
		$css = wp_strip_all_tags( $css );
		// Fix broken Google Fonts @import if someone used esc_url() (&#038; breaks CSS parse).
		$css = str_replace( '&#038;', '&', $css );
		$css = str_replace( '&amp;', '&', $css );
		// Allow only fonts.googleapis.com @import; strip other @import.
		$css = preg_replace_callback(
			'/@import\b[^;]+;/i',
			static function ( array $m ): string {
				$rule = $m[0];
				if ( stripos( $rule, 'fonts.googleapis.com' ) !== false ) {
					return $rule;
				}
				return '';
			},
			$css
		) ?? $css;
		$css = preg_replace( '/expression\s*\(/i', '', $css ) ?? $css;
		$css = preg_replace( '/javascript\s*:/i', '', $css ) ?? $css;
		$css = preg_replace( '/<\/style/i', '', $css ) ?? $css;
		return trim( $css );
	}

	/**
	 * Sanitize plugin-generated JS snippets (must already be trusted code + json).
	 */
	public static function safe_js( string $js ): string {
		$js = trim( wp_check_invalid_utf8( $js ) );
		if ( $js === '' ) {
			return '';
		}
		// Reject HTML injection into script context.
		if ( preg_match( '/<\/script/i', $js ) ) {
			return '';
		}
		return $js;
	}

	/**
	 * Broad allow-list for builder HTML (server-generated from sanitized document).
	 *
	 * @return array<string, array<string, bool>>
	 */
	public static function allowed_html(): array {
		// NOTE: WordPress wp_kses does NOT treat "data-*" as a wildcard.
		// Every data-* used by front-widgets.js must be listed explicitly
		// or gallery/carousel nav attributes are stripped and slides break.
		$global = array(
			'class'         => true,
			'id'            => true,
			'style'         => true,
			'role'          => true,
			'title'         => true,
			'aria-label'    => true,
			'aria-hidden'   => true,
			'aria-expanded' => true,
			'aria-controls' => true,
			'tabindex'      => true,
			// Gallery slider
			'data-ab-gallery-slider' => true,
			'data-ab-gal-prev'       => true,
			'data-ab-gal-next'       => true,
			'data-per'               => true,
			// Carousel
			'data-ab-carousel'       => true,
			'data-ab-car-prev'       => true,
			'data-ab-car-next'       => true,
			'data-ab-car-dot'        => true,
			'data-layout'            => true,
			'data-autoplay'          => true,
			'data-ab-img-carousel'   => true,
			// Tabs / accordion / toggle
			'data-ab-tabs'           => true,
			'data-tab'               => true,
			'data-ab-accordion'      => true,
			'data-ab-toggle'         => true,
			'data-multiple'          => true,
			// Nav / loop / fx / misc
			'data-ab-nav-menu'       => true,
			'data-ab-nav-toggle'     => true,
			'data-ab-loop-dot'       => true,
			'data-ab-fx'             => true,
			'data-ab-creative'       => true,
			'data-ab-cc-idx'         => true,
			'data-ab-cc-panel'       => true,
			'data-ab-cc-close'       => true,
			'data-ab-site-bg-carousel' => true,
			'data-ab-counter'        => true,
			'data-target'            => true,
			'data-node-id'           => true,
			'data-type'              => true,
			'data-field'             => true,
			'data-slide-index'       => true,
			'data-parent'            => true,
			'data-index'             => true,
		);
		$tags = array(
			'div', 'span', 'section', 'article', 'header', 'footer', 'nav', 'main', 'aside',
			'p', 'br', 'hr', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
			'ul', 'ol', 'li', 'a', 'img', 'figure', 'figcaption', 'picture', 'source',
			'strong', 'em', 'b', 'i', 'u', 's', 'small', 'sub', 'sup', 'mark',
			'button', 'form', 'input', 'label', 'textarea', 'select', 'option',
			'table', 'thead', 'tbody', 'tr', 'th', 'td', 'colgroup', 'col',
			'iframe', 'video', 'audio', 'svg', 'path', 'g', 'circle', 'rect', 'line', 'polyline', 'polygon',
			'time', 'blockquote', 'pre', 'code', 'details', 'summary',
		);
		$out = array();
		foreach ( $tags as $tag ) {
			$out[ $tag ] = $global;
		}
		$out['a']['href']                 = true;
		$out['a']['target']               = true;
		$out['a']['rel']                  = true;
		$out['img']['src']                = true;
		$out['img']['alt']                = true;
		$out['img']['width']              = true;
		$out['img']['height']             = true;
		$out['img']['loading']            = true;
		$out['img']['srcset']             = true;
		$out['img']['sizes']              = true;
		$out['iframe']['src']             = true;
		$out['iframe']['width']           = true;
		$out['iframe']['height']          = true;
		$out['iframe']['loading']         = true;
		$out['iframe']['referrerpolicy']  = true;
		$out['iframe']['allowfullscreen'] = true;
		$out['iframe']['title']           = true;
		$out['video']['src']              = true;
		$out['video']['controls']         = true;
		$out['video']['poster']           = true;
		$out['audio']['src']              = true;
		$out['audio']['controls']         = true;
		$out['source']['src']             = true;
		$out['source']['type']            = true;
		// Forms / comment_form() — attrs required for layout + native WP markup.
		$out['input']['type']             = true;
		$out['input']['name']             = true;
		$out['input']['value']            = true;
		$out['input']['placeholder']      = true;
		$out['input']['required']         = true;
		$out['input']['checked']          = true;
		$out['input']['disabled']         = true;
		$out['input']['readonly']         = true;
		$out['input']['maxlength']        = true;
		$out['input']['size']             = true;
		$out['input']['autocomplete']     = true;
		$out['textarea']['name']          = true;
		$out['textarea']['rows']          = true;
		$out['textarea']['cols']          = true;
		$out['textarea']['required']      = true;
		$out['textarea']['placeholder']   = true;
		$out['textarea']['maxlength']     = true;
		$out['label']['for']              = true;
		$out['button']['type']            = true;
		$out['button']['name']            = true;
		$out['button']['value']           = true;
		$out['button']['disabled']        = true;
		$out['form']['action']            = true;
		$out['form']['method']            = true;
		$out['form']['novalidate']        = true;
		$out['form']['enctype']           = true;
		$out['form']['name']              = true;
		$out['svg']['viewBox']            = true;
		$out['svg']['xmlns']              = true;
		$out['svg']['fill']               = true;
		$out['svg']['stroke']             = true;
		$out['svg']['width']              = true;
		$out['svg']['height']             = true;
		$out['path']['d']                 = true;
		$out['path']['fill']              = true;
		$out['path']['stroke']            = true;
		/**
		 * Filter allowed HTML for builder front output.
		 *
		 * @param array $out Allowed tags.
		 */
		return (array) apply_filters( 'aptb_allowed_html', $out );
	}

	/**
	 * Escape/sanitize builder HTML for front display.
	 *
	 * Important: wp_kses strips disallowed tags but keeps their text content.
	 * So raw <style>…</style> would become visible CSS on the page. We remove
	 * style/script blocks first; optional extracted CSS can be enqueued.
	 */
	public static function kses_builder( string $html, bool $enqueue_extracted_css = true ): string {
		if ( $html === '' ) {
			return '';
		}
		$extracted = self::extract_style_and_script_blocks( $html );
		$html      = $extracted['html'];
		// Prefer applying intentional CSS via WP style API (never as page text).
		if ( $enqueue_extracted_css && $extracted['css'] !== '' ) {
			self::inline_style(
				'aptb-extracted-' . substr( md5( $extracted['css'] ), 0, 12 ),
				$extracted['css'],
				array( 'aura-builder-front' )
			);
		}
		$html = wp_kses( $html, self::allowed_html() );
		// Last line of defense: leftover CSS blobs already stored as plain text
		// (e.g. older imports / previous kses passes that stripped tags only).
		return self::strip_orphaned_css_blobs( $html );
	}

	/**
	 * Remove <style> and <script> elements entirely (content included).
	 * Returns cleaned HTML + concatenated CSS from style blocks.
	 *
	 * @return array{html:string,css:string,scripts:string}
	 */
	public static function extract_style_and_script_blocks( string $html ): array {
		$css     = '';
		$scripts = '';
		// Style: capture inner CSS, drop the element so it never becomes text.
		$html = (string) preg_replace_callback(
			'#<style\b[^>]*>([\s\S]*?)</style>#i',
			static function ( array $m ) use ( &$css ): string {
				$css .= "\n" . $m[1];
				return '';
			},
			$html
		);
		// Script: drop entirely here (ContentFilter may re-attach trusted WP scripts separately).
		$html = (string) preg_replace_callback(
			'#<script\b[^>]*>[\s\S]*?</script>#i',
			static function ( array $m ) use ( &$scripts ): string {
				$scripts .= $m[0];
				return '';
			},
			$html
		);
		// Noscript with only style-like payload.
		$html = (string) preg_replace( '#<noscript\b[^>]*>[\s\S]*?</noscript>#i', '', $html );
		return array(
			'html'    => $html,
			'css'     => trim( $css ),
			'scripts' => $scripts,
		);
	}

	/**
	 * Detect and remove plain-text CSS rule dumps inside HTML
	 * (common after style tags were kses-stripped and saved).
	 */
	public static function strip_orphaned_css_blobs( string $html ): string {
		if ( $html === '' || ( ! str_contains( $html, '{' ) && ! str_contains( $html, 'font-family' ) ) ) {
			return $html;
		}
		// Remove whole elements whose only meaningful content is a CSS blob.
		$html = (string) preg_replace_callback(
			'#<(div|p|span|section|pre|code)\b([^>]*)>([\s\S]*?)</\1>#i',
			static function ( array $m ): string {
				$inner = $m[3];
				// Recurse into nested markup first via text-only check.
				$text = trim( html_entity_decode( wp_strip_all_tags( $inner ), ENT_QUOTES | ENT_HTML5, 'UTF-8' ) );
				if ( self::looks_like_css_blob( $text ) ) {
					return '';
				}
				// Also strip CSS-only text nodes mixed with empty tags.
				if ( self::looks_like_css_blob( trim( wp_strip_all_tags( $inner ) ) ) ) {
					return '';
				}
				return $m[0];
			},
			$html
		);
		// Standalone CSS dump with no wrapper (rare but possible).
		$plain = trim( wp_strip_all_tags( $html ) );
		if ( self::looks_like_css_blob( $plain ) && strlen( $plain ) > 80 ) {
			// Entire fragment is CSS — drop it.
			return '';
		}
		return $html;
	}

	/**
	 * Heuristic: string is almost entirely CSS rules (not normal prose).
	 */
	public static function looks_like_css_blob( string $text ): bool {
		$text = trim( $text );
		if ( $text === '' || strlen( $text ) < 40 ) {
			return false;
		}
		// Must have rule blocks.
		if ( ! preg_match( '/\{[^}]+\}/', $text ) ) {
			return false;
		}
		$open  = substr_count( $text, '{' );
		$close = substr_count( $text, '}' );
		if ( $open < 2 || $close < 2 ) {
			return false;
		}
		// Common CSS property density.
		$props = preg_match_all(
			'/\b(font-family|font-size|font-weight|line-height|letter-spacing|margin|padding|color|background|background-color|background-image|display|width|height|max-width|min-height|border|border-radius|position|top|left|right|bottom|flex|grid|text-align|box-shadow|z-index|opacity|overflow|transform|transition|justify-content|align-items|gap|object-fit|list-style|text-decoration|text-transform|white-space|cursor|content|box-sizing)\s*:/i',
			$text
		);
		if ( $props < 3 ) {
			return false;
		}
		// Reject normal prose that happens to mention a couple of properties.
		// CSS blobs have high punctuation density and few spaces relative to length.
		$letters = preg_match_all( '/[A-Za-zÀ-ÿ]/u', $text );
		$words   = preg_match_all( '/\b[A-Za-zÀ-ÿ]{3,}\b/u', $text );
		// Selectors + properties dominate; long natural sentences are rare.
		$sentence_words = preg_match_all( '/\b(the|and|para|com|uma|que|não|nao|você|voce|seu|sua|este|esta|de|do|da|em|no|na)\b/iu', $text );
		if ( $sentence_words > 8 && $props < 8 ) {
			return false;
		}
		// Brace pairs vs length: CSS is dense.
		$density = ( $open + $close + $props ) / max( 1, strlen( $text ) );
		return $density > 0.01 || ( $props >= 5 && $open >= 3 );
	}

	/**
	 * Sanitize user HTML widget content: never leave style/script as visible text.
	 *
	 * @param bool $enqueue_css When true (front render), apply extracted CSS via WP style API.
	 */
	public static function sanitize_user_html( string $code, bool $enqueue_css = false ): string {
		if ( $code === '' ) {
			return '';
		}
		$extracted = self::extract_style_and_script_blocks( $code );
		if ( $enqueue_css && $extracted['css'] !== '' ) {
			self::inline_style(
				'aptb-html-widget-' . substr( md5( $extracted['css'] ), 0, 12 ),
				$extracted['css'],
				array( 'aura-builder-front' )
			);
		}
		$code  = $extracted['html'];
		$plain = trim( html_entity_decode( wp_strip_all_tags( $code ), ENT_QUOTES | ENT_HTML5, 'UTF-8' ) );
		// Pure CSS dump (import leftover already stripped of <style>) → drop.
		if ( self::looks_like_css_blob( $plain ) ) {
			return '';
		}
		$code = wp_kses_post( $code );
		return self::strip_orphaned_css_blobs( $code );
	}
}
