<?php
/**
 * Blocks third-party scripts and iframes until the visitor consents.
 *
 * @package BlaminhorEssentials
 */

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

/**
 * Two-pronged interception:
 *
 *   1. wp_enqueue → we tag `<script src>` of known services with
 *      `type="text/plain" data-cookieconsent="<category>"`. Browsers don't
 *      execute text/plain scripts; our JS runtime swaps the type back to
 *      text/javascript once the visitor consents to that category.
 *
 *   2. Inline scripts (GTM/FB Pixel pasted in header.php, "Insert Headers
 *      and Footers" plugins, theme options, etc.) are captured via output
 *      buffering on `template_redirect` and the same data-cookieconsent
 *      attribute is injected on any script matching a known pattern.
 *
 * iframes (YouTube, Vimeo, Maps...) are converted to a placeholder element
 * that the runtime upgrades to a real iframe on per-service consent.
 */
class Blaminhor_Essentials_Cookies_Script_Blocker {

	/**
	 * @var Blaminhor_Essentials_Cookies_Service_Templates
	 */
	protected $templates;

	/**
	 * @var Blaminhor_Essentials_Cookies_Consent_Manager
	 */
	protected $consent;

	/**
	 * @var array Settings of the cookies module.
	 */
	protected $settings;

	public function __construct(
		Blaminhor_Essentials_Cookies_Service_Templates $templates,
		Blaminhor_Essentials_Cookies_Consent_Manager $consent,
		array $settings
	) {
		$this->templates = $templates;
		$this->consent   = $consent;
		$this->settings  = $settings;
	}

	/**
	 * Register the interception hooks. Should only run on the public front.
	 */
	public function register_hooks() {
		// 1) Filter enqueued <script src> tags.
		add_filter( 'script_loader_tag', array( $this, 'maybe_block_enqueued_script' ), 10, 3 );

		// 2) Output buffering for inline scripts and iframes.
		if ( ! empty( $this->settings['intercept_inline'] ) ) {
			add_action( 'template_redirect', array( $this, 'start_output_buffer' ), 0 );
		}
	}

	/**
	 * Block enqueued scripts pointing to a known service.
	 *
	 * @param string $tag    The full <script> tag HTML.
	 * @param string $handle WP enqueue handle.
	 * @param string $src    Resolved src URL.
	 * @return string
	 */
	public function maybe_block_enqueued_script( $tag, $handle, $src ) {
		// Never block scripts owned by this plugin. The runtime banner and
		// its inline JSON config island may legitimately reference third-party
		// service patterns (regexes) — blocking them breaks the banner itself.
		if ( 0 === strpos( (string) $handle, 'blaminhor-cookies-' )
			|| 0 === strpos( (string) $handle, 'blaminhor-essentials' ) ) {
			return $tag;
		}

		$category = $this->resolve_category_for_url( $src );
		if ( ! $category ) {
			return $tag;
		}

		// Apply once: skip if already tagged.
		if ( false !== strpos( $tag, 'data-cookieconsent=' ) ) {
			return $tag;
		}

		// If the visitor already consented to this category, leave the tag alone.
		if ( $this->category_consented( $category ) ) {
			return $tag;
		}

		// Swap type and add data-cookieconsent so our runtime can swap it back.
		$tag = preg_replace(
			'/<script(\s)/',
			'<script type="text/plain" data-cookieconsent="' . esc_attr( $category ) . '"$1',
			$tag,
			1
		);

		// Strip pre-existing type="...".
		$tag = preg_replace( '/(\s)type="text\/javascript"/', '', $tag );

		return $tag;
	}

	/**
	 * Start capturing the HTML body for inline-script post-processing.
	 */
	public function start_output_buffer() {
		ob_start( array( $this, 'rewrite_buffered_html' ) );
	}

	/**
	 * Rewriter applied to the buffered HTML.
	 *
	 * @param string $html Buffered HTML.
	 * @return string
	 */
	public function rewrite_buffered_html( $html ) {
		if ( '' === $html ) {
			return $html;
		}

		// Pass 1: inline + src scripts not already tagged.
		$html = $this->patch_inline_scripts( $html );

		// Pass 2: iframes for known services.
		$html = $this->patch_iframes( $html );

		return $html;
	}

	/**
	 * Inject data-cookieconsent on inline / src scripts matching a service pattern.
	 *
	 * @param string $html Buffered HTML.
	 * @return string
	 */
	protected function patch_inline_scripts( $html ) {
		return preg_replace_callback(
			'#<script\b([^>]*)>(.*?)</script>#is',
			function ( $m ) {
				$attrs   = $m[1];
				$body    = $m[2];
				$context = $attrs . ' ' . $body;

				if ( false !== strpos( $attrs, 'data-cookieconsent=' ) ) {
					return $m[0];
				}

				// Never tag scripts that belong to this plugin — their body
				// may legitimately reference third-party service patterns.
				if ( preg_match( '/\bid=("|\')(blaminhor-cookies-|blaminhor-essentials)/', $attrs ) ) {
					return $m[0];
				}

				$category = $this->resolve_category_for_blob( $context );
				if ( ! $category ) {
					return $m[0];
				}

				if ( $this->category_consented( $category ) ) {
					return $m[0];
				}

				$attrs = preg_replace( '/\s*type=("|\')text\/javascript("|\')/i', '', $attrs );
				$attrs = trim( $attrs );

				return '<script type="text/plain" data-cookieconsent="' . esc_attr( $category ) . '"' . ( '' !== $attrs ? ' ' . $attrs : '' ) . '>' . $body . '</script>';
			},
			$html
		);
	}

	/**
	 * Replace known iframes by a placeholder element awaiting per-service consent.
	 *
	 * @param string $html Buffered HTML.
	 * @return string
	 */
	protected function patch_iframes( $html ) {
		return preg_replace_callback(
			'#<iframe\b([^>]*)></iframe>|<iframe\b([^>]*?)/>#i',
			function ( $m ) {
				// Regex has two alternatives; only one group is captured per match.
				$attrs = ! empty( $m[1] ) ? $m[1] : ( $m[2] ?? '' );
				$src   = '';
				if ( '' !== $attrs && preg_match( '/\bsrc=("|\')([^"\']+)\1/i', $attrs, $sm ) ) {
					$src = $sm[2];
				}
				if ( '' === $src ) {
					return $m[0];
				}

				$service = $this->resolve_service_for_url( $src, 'iframe_patterns' );
				if ( ! $service ) {
					return $m[0];
				}

				if ( $this->category_consented( $service['category_default'] ) ) {
					return $m[0];
				}

				return $this->render_iframe_placeholder( $service, $src, $attrs );
			},
			$html
		);
	}

	/**
	 * Produce the HTML for a click-to-load placeholder.
	 *
	 * @param array  $service Service template.
	 * @param string $src     Original iframe src.
	 * @param string $attrs   Original iframe attributes.
	 * @return string
	 */
	protected function render_iframe_placeholder( array $service, $src, $attrs ) {
		// Preserve width/height for proper layout reservation.
		$width  = '';
		$height = '';
		if ( preg_match( '/\bwidth=("|\')([^"\']+)\1/i', $attrs, $w ) ) {
			$width = $w[2];
		}
		if ( preg_match( '/\bheight=("|\')([^"\']+)\1/i', $attrs, $h ) ) {
			$height = $h[2];
		}

		return sprintf(
			'<div class="blaminhor-cookies-iframe-placeholder" data-service="%1$s" data-src="%2$s" data-category="%3$s" data-width="%4$s" data-height="%5$s" data-provider="%6$s"></div>',
			esc_attr( $service['handle'] ),
			esc_attr( $src ),
			esc_attr( $service['category_default'] ),
			esc_attr( $width ),
			esc_attr( $height ),
			esc_attr( $service['provider'] )
		);
	}

	/**
	 * Return the consent category required for a URL, or '' if none.
	 *
	 * @param string $url URL of a script.
	 * @return string
	 */
	protected function resolve_category_for_url( $url ) {
		$svc = $this->resolve_service_for_url( $url, 'script_patterns' );
		return $svc ? $svc['category_default'] : '';
	}

	/**
	 * Return the consent category for a blob of script attrs+body.
	 *
	 * @param string $blob String to search patterns in.
	 * @return string
	 */
	protected function resolve_category_for_blob( $blob ) {
		foreach ( $this->templates->all() as $svc ) {
			foreach ( $svc['script_patterns'] as $pattern ) {
				if ( '' === $pattern ) {
					continue;
				}
				if ( preg_match( '#' . str_replace( '#', '\#', $pattern ) . '#i', $blob ) ) {
					return $svc['category_default'];
				}
			}
		}
		return '';
	}

	/**
	 * Find which service template matches the given URL, if any.
	 *
	 * @param string $url     URL to test.
	 * @param string $pat_key Pattern field to use (script_patterns or iframe_patterns).
	 * @return array|null
	 */
	protected function resolve_service_for_url( $url, $pat_key ) {
		foreach ( $this->templates->all() as $svc ) {
			if ( empty( $svc[ $pat_key ] ) ) {
				continue;
			}
			foreach ( $svc[ $pat_key ] as $pattern ) {
				if ( '' === $pattern ) {
					continue;
				}
				if ( preg_match( '#' . str_replace( '#', '\#', $pattern ) . '#i', $url ) ) {
					return $svc;
				}
			}
		}
		return null;
	}

	/**
	 * Whether the visitor has already consented to this category.
	 *
	 * @param string $category Consent category.
	 * @return bool
	 */
	protected function category_consented( $category ) {
		$payload = $this->consent->read_cookie();
		if ( ! $payload ) {
			return false;
		}
		return ! empty( $payload['choices'][ $category ] );
	}
}
