<?php
/**
 * Foreign tracking-tag / container detection.
 *
 * @package Carticy\Conversions\Diagnostics
 */

declare(strict_types=1);

namespace Carticy\Conversions\Diagnostics;

use Carticy\Conversions\Client\ClientTrackingSettings;

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

/**
 * Detects GA4 / Google Ads / Meta Pixel tags and GTM containers that exist on
 * the store OUTSIDE this plugin, and recommends a safe action (attach to an
 * existing container, replace a duplicate, or keep both).
 *
 * Detection uses signals the server can see honestly: the registered/enqueued
 * script graph plus an optional browser probe for dataLayer and known globals.
 * It deliberately avoids loopback-rendering the storefront, which is slow and
 * often blocked by local, auth, firewall, or cache configuration.
 */
final class ExistingTagScanner {

	/**
	 * Configured client tracking (to mark our own ids as not-foreign).
	 *
	 * @var ClientTrackingSettings
	 */
	private ClientTrackingSettings $settings;

	/**
	 * Constructor.
	 *
	 * @param ClientTrackingSettings $settings Client tracking settings.
	 */
	public function __construct( ClientTrackingSettings $settings ) {
		$this->settings = $settings;
	}

	/**
	 * Scan for foreign tags and recommend a safe action.
	 *
	 * @param array<int, array<string, mixed>> $probe Optional browser-detected tags.
	 * @return array<string, mixed>
	 */
	public function scan( array $probe = array() ): array {
		$owned = $this->owned_ids();
		$found = array();

		foreach ( $this->scan_scripts() as $item ) {
			$found[] = $this->finalize_item( $item, $owned );
		}

		foreach ( $probe as $item ) {
			if ( ! is_array( $item ) ) {
				continue;
			}
			$found[] = $this->finalize_item(
				array(
					'type' => sanitize_key( (string) ( $item['type'] ?? '' ) ),
					'id'   => sanitize_text_field( (string) ( $item['id'] ?? '' ) ),
					'via'  => sanitize_key( (string) ( $item['via'] ?? 'browser_probe' ) ),
				),
				$owned
			);
		}

		$found = $this->dedupe( $found );

		return array(
			'found'          => $found,
			'recommendation' => $this->recommend( $found ),
		);
	}

	/**
	 * Detect tracking scripts from the registered/enqueued script graph.
	 *
	 * @return array<int, array<string, string>>
	 */
	private function scan_scripts(): array {
		if ( ! function_exists( 'wp_scripts' ) ) {
			return array();
		}

		$scripts = wp_scripts();
		if ( ! is_object( $scripts ) || ! isset( $scripts->registered ) || ! is_array( $scripts->registered ) ) {
			return array();
		}

		$found = array();
		foreach ( $scripts->registered as $handle => $script ) {
			$src = is_object( $script ) && isset( $script->src ) ? (string) $script->src : '';
			if ( '' === $src ) {
				continue;
			}

			$detected = $this->classify_src( $src );
			if ( null !== $detected ) {
				$detected['via'] = 'script:' . sanitize_key( (string) $handle );
				$found[]         = $detected;
			}
		}

		return $found;
	}

	/**
	 * Classify a script src into a tracking type + id when recognizable.
	 *
	 * @param string $src Script source URL.
	 * @return array<string, string>|null
	 */
	private function classify_src( string $src ): ?array {
		if ( false !== strpos( $src, 'googletagmanager.com/gtm.js' ) ) {
			$id = $this->query_id( $src, 'id' );

			// A real GTM/Google tag always carries its id in the query string; a
			// src without one is not a usable tag and must not become a spurious
			// "duplicate" that recommends replacing a configured platform.
			return '' === $id ? null : array(
				'type' => 'gtm',
				'id'   => $id,
			);
		}

		if ( false !== strpos( $src, 'googletagmanager.com/gtag/js' ) ) {
			$id = $this->query_id( $src, 'id' );
			if ( '' === $id ) {
				return null;
			}

			return array(
				'type' => 0 === strpos( $id, 'AW-' ) ? 'google_ads' : 'ga4',
				'id'   => $id,
			);
		}

		if ( false !== strpos( $src, 'connect.facebook.net' ) && false !== strpos( $src, 'fbevents' ) ) {
			return array(
				'type' => 'meta',
				'id'   => '',
			);
		}

		return null;
	}

	/**
	 * Extract a query argument from a URL.
	 *
	 * @param string $src URL.
	 * @param string $key Query key.
	 * @return string
	 */
	private function query_id( string $src, string $key ): string {
		$query = (string) wp_parse_url( $src, PHP_URL_QUERY );
		if ( '' === $query ) {
			return '';
		}

		parse_str( $query, $args );

		return strtoupper( sanitize_text_field( (string) ( $args[ $key ] ?? '' ) ) );
	}

	/**
	 * Normalize a detected item and flag whether the plugin owns its id.
	 *
	 * @param array<string, string>             $item Detected item.
	 * @param array<string, array<int, string>> $owned Owned ids by type.
	 * @return array<string, mixed>
	 */
	private function finalize_item( array $item, array $owned ): array {
		$type = (string) ( $item['type'] ?? '' );
		$id   = strtoupper( trim( (string) ( $item['id'] ?? '' ) ) );

		$owned_for_type = $owned[ $type ] ?? array();
		$owned_by_us    = '' !== $id && in_array( $id, $owned_for_type, true );

		return array(
			'type'          => $type,
			'id'            => $id,
			'via'           => (string) ( $item['via'] ?? '' ),
			'ownedByPlugin' => $owned_by_us,
		);
	}

	/**
	 * Collapse duplicate detections (same type + id + via).
	 *
	 * @param array<int, array<string, mixed>> $found Detected items.
	 * @return array<int, array<string, mixed>>
	 */
	private function dedupe( array $found ): array {
		$seen   = array();
		$unique = array();
		foreach ( $found as $item ) {
			$key = $item['type'] . '|' . $item['id'] . '|' . $item['via'];
			if ( isset( $seen[ $key ] ) ) {
				continue;
			}
			$seen[ $key ] = true;
			$unique[]     = $item;
		}

		return $unique;
	}

	/**
	 * Recommend a safe resolution from what was found.
	 *
	 * @param array<int, array<string, mixed>> $found Detected items.
	 * @return array<string, string>
	 */
	private function recommend( array $found ): array {
		$foreign = array_values(
			array_filter(
				$found,
				static function ( array $item ): bool {
					return empty( $item['ownedByPlugin'] );
				}
			)
		);

		if ( array() === $foreign ) {
			return array(
				'action' => 'none',
				'reason' => 'no_foreign_tags_detected',
			);
		}

		// A foreign tag for a platform we ALSO fire risks a double count and
		// cannot be silently deduplicated (different keys) - recommend replace.
		foreach ( $foreign as $item ) {
			if ( $this->settings->provider_enabled( $this->provider_key( (string) $item['type'] ) ) ) {
				return array(
					'action' => 'replace',
					'reason' => 'foreign_duplicate_' . (string) $item['type'],
				);
			}
		}

		// A foreign GTM container is the natural attach point when we use GTM.
		$gtm_enabled = ! empty( $this->settings->to_array()['gtm']['enabled'] );
		foreach ( $foreign as $item ) {
			if ( 'gtm' === $item['type'] && $gtm_enabled ) {
				return array(
					'action' => 'attach',
					'reason' => 'foreign_gtm_container',
				);
			}
		}

		return array(
			'action' => 'keep_both',
			'reason' => 'foreign_tags_no_overlap',
		);
	}

	/**
	 * Map a detected type to the client settings provider key.
	 *
	 * @param string $type Detected type.
	 * @return string
	 */
	private function provider_key( string $type ): string {
		return in_array( $type, array( 'ga4', 'google_ads', 'meta' ), true ) ? $type : '';
	}

	/**
	 * Collect the plugin's own configured ids by type.
	 *
	 * @return array<string, array<int, string>>
	 */
	private function owned_ids(): array {
		$config = $this->settings->to_array();
		$ga4    = strtoupper( (string) ( $config['providers']['ga4']['measurementId'] ?? '' ) );
		$ads    = strtoupper( (string) ( $config['providers']['google_ads']['conversionId'] ?? '' ) );
		$meta   = (string) ( $config['providers']['meta']['pixelId'] ?? '' );
		$gtm    = strtoupper( (string) ( $config['gtm']['containerId'] ?? '' ) );

		return array(
			'ga4'        => '' !== $ga4 ? array( $ga4 ) : array(),
			'google_ads' => '' !== $ads ? array( $ads ) : array(),
			'meta'       => '' !== $meta ? array( strtoupper( $meta ) ) : array(),
			'gtm'        => '' !== $gtm ? array( $gtm ) : array(),
		);
	}
}
