<?php
/**
 * BaseCloud Boost — Speculation Rules API
 *
 * Injects the Chrome Speculation Rules API to enable instant page navigation
 * (prefetch or prerender) for links on the current page.
 *
 * Supported strategies:
 *  - prefetch:  Downloads HTML + subresources for linked pages in background.
 *  - prerender: Fully renders linked pages in a hidden tab — instant navigation.
 *
 * Browser support (as of early 2025): Chrome 109+, Edge 109+.
 * Other browsers silently ignore the <script type="speculationrules"> tag.
 *
 * Eagerness levels:
 *  - "conservative" — speculate on links the user has clicked (safest)
 *  - "moderate"     — speculate on links in viewport
 *  - "eager"        — speculate on all eligible links (most aggressive)
 *
 * Excludes:
 *  - wp-login, wp-admin, cart, checkout, my-account
 *  - URLs matching custom exclude patterns from settings
 *
 * @package BaseCloud\Boost\Optimizer
 */

declare( strict_types=1 );

namespace BaseCloud\Boost\Optimizer;

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

/**
 * Injects Speculation Rules for instant navigation.
 */
class Speculation_Rules {

	/**
	 * Boot Speculation Rules module.
	 *
	 * @return void
	 */
	public function boot(): void {
		$mode = (string) BCBOOST_get_option( 'speculation_rules', 'off' );

		if ( $mode === 'off' || $mode === '' ) {
			return;
		}

		if ( is_admin() ) {
			return;
		}

		add_action( 'wp_footer', [ $this, 'print_speculation_rules' ], 1 );
	}

	/**
	 * Output the speculationrules script tag.
	 *
	 * @return void
	 */
	public function print_speculation_rules(): void {
		if ( is_feed() || is_search() || is_404() ) {
			return;
		}

		$mode      = (string) BCBOOST_get_option( 'speculation_rules', 'prefetch' );
		$eagerness = (string) BCBOOST_get_option( 'speculation_eagerness', 'moderate' );

		if ( ! in_array( $mode, [ 'prefetch', 'prerender' ], true ) ) {
			return;
		}

		if ( ! in_array( $eagerness, [ 'conservative', 'moderate', 'eager' ], true ) ) {
			$eagerness = 'moderate';
		}

		// Build href exclusion list.
		$excludes = $this->build_excludes();

		$rules = [
			$mode => [
				[
					'source'    => 'document',
					'eagerness' => $eagerness,
					'where'     => [
						'and' => array_merge(
							[
								[ 'href_matches' => '/*' ], // Only same-origin links.
								// Never speculate on rel="nofollow" links — WooCommerce
								// add-to-cart buttons and other state-changing actions are
								// marked nofollow. Prefetching/prerendering one would fire
								// the action and add a "ghost product" to the cart.
								[ 'not' => [ 'selector_matches' => 'a[rel~="nofollow"]' ] ],
							],
							$this->build_exclude_conditions( $excludes )
						),
					],
				],
			],
		];

		$json = wp_json_encode( $rules, JSON_UNESCAPED_SLASHES );

		wp_print_inline_script_tag( $json, [ 'type' => 'speculationrules' ] );
	}

	// ─── Private ──────────────────────────────────────────────────────────────

	/**
	 * Build the list of URL patterns to exclude from speculation.
	 *
	 * @return string[]
	 */
	private function build_excludes(): array {
		$always = [
			'/wp-login*',
			'/wp-admin*',
			'/wp-cron*',
			'/xmlrpc*',
			'/checkout*',
			'/cart*',
			'/my-account*',
			'/feed*',
			// State-changing query actions (WooCommerce add/remove from cart, AJAX,
			// nonces). The `\?` matches the query-string separator so these patterns
			// catch the action on any path, e.g. /shop/?add-to-cart=123.
			'/*\\?*add-to-cart=*',
			'/*\\?*remove_item=*',
			'/*\\?*undo_item=*',
			'/*\\?*wc-ajax=*',
			'/*\\?*add_to_wishlist=*',
			'/*\\?*_wpnonce=*',
		];

		// Add custom user-defined exclude patterns.
		$custom = array_filter( array_map( 'trim', explode( "\n", (string) BCBOOST_get_option( 'exclude_urls', '' ) ) ) );
		foreach ( $custom as $pattern ) {
			if ( $pattern !== '' ) {
				// Convert to glob-like href_matches pattern.
				$always[] = '*' . ltrim( $pattern, '*' );
			}
		}

		return $always;
	}

	/**
	 * Convert exclude patterns to Speculation Rules "not" conditions.
	 *
	 * @param string[] $excludes URL patterns.
	 * @return array<int, array>
	 */
	private function build_exclude_conditions( array $excludes ): array {
		$conditions = [];
		foreach ( $excludes as $pattern ) {
			$conditions[] = [
				'not' => [ 'href_matches' => $pattern ],
			];
		}
		return $conditions;
	}
}
