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

/**
 * Gravity Forms custom field integration.
 *
 * Registers the "BW Lead Data" field type and its form-editor UI. Everything in
 * here is admin-side: the field itself renders a `{bw:*}` merge tag into a
 * hidden input, and the existing capture script resolves it before submit. No
 * new front-end JavaScript is involved.
 *
 * The GF_Field subclass lives in a separate file because it extends GF_Field,
 * which does not exist until Gravity Forms has loaded.
 */
class BW_Lead_AI_GF_Field {

	/** GF field type slug. */
	const TYPE = 'bw_lead_ai_datapoint';

	/** Field property holding the chosen data point. */
	const PROP = 'bwDataPoint';

	/** Used when a field has no stored data point, or its stored one is invalid. */
	const DEFAULT_POINT = 'source_medium';

	/** Data points whose value spans multiple lines — rendered as a textarea. */
	const MULTILINE_POINTS = array( 'summary', 'summary_detailed' );

	/** The editor setting's CSS class, referenced by the field's settings list. */
	const SETTING_CLASS = 'bw_lead_ai_datapoint_setting';

	private static $instance = null;

	/** Guards against registering the field type twice. */
	private $field_registered = false;

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

	public function register() {
		// The field type can only be registered once GF_Field exists, which GF
		// signals with `gform_loaded`. GF fires that from its own `plugins_loaded`
		// callback at the same priority as ours, so whether it has already fired
		// by the time we boot depends on plugin load order — handle both cases.
		if ( did_action( 'gform_loaded' ) ) {
			$this->register_field();
		} else {
			add_action( 'gform_loaded', array( $this, 'register_field' ), 5 );
		}

		add_action( 'gform_field_standard_settings', array( $this, 'render_setting' ), 10, 2 );
		add_action( 'gform_editor_js', array( $this, 'editor_js' ) );
		add_filter( 'gform_tooltips', array( $this, 'tooltips' ) );

		// Merge tags are resolved in the browser, so a submission from anyone the
		// capture script never ran for — a visitor with JavaScript blocked, or
		// site staff, for whom capture is deliberately not enqueued — stores the
		// literal text "{bw:summary}" in the entry and mails it to whoever gets
		// notifications. An unresolved tag is not information; blank it rather
		// than archive it.
		add_filter( 'gform_entry_post_save', array( $this, 'blank_unresolved_tags' ), 5, 2 );
	}

	/**
	 * Replace any field value that is exactly an unresolved {bw:...} merge tag
	 * with an empty string, in the saved entry.
	 *
	 * Exact-match only: a value that merely CONTAINS a tag was typed by a person,
	 * and rewriting human input is not this filter's business.
	 */
	public function blank_unresolved_tags( $entry, $form ) {
		if ( ! is_array( $entry ) ) {
			return $entry;
		}
		foreach ( $entry as $key => $value ) {
			if ( ! is_string( $value ) || '' === $value ) {
				continue;
			}
			if ( preg_match( '/^\{bw:[a-z0-9_.\-]+\}$/i', trim( $value ) ) ) {
				$entry[ $key ] = '';
				if ( is_numeric( $key ) && class_exists( 'GFAPI' ) ) {
					GFAPI::update_entry_field( (int) $entry['id'], $key, '' );
				}
			}
		}
		return $entry;
	}

	public function register_field() {
		if ( $this->field_registered ) {
			return;
		}
		if ( ! class_exists( 'GF_Fields' ) || ! class_exists( 'GF_Field' ) ) {
			return;
		}
		require_once BW_LEAD_AI_DIR . 'includes/class-bw-lead-ai-gf-field-datapoint.php';
		if ( ! class_exists( 'BW_Lead_AI_GF_Field_Datapoint' ) ) {
			return;
		}
		GF_Fields::register( new BW_Lead_AI_GF_Field_Datapoint() );
		$this->field_registered = true;
	}

	/**
	 * The grouped list of selectable data points.
	 *
	 * A projection of the one register — BW_Lead_AI_Merge_Tags::available_tags() —
	 * into the nested shape this dropdown wants. The register decides what exists,
	 * including the per-site parts (custom dimensions, the GA client ID, the
	 * interaction tags), so a client who adds `match_type` gets it here without any
	 * code change and this dropdown can never disagree with the merge-tag list.
	 *
	 * Field-only points are filtered out: this field seeds `{bw:<point>}` into the
	 * form, so a point with no merge tag would render a tag that resolves to nothing.
	 *
	 * @return array [ group_key => [ 'label' => string, 'points' => [ key => label ] ] ]
	 */
	public static function data_point_groups() {
		$labels = BW_Lead_AI_Merge_Tags::tag_groups();
		$groups = array();

		foreach ( BW_Lead_AI_Merge_Tags::available_tags() as $point => $meta ) {
			if ( empty( $meta['merge_tag'] ) ) {
				continue;
			}
			$group = $meta['group'];
			if ( ! isset( $groups[ $group ] ) ) {
				$groups[ $group ] = array(
					'label'  => isset( $labels[ $group ] ) ? $labels[ $group ] : $group,
					'points' => array(),
				);
			}
			$groups[ $group ]['points'][ $point ] = $meta['label'];
		}

		return $groups;
	}

	/**
	 * Display label for an interaction event type. Shared by the field dropdown
	 * and the Events settings tab so the wording stays consistent.
	 */
	public static function event_type_label( $type ) {
		$labels = array(
			'video'      => __( 'Video plays', 'bw-lead-ai' ),
			'download'   => __( 'File downloads', 'bw-lead-ai' ),
			'phone'      => __( 'Phone clicks', 'bw-lead-ai' ),
			'email'      => __( 'Email clicks', 'bw-lead-ai' ),
			'social'     => __( 'Social clicks', 'bw-lead-ai' ),
			'outbound'   => __( 'Outbound clicks', 'bw-lead-ai' ),
			'custom'     => __( 'Custom actions', 'bw-lead-ai' ),
			'scroll'     => __( 'Scroll depth', 'bw-lead-ai' ),
			'form_start' => __( 'Form starts', 'bw-lead-ai' ),
		);
		return isset( $labels[ $type ] ) ? $labels[ $type ] : $type;
	}

	/**
	 * Whether $point is a data point this plugin can currently resolve.
	 */
	public static function is_valid_point( $point ) {
		foreach ( self::data_point_groups() as $group ) {
			if ( isset( $group['points'][ $point ] ) ) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Human-readable label for a data point, falling back to the raw key.
	 */
	public static function label_for( $point ) {
		foreach ( self::data_point_groups() as $group ) {
			if ( isset( $group['points'][ $point ] ) ) {
				return $group['points'][ $point ];
			}
		}
		return $point;
	}

	/**
	 * Whether this data point's value spans multiple lines (textarea, not input).
	 */
	public static function is_multiline( $point ) {
		return in_array( $point, self::MULTILINE_POINTS, true );
	}

	/**
	 * The read-only text shown in the form editor's field preview. Mirrored by
	 * the editor JS so the preview updates the moment the dropdown changes —
	 * keep the two in sync if this format ever changes.
	 */
	public static function preview_text( $point ) {
		return self::label_for( $point ) . '  —  {bw:' . $point . '}';
	}

	/**
	 * Whether the current request should render BW Lead Data fields visibly.
	 *
	 * Requires the plugin's global Debug mode setting AND the debug capability.
	 * Defaults to `manage_options` rather than "any logged-in user" so tracking
	 * internals aren't exposed to subscribers on membership sites. Widen it with
	 * the `bw_lead_ai_debug_capability` filter if a site genuinely needs that.
	 */
	public static function debug_visible() {
		$settings = BW_Lead_AI_Settings::get();
		if ( empty( $settings['debug'] ) ) {
			return false;
		}
		$capability = apply_filters( 'bw_lead_ai_debug_capability', 'manage_options' );
		return current_user_can( $capability );
	}

	/**
	 * Render the data point picker in the form editor's field settings panel.
	 */
	public function render_setting( $position, $form_id ) {
		if ( 25 !== (int) $position ) {
			return;
		}
		?>
		<li class="<?php echo esc_attr( self::SETTING_CLASS ); ?> field_setting">
			<label for="bw_lead_ai_datapoint_select" class="section_label">
				<?php esc_html_e( 'Data point', 'bw-lead-ai' ); ?>
				<?php gform_tooltip( self::SETTING_CLASS ); ?>
			</label>
			<select id="bw_lead_ai_datapoint_select">
				<?php foreach ( self::data_point_groups() as $group ) : ?>
					<optgroup label="<?php echo esc_attr( $group['label'] ); ?>">
						<?php foreach ( $group['points'] as $key => $label ) : ?>
							<option value="<?php echo esc_attr( $key ); ?>"><?php echo esc_html( $label ); ?></option>
						<?php endforeach; ?>
					</optgroup>
				<?php endforeach; ?>
			</select>
			<p class="description">
				<?php esc_html_e( 'Which attribution data this field captures — hidden from the visitor, submitted with the entry.', 'bw-lead-ai' ); ?>
			</p>
		</li>
		<?php
	}

	public function tooltips( $tooltips ) {
		$tooltips[ self::SETTING_CLASS ] = '<h6>' . esc_html__( 'Data point', 'bw-lead-ai' ) . '</h6>'
			. esc_html__( 'What this field records — for example Source / Medium ("google / cpc") or Summary for the full readout. The value is filled in the visitor\'s browser at submit time.', 'bw-lead-ai' );
		return $tooltips;
	}

	/**
	 * Wire the picker into the editor: load the field's stored value when a
	 * field is selected, and persist changes back onto the field.
	 */
	public function editor_js() {
		$type    = wp_json_encode( self::TYPE );
		$prop    = wp_json_encode( self::PROP );
		$default = wp_json_encode( self::DEFAULT_POINT );
		?>
		<script type="text/javascript">
		( function ( $ ) {
			var BW_TYPE    = <?php echo $type; // phpcs:ignore WordPress.Security.EscapeOutput -- wp_json_encode output. ?>;
			var BW_PROP    = <?php echo $prop; // phpcs:ignore WordPress.Security.EscapeOutput -- wp_json_encode output. ?>;
			var BW_DEFAULT = <?php echo $default; // phpcs:ignore WordPress.Security.EscapeOutput -- wp_json_encode output. ?>;

			// Tracked so the change handler knows which field's preview to refresh.
			var currentFieldId = null;

			$( document ).on( 'gform_load_field_settings', function ( event, field ) {
				if ( ! field || field.type !== BW_TYPE ) {
					currentFieldId = null;
					return;
				}
				currentFieldId = field.id;
				var current = field[ BW_PROP ] || BW_DEFAULT;
				$( '#bw_lead_ai_datapoint_select' ).val( current );
				// Persist the fallback so a newly dropped field saves with a value
				// instead of relying on the PHP-side default forever.
				if ( ! field[ BW_PROP ] ) {
					SetFieldProperty( BW_PROP, current );
				}
			} );

			$( document ).on( 'change', '#bw_lead_ai_datapoint_select', function () {
				var value = $( this ).val();
				SetFieldProperty( BW_PROP, value );

				// GF does not re-render a custom field's preview on a property
				// change, so update the preview text in place. Format must match
				// BW_Lead_AI_GF_Field::preview_text() on the PHP side.
				if ( null !== currentFieldId ) {
					var label = $( this ).find( 'option:selected' ).text();
					$( '#field_' + currentFieldId + ' .bw-lead-ai-preview' )
						.val( label + '  —  {bw:' + value + '}' );
				}
			} );
		} )( jQuery );
		</script>
		<?php
	}
}
