<?php
/**
 * Scan-Detected Scripts Merger.
 *
 * Merges scan-detected third-party resources into the known scripts
 * database via the surecookie_known_scripts filter.
 *
 * @package SureCookie\Inc\Modules\ScriptBlocking
 * @since 0.0.0-alpha.2
 */

namespace SureCookie\Inc\Modules\ScriptBlocking;

use SureCookie\Inc\Functions\Settings;
use SureCookie\Inc\Traits\GetInstance;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * Scan_Scripts
 *
 * Hooks into the known_scripts filter to merge scan-detected resources.
 *
 * @since 0.0.0-alpha.2
 */
class Scan_Scripts {
	use GetInstance;

	/**
	 * Cached scanned resources data.
	 *
	 * @var array<string, mixed>|null
	 */
	private static ?array $cached_resources = null;

	/**
	 * Cached excluded domains list (avoids repeated DB reads on every script/iframe).
	 *
	 * @var array<int, string>|null
	 */
	private static ?array $cached_excluded = null;

	/**
	 * Constructor.
	 *
	 * @since 0.0.0-alpha.2
	 */
	private function __construct() {
		add_filter( 'surecookie_known_scripts', [ $this, 'merge_scan_detected_resources' ], 20 );

		// Skip blocking for scripts/iframes whose src matches an excluded domain.
		add_filter( 'surecookie_skip_script', [ $this, 'should_skip_excluded_resource' ], 10, 2 );
		add_filter( 'surecookie_skip_iframe', [ $this, 'should_skip_excluded_resource' ], 10, 2 );
	}

	/**
	 * Merge scan-detected resources into the known scripts dataset.
	 *
	 * @param array<string, array<string, mixed>> $scripts Known scripts by category.
	 * @since 0.0.0-alpha.2
	 * @return array<string, array<string, mixed>> Merged scripts.
	 */
	public function merge_scan_detected_resources( array $scripts ): array {
		$resources = $this->get_scanned_resources();

		if ( empty( $resources ) ) {
			return $scripts;
		}

		$excluded_domains = $this->get_excluded_domains();

		// Build a flat list of all existing patterns to avoid duplicates.
		$existing_patterns = $this->build_existing_pattern_index( $scripts );

		// Merge scan-detected scripts.
		foreach ( $resources['scripts'] ?? [] as $resource ) {
			$this->merge_resource( $scripts, $resource, 'scripts', $excluded_domains, $existing_patterns );
		}

		// Merge scan-detected iframes.
		foreach ( $resources['iframes'] ?? [] as $resource ) {
			$this->merge_resource( $scripts, $resource, 'iframes', $excluded_domains, $existing_patterns );
		}

		return $scripts;
	}

	/**
	 * Clear the static cache (useful after scan results are updated).
	 *
	 * @since 0.0.0-alpha.2
	 * @return void
	 */
	public static function clear_cache(): void {
		self::$cached_resources = null;
		self::$cached_excluded  = null;
	}

	/**
	 * Filter callback: skip blocking when the resource src matches an excluded domain.
	 *
	 * Hooked into surecookie_skip_script and surecookie_skip_iframe so the
	 * per-resource "Block" toggle in the admin UI works for both scan-detected
	 * AND known-scripts entries.
	 *
	 * @since 0.0.0-alpha.2
	 * @param bool   $skip Whether the resource is already marked to skip.
	 * @param string $src  The resource URL (script src or iframe src).
	 * @return bool
	 */
	public function should_skip_excluded_resource( bool $skip, string $src ): bool {
		if ( $skip || empty( $src ) ) {
			return $skip;
		}

		$excluded_domains = $this->get_excluded_domains_cached();

		foreach ( $excluded_domains as $domain ) {
			if ( strpos( $src, $domain ) !== false ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Merge a single resource into the scripts array.
	 *
	 * @param array<mixed>         $scripts          Known scripts (by reference).
	 * @param array<string, mixed> $resource         Scan-detected resource.
	 * @param string               $type             Resource type ('scripts' or 'iframes').
	 * @param array<int, string>   $excluded_domains Domains excluded from blocking.
	 * @param array<string, bool>  $existing_patterns  Index of existing patterns.
	 * @since 0.0.0-alpha.2
	 * @return void
	 */
	private function merge_resource( array &$scripts, array $resource, string $type, array $excluded_domains, array $existing_patterns ): void {
		$domain   = $resource['domain'] ?? '';
		$category = $resource['category'] ?? 'marketing';

		if ( empty( $domain ) ) {
			return;
		}

		// Skip if excluded by admin.
		if ( in_array( $domain, $excluded_domains, true ) ) {
			return;
		}

		// Skip if this domain already exists in known-scripts patterns.
		if ( isset( $existing_patterns[ $domain ] ) ) {
			return;
		}

		// Ensure the category exists.
		if ( ! isset( $scripts[ $category ] ) ) {
			$scripts[ $category ] = [];
		}

		// Build a unique service key from the domain.
		$service_key = 'scan_' . str_replace( [ '.', '-' ], '_', $domain );

		// Add to the appropriate type array.
		$entry = [
			'label' => $resource['vendor'] ?? $domain,
		];

		if ( $type === 'iframes' ) {
			$entry['iframes'] = [ $domain ];
		} else {
			$entry['scripts'] = [ $domain ];
		}

		$scripts[ $category ][ $service_key ] = $entry;
	}

	/**
	 * Build an index of existing patterns for fast duplicate checks.
	 *
	 * @param array<string, array<string, mixed>> $scripts Known scripts by category.
	 * @since 0.0.0-alpha.2
	 * @return array<string, bool>
	 */
	private function build_existing_pattern_index( array $scripts ): array {
		$index = [];

		foreach ( $scripts as $services ) {
			if ( ! is_array( $services ) ) {
				continue;
			}

			foreach ( $services as $service ) {
				foreach ( $service['scripts'] ?? [] as $pattern ) {
					$index[ $pattern ] = true;
				}
				foreach ( $service['iframes'] ?? [] as $pattern ) {
					$index[ $pattern ] = true;
				}
			}
		}

		return $index;
	}

	/**
	 * Get excluded domains with static cache (called once per page load, then reused).
	 *
	 * @since 0.0.0-alpha.2
	 * @return array<int, string>
	 */
	private function get_excluded_domains_cached(): array {
		if ( self::$cached_excluded !== null ) {
			return self::$cached_excluded;
		}

		self::$cached_excluded = $this->get_excluded_domains();
		return self::$cached_excluded;
	}

	/**
	 * Get scanned resources from the database (with static cache).
	 *
	 * @since 0.0.0-alpha.2
	 * @return array<string, mixed>
	 */
	private function get_scanned_resources(): array {
		if ( self::$cached_resources !== null ) {
			return self::$cached_resources;
		}

		$resources = get_option( SURECOOKIE_SCANNED_RESOURCES_OPTION, [] );

		if ( ! is_array( $resources ) ) {
			$resources = [];
		}

		self::$cached_resources = $resources;

		return self::$cached_resources;
	}

	/**
	 * Get domains excluded from scan-based blocking.
	 *
	 * @since 0.0.0-alpha.2
	 * @return array<int, string>
	 */
	private function get_excluded_domains(): array {
		$excluded = Settings::get( 'excluded_scan_resources' );

		if ( ! is_array( $excluded ) ) {
			return [];
		}

		return $excluded;
	}
}
