<?php
defined( 'ABSPATH' ) || exit;

/**
 * Frontend: enqueues the capture script and passes config.
 */
class BW_Lead_AI_Frontend {

	private static $instance = null;

	public static function instance() {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	public function register() {
		add_action( 'wp_enqueue_scripts', array( $this, 'enqueue' ) );
		add_action( 'wp_head', array( $this, 'hide_tracking_fields' ), 100 );
	}

	/**
	 * Is the current visitor staff, whose browsing is not lead behaviour?
	 *
	 * `edit_posts` is the line: anyone who can edit content is working on the site, not
	 * shopping it. Editors, authors and contributors are all in; subscribers and
	 * customers — who are frequently the actual leads on a membership or shop site —
	 * are all out, which is why this is not `is_user_logged_in()`.
	 *
	 * The cost of not doing this is not theoretical. On a real dataset the single most
	 * engaged "lead" in the whole table was the site's own staff account: 111 visits,
	 * with a wp-admin page sitting in the middle of its journey. It topped every
	 * engagement sort, and a marketer reading that page is being shown their own
	 * behaviour presented as their best prospect's. A handful of staff browsing daily
	 * also outweighs hundreds of real visitors in any per-visitor average.
	 *
	 * Capture, not derivation: this changes who is recorded from now on and rewrites
	 * nothing already stored. Journeys captured before it stay exactly as they were, so
	 * it is not a Reprocess CHANGES entry — there is no old record it could improve.
	 *
	 * One consequence worth knowing rather than discovering: a staff member submitting
	 * one of the site's own forms now posts the literal `{bw:...}` text in any tracking
	 * field, because nothing is loaded to substitute it. That is the honest outcome —
	 * the alternative is a fabricated journey — and it is obviously wrong in the entry
	 * rather than quietly plausible in the reports.
	 */
	private function is_staff() {
		return is_user_logged_in() && current_user_can( 'edit_posts' );
	}

	public function enqueue() {
		if ( is_admin() || $this->is_staff() ) {
			return;
		}
		wp_register_script(
			'bw-lead-ai-capture',
			BW_LEAD_AI_URL . 'assets/js/capture.js',
			array(),
			BW_LEAD_AI_VERSION,
			true
		);
		wp_localize_script( 'bw-lead-ai-capture', 'bwLeadAI', $this->client_config() );
		wp_enqueue_script( 'bw-lead-ai-capture' );

		// The interaction-event engine is a separate file loaded only when at
		// least one event type is switched on, so sites that don't use it pay
		// nothing — no extra request, no listeners, no third-party video APIs.
		$settings = BW_Lead_AI_Settings::get();
		if ( ! empty( BW_Lead_AI_Settings::parse_event_types( $settings['event_types'] ) ) ) {
			wp_enqueue_script(
				'bw-lead-ai-events',
				BW_LEAD_AI_URL . 'assets/js/events.js',
				array( 'bw-lead-ai-capture' ),
				BW_LEAD_AI_VERSION,
				true
			);
		}

		// handoff.js is the piece that stores a journey server-side — it mints
		// the token, stamps forms, and sends the update beacon. Cross-domain
		// link decoration is one of its jobs, not its gate: a site saving its
		// own submissions needs it exactly as a handoff site does. Sites storing
		// nothing (explicit confirm-only with no destination) still skip it.
		if ( BW_Lead_AI_Settings::journey_storage_enabled( $settings ) ) {
			wp_enqueue_script(
				'bw-lead-ai-handoff',
				BW_LEAD_AI_URL . 'assets/js/handoff.js',
				array( 'bw-lead-ai-capture' ),
				BW_LEAD_AI_VERSION,
				true
			);
		}
	}

	public function client_config() {
		$settings = BW_Lead_AI_Settings::get();

		$targets = array();
		foreach ( $settings['field_targets'] as $key => $row ) {
			$targets[ $key ] = array(
				'attr' => isset( $row['attr'] ) ? (string) $row['attr'] : '',
				'val'  => isset( $row['val'] ) ? (string) $row['val'] : '',
			);
		}

		$aliases = BW_Lead_AI_Settings::parse_parameter_aliases( $settings['parameter_aliases'] );

		return array(
			'version'               => BW_LEAD_AI_VERSION,
			'debug'                 => ! empty( $settings['debug'] ),
			'dimensions'            => $aliases['standard'],
			'customDimensions'      => $aliases['custom'],
			'clickIds'              => BW_Lead_AI_Settings::parse_click_ids( $settings['click_ids'] ),
			'channels'              => BW_Lead_AI_Settings::parse_channels( $settings['channels'] ),
			'referrerClasses'       => BW_Lead_AI_Settings::classification_rules( $settings ),
			// Hostname patterns from the `internal` row of Medium Mappings. This
			// site's own hostname is not among them and does not need to be — every
			// place capture.js consults the list already treats
			// window.location.hostname as ours on its own.
			'ownedHosts'            => BW_Lead_AI_Settings::owned_hosts( $settings ),
			'captureGaClientId'     => ! empty( $settings['capture_ga_client_id'] ),
			'targets'               => $targets,
			// Emitted whenever journeys are stored server-side at all. The
			// cross-domain-only pieces stay gated on a configured destination:
			// 'domains' (link decoration + token handoff) is empty without one,
			// so nothing is ever decorated toward a domain nobody configured.
			'handoff'               => BW_Lead_AI_Settings::journey_storage_enabled( $settings )
				? array(
					'endpoint'     => rest_url( BW_Lead_AI_Handoff_REST::NAMESPACE_V1 . '/handoff' ),
					'param'        => (string) $settings['handoff_param'],
					'tokenField'   => BW_Lead_AI_Identity::TOKEN_FIELD,
					// Tells handoff.js a Gravity Forms submission here saves the
					// journey — which makes a page with a GF form worth minting
					// a token for, and post-submission pageviews worth reporting.
					'saveOnSubmit' => 'submission' === BW_Lead_AI_Settings::journey_save_mode( $settings ),
					'beacon'       => BW_Lead_AI_Settings::journey_updates_enabled( $settings )
						? rest_url( BW_Lead_AI_Handoff_REST::NAMESPACE_V1 . '/journey/beacon' )
						: '',
					'domains'      => BW_Lead_AI_Settings::handoff_enabled( $settings )
						? BW_Lead_AI_Settings::parse_handoff_domains( $settings )
						: array(),
					'datapoints'   => BW_Lead_AI_Settings::parse_handoff_datapoints( $settings ),
				)
				: null,
			'events'                => array(
				'enabled'     => BW_Lead_AI_Settings::parse_event_types( $settings['event_types'] ),
				'downloads'   => BW_Lead_AI_Settings::parse_csv_list( $settings['event_downloads'] ),
				'socialHosts' => BW_Lead_AI_Settings::parse_csv_list( $settings['event_social_hosts'] ),
				'videoMarks'  => BW_Lead_AI_Settings::parse_percent_list( $settings['event_video_milestones'] ),
				'scrollMarks' => BW_Lead_AI_Settings::parse_percent_list( $settings['event_scroll_thresholds'] ),
				'custom'      => BW_Lead_AI_Settings::parse_custom_events( $settings['event_custom_selectors'] ),
			),
		);
	}

	public function hide_tracking_fields() {
		if ( is_admin() ) {
			return;
		}
		$settings = BW_Lead_AI_Settings::get();
		$show     = current_user_can( 'manage_options' ) && ! empty( $settings['debug'] );
		$display  = $show ? 'block' : 'none';

		$selectors = array();
		foreach ( $settings['field_targets'] as $row ) {
			if ( empty( $row['attr'] ) || empty( $row['val'] ) ) {
				continue;
			}
			$attr = $row['attr'];
			$val  = $row['val'];
			if ( 'id' === $attr ) {
				$selectors[] = '#' . $val;
			} elseif ( 'class' === $attr ) {
				$selectors[] = '.' . $val;
			} elseif ( 'name' === $attr ) {
				$selectors[] = '[name="' . $val . '"]';
			} elseif ( 'selector' === $attr ) {
				$selectors[] = $val;
			}
		}
		if ( empty( $selectors ) ) {
			return;
		}
		// Sanitize each selector before output.
		$safe = array();
		foreach ( $selectors as $sel ) {
			// Allow a permissive but CSS-only character set.
			$sel = preg_replace( '/[^a-zA-Z0-9_\-\.\#\[\]\=\"\' :\(\),\*>~\+]/', '', $sel );
			if ( '' !== $sel ) {
				$safe[] = $sel;
			}
		}
		if ( empty( $safe ) ) {
			return;
		}
		echo '<style id="bw-lead-ai-hide">' . esc_html( implode( ', ', $safe ) ) . ' { display: ' . esc_html( $display ) . "; }</style>\n";
	}
}
