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

/**
 * The register of everything this site can put into a form field, and the
 * Gravity Forms merge tags built from it.
 *
 * Client-side JS populates hidden fields whose default value references a merge tag like
 * {bw:source}. The capture script reads the tag, resolves it from storage, and writes the
 * result before submit. Server-side we only need to register the tags so GF doesn't strip
 * them from the UI.
 *
 * Which tags exist is per-site: custom dimensions come from Tracking Parameter
 * Definitions, the GA client ID appears only once that capture is switched on, and the
 * interaction tags only once an event type is enabled. That assembly is written ONCE,
 * in available_tags(), and every surface that lists tags reads it — the GF merge-tag
 * dropdown here, the GF field's data-point dropdown
 * (BW_Lead_AI_GF_Field::data_point_groups()) and the Form Field Mapping screen. A
 * second hand-kept list is how a tag ends up offered on one screen and missing on
 * another, which is exactly what this file replaced.
 */
class BW_Lead_AI_Merge_Tags {

	const TAGS = array(
		'source', 'medium', 'source_medium', 'channel', 'campaign', 'term',
		'content', 'adgroup', 'first_page', 'last_page', 'submit_page',
		'first_source', 'first_medium', 'first_channel', 'visits', 'pages',
		'tagged_visits', 'summary', 'summary_detailed',
	);

	/** Interaction tags, only registered when interaction tracking is on. */
	const EVENT_TAGS = array( 'events', 'events_list' );

	/**
	 * Aggregations that only a mapped form field can carry, never a merge tag.
	 *
	 * `sources` and `terms` are the distinct source (and term) of EVERY stored visit,
	 * comma-joined — plural aggregations across the journey, which is why they are
	 * plural. They have been in `field_targets` since the first release and capture.js
	 * builds them in setLegacyTargets(); `resolveMergeTag()` has never known them, so
	 * `{bw:sources}` resolves to an empty string and always has.
	 *
	 * They stay field-target-only deliberately rather than being promoted to real
	 * tags: `sources` and `terms` are not in RESERVED_TAG_NAMES, so a site is free to
	 * have declared either as a custom dimension, and a new built-in of the same name
	 * would silently shadow that site's own data.
	 */
	const FIELD_ONLY_TAGS = array( 'sources', 'terms' );

	private static $instance = null;

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

	public function register() {
		if ( ! $this->gf_active() ) {
			return;
		}
		add_filter( 'gform_custom_merge_tags', array( $this, 'register_tags' ), 10, 4 );
		// Server-side replacement: leave value empty so the JS front-end replaces it in the
		// hidden input before submission. (We cannot resolve storage values on the server.)
		add_filter( 'gform_replace_merge_tags', array( $this, 'passthrough' ), 10, 7 );
	}

	private function gf_active() {
		return class_exists( 'GFForms' );
	}

	/**
	 * The headings the register is grouped under, in reading order.
	 *
	 * Order is meaningful: it is the order every screen lists tags in, so the GF
	 * dropdown and the mapping screen scan the same way.
	 *
	 * @return array group key => heading.
	 */
	public static function tag_groups() {
		return array(
			'latest'    => __( 'Latest visit', 'bw-lead-ai' ),
			'first'     => __( 'First visit', 'bw-lead-ai' ),
			'counts'    => __( 'Counts', 'bw-lead-ai' ),
			'summary'   => __( 'Summary', 'bw-lead-ai' ),
			'journey'   => __( 'Across every visit', 'bw-lead-ai' ),
			'custom'    => __( 'Custom dimensions', 'bw-lead-ai' ),
			'analytics' => __( 'Analytics', 'bw-lead-ai' ),
			'events'    => __( 'Interactions', 'bw-lead-ai' ),
		);
	}

	/**
	 * Every data point this site actually has, in reading order.
	 *
	 * The single register. Built-ins are fixed; the rest depend on the site's own
	 * settings, which is why this takes them: custom dimensions come from Tracking
	 * Parameter Definitions, `ga_client_id` exists only while GA client ID capture is
	 * on, and the interaction tags only while at least one event type is enabled. A
	 * caller that hand-listed the built-ins would drift on the conditional ones first
	 * and silently — a tag offered on one screen and absent on the next.
	 *
	 * `merge_tag` false means the point can be mapped to a form field but is NOT a
	 * `{bw:...}` tag — see FIELD_ONLY_TAGS. Both Gravity Forms surfaces filter those
	 * out; the Form Field Mapping screen keeps them.
	 *
	 * @param array|null $settings Settings to read, or null for the stored ones. A
	 *                             partial array is filled from the defaults, so the
	 *                             sanitizer can pass the values it is about to save.
	 * @return array tag key => array( 'label' => string, 'group' => string, 'merge_tag' => bool )
	 */
	public static function available_tags( $settings = null ) {
		$settings = is_array( $settings )
			? wp_parse_args( $settings, BW_Lead_AI_Settings::defaults() )
			: BW_Lead_AI_Settings::get();

		$tags = array();

		$builtin = array(
			'latest'  => array(
				'source'        => __( 'Source', 'bw-lead-ai' ),
				'medium'        => __( 'Medium', 'bw-lead-ai' ),
				'source_medium' => __( 'Source / Medium', 'bw-lead-ai' ),
				'channel'       => __( 'Channel', 'bw-lead-ai' ),
				'campaign'      => __( 'Campaign', 'bw-lead-ai' ),
				'term'          => __( 'Term / keyword', 'bw-lead-ai' ),
				'content'       => __( 'Content / creative', 'bw-lead-ai' ),
				'adgroup'       => __( 'Ad group', 'bw-lead-ai' ),
				'last_page'     => __( 'Landing page', 'bw-lead-ai' ),
				'submit_page'   => __( 'Submit page', 'bw-lead-ai' ),
			),
			'first'   => array(
				'first_source'  => __( 'First source', 'bw-lead-ai' ),
				'first_medium'  => __( 'First medium', 'bw-lead-ai' ),
				'first_channel' => __( 'First channel', 'bw-lead-ai' ),
				'first_page'    => __( 'First landing page', 'bw-lead-ai' ),
			),
			'counts'  => array(
				'visits'        => __( 'Visits', 'bw-lead-ai' ),
				'pages'         => __( 'Pages viewed', 'bw-lead-ai' ),
				'tagged_visits' => __( 'Tagged visits', 'bw-lead-ai' ),
			),
			'summary' => array(
				'summary'          => __( 'Summary', 'bw-lead-ai' ),
				'summary_detailed' => __( 'Summary (detailed)', 'bw-lead-ai' ),
			),
			'journey' => array(
				'sources' => __( 'Every source, comma-joined', 'bw-lead-ai' ),
				'terms'   => __( 'Every term, comma-joined', 'bw-lead-ai' ),
			),
		);
		foreach ( $builtin as $group => $points ) {
			foreach ( $points as $key => $label ) {
				$tags[ $key ] = array(
					'label'     => $label,
					'group'     => $group,
					'merge_tag' => ! in_array( $key, self::FIELD_ONLY_TAGS, true ),
				);
			}
		}

		// Custom dimensions declared in Tracking Parameter Definitions. Per-site, and
		// the reason nothing may hard-code this list.
		$aliases = BW_Lead_AI_Settings::parse_parameter_aliases( $settings['parameter_aliases'] );
		foreach ( array_keys( $aliases['custom'] ) as $custom_key ) {
			$tags[ $custom_key ] = array(
				'label'     => $custom_key,
				'group'     => 'custom',
				'merge_tag' => true,
			);
		}

		// Analytics tags only appear once GA client ID capture is switched on.
		if ( ! empty( $settings['capture_ga_client_id'] ) ) {
			$tags['ga_client_id'] = array(
				'label'     => __( 'GA client ID', 'bw-lead-ai' ),
				'group'     => 'analytics',
				'merge_tag' => true,
			);
		}

		// Interaction tags only appear once at least one event type is enabled, so
		// every list stays short for sites that don't track interactions.
		$event_types = BW_Lead_AI_Settings::parse_event_types( $settings['event_types'] );
		if ( ! empty( $event_types ) ) {
			$tags['events']      = array(
				'label'     => __( 'Interactions (total count)', 'bw-lead-ai' ),
				'group'     => 'events',
				'merge_tag' => true,
			);
			$tags['events_list'] = array(
				'label'     => __( 'Interactions (breakdown)', 'bw-lead-ai' ),
				'group'     => 'events',
				'merge_tag' => true,
			);
			foreach ( $event_types as $event_type ) {
				$tags[ 'event.' . $event_type ] = array(
					/* translators: %s is an interaction type label, e.g. "Video plays". */
					'label'     => sprintf( __( '%s (count)', 'bw-lead-ai' ), BW_Lead_AI_GF_Field::event_type_label( $event_type ) ),
					'group'     => 'events',
					'merge_tag' => true,
				);
			}
		}

		return $tags;
	}

	/**
	 * Clean an arriving tag key without destroying the one tag shape that is not a
	 * plain slug.
	 *
	 * sanitize_key() strips the dot, so it would turn `event.video` into `eventvideo`
	 * and quietly drop every interaction mapping on save. This is sanitize_key plus
	 * the dot, and it is defence in depth only: every caller also checks the result
	 * against available_tags(), which is what actually bounds the key set.
	 *
	 * @param string $raw
	 * @return string
	 */
	public static function sanitize_tag_key( $raw ) {
		return (string) preg_replace( '/[^a-z0-9_.\-]/', '', strtolower( (string) $raw ) );
	}

	public function register_tags( $merge_tags, $form_id, $fields, $element_id ) {
		foreach ( self::available_tags() as $tag => $meta ) {
			if ( empty( $meta['merge_tag'] ) ) {
				continue;
			}
			$merge_tags[] = array(
				'label' => 'BW: ' . $tag,
				'tag'   => '{bw:' . $tag . '}',
			);
		}

		return $merge_tags;
	}

	/**
	 * Passthrough: leave {bw:*} tags untouched on the server so the capture script can
	 * replace them in hidden field defaults before submission.
	 */
	public function passthrough( $text, $form, $entry, $url_encode, $esc_html, $nl2br, $format ) {
		return $text;
	}
}
