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

// Loaded from BW_Lead_AI_GF_Field::register_field() on `gform_loaded`, so GF_Field
// is guaranteed to exist. The guard is belt-and-braces for direct includes.
if ( ! class_exists( 'GF_Field' ) ) {
	return;
}

/**
 * Gravity Forms field: "BW Lead Data".
 *
 * Renders a single hidden input (or textarea, for multi-line data points) whose
 * value is seeded with a `{bw:*}` merge tag. `capture.js` already scans every
 * `input, textarea` on the page and substitutes resolved values before submit,
 * so this field needs no front-end JavaScript of its own.
 *
 * The chosen data point is stored on the field as the `bwDataPoint` property by
 * the editor UI in BW_Lead_AI_GF_Field.
 */
class BW_Lead_AI_GF_Field_Datapoint extends GF_Field {

	public $type = BW_Lead_AI_GF_Field::TYPE;

	public function get_form_editor_field_title() {
		return esc_attr__( 'BW Lead Data', 'bw-lead-ai' );
	}

	public function get_form_editor_field_description() {
		return esc_attr__( 'Silently records where this lead came from — traffic source, campaign, landing page, or a full attribution summary.', 'bw-lead-ai' );
	}

	public function get_form_editor_field_icon() {
		return 'gform-icon--analytics';
	}

	public function get_form_editor_button() {
		return array(
			'group'       => 'advanced_fields',
			'text'        => $this->get_form_editor_field_title(),
			'icon'        => $this->get_form_editor_field_icon(),
			'description' => $this->get_form_editor_field_description(),
		);
	}

	public function get_form_editor_field_settings() {
		return array(
			'label_setting',
			'bw_lead_ai_datapoint_setting',
			'admin_label_setting',
			'css_class_setting',
		);
	}

	/**
	 * Conditional logic is deliberately unsupported.
	 *
	 * The value is written by the capture script after page load, so any rule
	 * evaluated against it would run before the value exists and behave
	 * unpredictably. Better to not offer it than to ship a subtle trap.
	 */
	public function is_conditional_logic_supported() {
		return false;
	}

	/**
	 * The data point this field captures, falling back to the default if the
	 * stored value is missing or no longer valid (e.g. a custom dimension that
	 * has since been removed from Tracking Parameter Definitions).
	 */
	public function get_data_point() {
		// Not sanitize_key(): interaction points look like `event.video` and
		// sanitize_key() strips the dot, which would silently mangle them.
		$raw   = (string) rgobj( $this, BW_Lead_AI_GF_Field::PROP );
		$point = strtolower( preg_replace( '/[^A-Za-z0-9_.\-]/', '', $raw ) );

		if ( '' === $point || ! BW_Lead_AI_GF_Field::is_valid_point( $point ) ) {
			return BW_Lead_AI_GF_Field::DEFAULT_POINT;
		}
		return $point;
	}

	/**
	 * Hide the whole field wrapper on the front end unless debug mode is showing
	 * it. GF only auto-applies `gform_hidden` to its own `hidden` type, so a
	 * custom field has to opt in here or it leaves an empty gap in the form.
	 */
	public function get_field_css_class() {
		if ( $this->is_form_editor() || $this->is_entry_detail() ) {
			return '';
		}
		return BW_Lead_AI_GF_Field::debug_visible() ? 'bw-lead-ai-debug-visible' : 'gform_hidden';
	}

	/**
	 * Suppress the label on the front end (mirrors GF_Field_Hidden) except when
	 * debug mode is revealing the field, where the label is what makes the
	 * readout readable.
	 */
	public function get_field_content( $value, $force_frontend_label, $form ) {
		$is_admin = $this->is_entry_detail() || $this->is_form_editor();

		if ( ! $is_admin && ! BW_Lead_AI_GF_Field::debug_visible() ) {
			return '{FIELD}';
		}

		$form_id       = absint( rgar( $form, 'id' ) );
		$admin_buttons = $is_admin ? $this->get_admin_buttons() : '';
		$field_label   = $this->get_field_label( $force_frontend_label, $value );
		$field_id      = ( $is_admin || 0 === $form_id ) ? 'input_' . $this->id : 'input_' . $form_id . '_' . $this->id;

		return sprintf(
			"%s<label class='gfield_label gform-field-label' for='%s'>%s</label>{FIELD}",
			$admin_buttons,
			esc_attr( $field_id ),
			esc_html( $field_label )
		);
	}

	/**
	 * How the captured value is rendered in the entry detail screen, in
	 * notification emails, and in `{all_fields}`.
	 *
	 * This has to be implemented because GF_Field's default gets the order wrong:
	 *
	 *     $value  = nl2br( (string) $value );   // \n  ->  <br />
	 *     $return = esc_html( $value );         // <br />  ->  &lt;br /&gt;
	 *
	 * — it inserts the tags and then escapes them, so a multi-line value renders
	 * as literal "<br />" text rather than as line breaks. GF_Field_Textarea gets
	 * it right; every other core field type is single-line, so nothing in core
	 * trips over it. Mirror the textarea behaviour: escape first, then break.
	 *
	 * Single-line data points are unaffected either way — nl2br on a string with
	 * no newlines is a no-op — so this is applied uniformly rather than only for
	 * the Summary points.
	 *
	 * Non-HTML formats (plain-text notifications, CSV export) get the raw value,
	 * where the real newlines are what's wanted.
	 */
	public function get_value_entry_detail( $value, $entry = array(), $use_text = false, $format = 'html', $media = 'screen' ) {
		if ( 'html' !== $format ) {
			return $value;
		}
		return nl2br( esc_html( (string) $value ) );
	}

	public function get_field_input( $form, $value = '', $entry = null ) {
		$form_id   = absint( rgar( $form, 'id' ) );
		$id        = (int) $this->id;
		$is_editor = $this->is_form_editor();
		$is_detail = $this->is_entry_detail();
		$point     = $this->get_data_point();
		$tag       = '{bw:' . $point . '}';
		$multiline = BW_Lead_AI_GF_Field::is_multiline( $point );
		$field_id  = ( $is_detail || $is_editor || 0 === $form_id ) ? 'input_' . $id : 'input_' . $form_id . '_' . $id;

		// Form editor: a disabled preview of what will be captured. Never a real
		// input — the editor must not look like somewhere you type a value. The
		// `bw-lead-ai-preview` class lets the editor JS refresh this text in place
		// when the data point dropdown changes, without a round trip.
		if ( $is_editor ) {
			return sprintf(
				"<div class='ginput_container ginput_container_text'><input type='text' id='%s' class='bw-lead-ai-preview' value='%s' disabled='disabled' /></div>",
				esc_attr( $field_id ),
				esc_attr( BW_Lead_AI_GF_Field::preview_text( $point ) )
			);
		}

		// Entry detail: an ordinary editable control holding the captured value.
		if ( $is_detail ) {
			if ( $multiline ) {
				return sprintf(
					"<div class='ginput_container ginput_container_textarea'><textarea name='input_%d' id='%s' rows='5' class='textarea medium'>%s</textarea></div>",
					$id,
					esc_attr( $field_id ),
					esc_textarea( $value )
				);
			}
			return sprintf(
				"<div class='ginput_container ginput_container_text'><input type='text' name='input_%d' id='%s' value='%s' class='medium' /></div>",
				$id,
				esc_attr( $field_id ),
				esc_attr( $value )
			);
		}

		// Front end. Seed with the merge tag and let capture.js resolve it. An
		// already-resolved $value (validation repost, multi-page form) wins so a
		// captured value is never clobbered back to the raw tag.
		$seed = ( '' !== (string) $value ) ? (string) $value : $tag;

		if ( BW_Lead_AI_GF_Field::debug_visible() ) {
			return $this->debug_input( $id, $field_id, $seed, $point, $tag, $multiline );
		}

		// Every data point renders as a plain hidden input on the front end —
		// including the multi-line Summary ones, which used to be a hidden
		// <textarea>.
		//
		// `type=hidden` has no value sanitization algorithm in the HTML spec, so
		// newlines survive both `.value` and form submission byte-identically to a
		// textarea (verified in-browser; `type=text` does strip them, which is
		// where the "inputs can't hold multiline" assumption comes from).
		//
		// The point of the uniform shape is integration safety: third-party code
		// that skips hidden inputs by type — a common shortcut — would otherwise
		// treat a hidden <textarea> as a user-facing field. A multi-page form's
		// review step did exactly that and showed the attribution summary back to
		// the visitor. Entry detail and debug mode still use a textarea, where the
		// value is meant to be read and edited.
		return sprintf(
			"<div class='ginput_container ginput_container_text'><input name='input_%d' id='%s' type='hidden' class='gform_hidden' value='%s' /></div>",
			$id,
			esc_attr( $field_id ),
			esc_attr( $seed )
		);
	}

	/**
	 * Visible, read-only rendering used when debug mode is on and the current
	 * user has the debug capability. Readonly (not disabled) so the value still
	 * submits and capture.js can still write to it.
	 */
	private function debug_input( $id, $field_id, $seed, $point, $tag, $multiline ) {
		$caption = sprintf(
			/* translators: 1: data point label, 2: merge tag, e.g. {bw:source} */
			esc_html__( 'BW Lead AI debug — %1$s (%2$s). Only you can see this.', 'bw-lead-ai' ),
			esc_html( BW_Lead_AI_GF_Field::label_for( $point ) ),
			esc_html( $tag )
		);

		$control = $multiline
			? sprintf(
				"<textarea name='input_%d' id='%s' rows='5' readonly='readonly' style='width:100%%;font-family:monospace;'>%s</textarea>",
				$id,
				esc_attr( $field_id ),
				esc_textarea( $seed )
			)
			: sprintf(
				"<input name='input_%d' id='%s' type='text' readonly='readonly' value='%s' style='width:100%%;font-family:monospace;' />",
				$id,
				esc_attr( $field_id ),
				esc_attr( $seed )
			);

		return sprintf(
			"<div class='ginput_container ginput_container_text' style='border:1px dashed #d63638;padding:8px;border-radius:3px;'>%s<p style='margin:6px 0 0;font-size:11px;color:#d63638;'>%s</p></div>",
			$control,
			$caption
		);
	}
}
