<?php
if ( ! defined( 'ABSPATH' ) ) {
	die( 'You are not allowed to call this page directly.' );
}

class FrmHoneypot extends FrmValidate {

	/**
	 * Track the printed selectors so we do not print the same CSS twice.
	 *
	 * @since 6.22
	 *
	 * @var array
	 */
	private static $printed_honeypot_selectors = array();

	/**
	 * Option type.
	 *
	 * @since 6.21
	 *
	 * @var string
	 */
	protected $option_type = 'global';

	/**
	 * @return string
	 */
	protected function get_option_key() {
		return 'honeypot';
	}

	/**
	 * @return bool
	 */
	private static function is_enabled() {
		$frm_settings = FrmAppHelper::get_settings();
		return $frm_settings->honeypot;
	}

	/**
	 * @return bool
	 */
	public function validate() {
		if ( ! $this->is_option_on() || ! $this->check_honeypot_filter() ) {
			// never flag as honeypot spam if disabled.
			return true;
		}
		return ! $this->is_honeypot_spam();
	}

	/**
	 * @return bool
	 */
	private function is_honeypot_spam() {
		$is_honeypot_spam = $this->is_legacy_honeypot_spam();

		if ( ! $is_honeypot_spam ) {
			$field_id = $this->get_honeypot_field_id();

			if ( ! $field_id ) {
				return false;
			}

			$value            = $this->get_honeypot_field_value( $field_id );
			$is_honeypot_spam = '' !== $value;
		}

		$atts = array(
			'form' => $this->get_form(),
		);

		/**
		 * Filters the honeypot spam check.
		 *
		 * @param bool  $is_honeypot_spam Set to `true` if is spam.
		 * @param array $atts             Contains `form` and `fields`.
		 */
		return apply_filters( 'frm_process_honeypot', $is_honeypot_spam, $atts );
	}

	/**
	 * Check the old frm_verify key. We'll continue to consider any entry with an frm_verify value as spam.
	 *
	 * @return bool
	 */
	private function is_legacy_honeypot_spam() {
		$legacy_honeypot_value = FrmAppHelper::get_param( 'frm_verify', '', 'get', 'sanitize_text_field' );
		return '' !== $legacy_honeypot_value;
	}

	/**
	 * @return mixed either true, or false.
	 */
	private function check_honeypot_filter() {
		$form = $this->get_form();
		return apply_filters( 'frm_run_honeypot', true, compact( 'form' ) );
	}

	/**
	 * @param int $form_id Form ID.
	 *
	 * @return void
	 */
	public static function maybe_render_field( $form_id ) {
		$honeypot = new self( $form_id );

		if ( ! $honeypot->should_render_field() ) {
			return;
		}

		$max_field_id = FrmDb::get_var(
			'frm_fields',
			array(),
			'id',
			array(
				'order_by' => 'id DESC',
			)
		);

		global $frm_vars;
		$offset            = isset( $frm_vars['honeypot_selectors'] ) ? count( $frm_vars['honeypot_selectors'] ) + 1 : 1;
		$honeypot_field_id = $max_field_id ? $max_field_id + $offset : $offset;
		$class             = class_exists( 'FrmProFormState' ) ? 'FrmProFormState' : 'FrmFormState';
		$class::set_initial_value( 'honeypot_field_id', $honeypot_field_id );

		$honeypot->render_field( $honeypot_field_id );
		self::maybe_print_honeypot_css();
	}

	/**
	 * Maybe print honeypot JS.
	 *
	 * @since 6.21
	 *
	 * @return void
	 */
	public static function maybe_print_honeypot_js() {
		if ( FrmAppHelper::is_admin() || ! self::is_enabled() ) {
			return;
		}

		$css = self::get_honeypot_field_css();

		if ( ! $css ) {
			return;
		}

		// There must be no empty lines inside the script. Otherwise, wpautop adds <p> tags which break script execution.
		printf(
			"<script>
				( function() {
					const style = document.createElement( 'style' );
					style.appendChild( document.createTextNode( '%s' ) );
					document.head.appendChild( style );
					document.currentScript?.remove();
				} )();
			</script>",
			esc_js( $css )
		);

		global $frm_vars;
		self::$printed_honeypot_selectors = $frm_vars['honeypot_selectors'];
	}

	/**
	 * Maybe print honeypot CSS in case JS doesn't run.
	 *
	 * @since 6.21
	 *
	 * @return void
	 */
	public static function maybe_print_honeypot_css() {
		// Print the CSS if form is loaded by API.
		if ( ! FrmFormsHelper::form_is_loaded_by_api() ) {
			return;
		}

		$css = self::get_honeypot_field_css();

		if ( $css ) {
			echo '<style>' . esc_html( $css ) . '</style>';
		}
	}

	/**
	 * Gets honeypot field CSS.
	 *
	 * @return string
	 */
	private static function get_honeypot_field_css() {
		global $frm_vars;

		if ( empty( $frm_vars['honeypot_selectors'] ) ) {
			return '';
		}

		$selectors = $frm_vars['honeypot_selectors'];

		if ( self::$printed_honeypot_selectors ) {
			$selectors = array_diff( $selectors, self::$printed_honeypot_selectors );

			if ( ! $selectors ) {
				return '';
			}
		}

		return sprintf(
			'%s {visibility:hidden;overflow:hidden;width:0;height:0;position:absolute;}',
			implode( ',', $selectors )
		);
	}

	/**
	 * @return bool
	 */
	public function should_render_field() {
		return $this->is_option_on() && $this->check_honeypot_filter();
	}

	/**
	 * @param int $honeypot_field_id
	 *
	 * @return void
	 */
	public function render_field( $honeypot_field_id = 0 ) {
		if ( ! $honeypot_field_id ) {
			return;
		}

		$field_id    = $honeypot_field_id;
		$field_key   = $this->get_honeypot_field_key();
		$input_attrs = array(
			'id'    => 'field_' . $field_key,
			'type'  => 'text',
			'class' => 'frm_form_field form-field frm_verify',
			'name'  => 'item_meta[' . $field_id . ']',
			'value' => $this->get_honeypot_field_value( $field_id ),
		);

		$container_id = 'frm_field_' . $field_id . '_container';
		$this->track_html_id( $container_id );
		?>
		<div id="<?php echo esc_attr( $container_id ); ?>">
			<label for="<?php echo esc_attr( $input_attrs['id'] ); ?>" <?php FrmFormsHelper::maybe_hide_inline(); ?>>
				<?php esc_html_e( 'If you are human, leave this field blank.', 'formidable' ); ?>
			</label>
			<input <?php FrmAppHelper::array_to_html_params( $input_attrs, true ); ?> <?php FrmFormsHelper::maybe_hide_inline(); ?> />
		</div>
		<?php
	}

	/**
	 * @param string $html_id
	 *
	 * @return void
	 */
	private function track_html_id( $html_id ) {
		global $frm_vars;

		if ( ! isset( $frm_vars['honeypot_selectors'] ) ) {
			$frm_vars['honeypot_selectors'] = array();
		}

		$frm_vars['honeypot_selectors'][] = '#' . $html_id;
	}

	/**
	 * @return int
	 */
	private function get_honeypot_field_id() {
		$class = class_exists( 'FrmProFormState' ) ? 'FrmProFormState' : 'FrmFormState';
		return $class::get_from_request( 'honeypot_field_id', 0 );
	}

	/**
	 * @return string
	 */
	private function get_honeypot_field_key() {
		return FrmAppHelper::generate_new_key( 5 );
	}

	/**
	 * Gets honeypot field value.
	 *
	 * @param string $field_id Field ID.
	 *
	 * @return string
	 */
	private function get_honeypot_field_value( $field_id ) {
		$item_meta = FrmAppHelper::get_simple_request(
			array(
				'param'   => 'item_meta',
				'default' => array(),
				'type'    => 'post',
			)
		);

		if ( ! $item_meta || ! is_array( $item_meta ) ) {
			return '';
		}

		return $item_meta[ $field_id ] ?? '';
	}

	/**
	 * Generate a random class name for our honeypot so it is less easy to detect.
	 *
	 * @return string The generated class name.
	 */
	public static function generate_class_name() {
		$class_name = self::get_honeypot_class_name();

		if ( 'frm_verify' !== $class_name ) {
			// Re-use the option.
			// We can't generate a new class too often or the field may not be hidden.
			return $class_name;
		}

		$prefix     = 'frm__';
		$class_name = $prefix . uniqid();
		update_option( 'frm_honeypot_class', $class_name );
		return $class_name;
	}

	/**
	 * @return string The current class name to use the for Honeypot field.
	 */
	private static function get_honeypot_class_name() {
		$option = get_option( 'frm_honeypot_class' );

		if ( ! is_string( $option ) ) {
			// For backward compatibility use the old class name.
			return 'frm_verify';
		}

		return $option;
	}
}
