<?php
/**
 * Sureforms Submit Class file.
 *
 * @package sureforms.
 * @since 0.0.1
 */

namespace SRFM\Inc;

use SRFM\Inc\Compatibility\Multilingual\String_Translator;
use SRFM\Inc\Database\Tables\Entries;
use SRFM\Inc\Traits\Get_Instance;
use WP_Error;
use WP_Post;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * Sureforms Helper Class.
 *
 * @since 0.0.1
 */
class Helper {
	use Get_Instance;

	/**
	 * Allowed HTML tags for SVG.
	 *
	 * @var array<string, array<string, bool>>
	 */
	public static $allowed_tags_svg = [
		'span' => [
			'class'       => true,
			'aria-hidden' => true,
		],
		'svg'  => [
			'xmlns'   => true,
			'width'   => true,
			'height'  => true,
			'viewBox' => true,
			'fill'    => true,
		],
		'path' => [
			'd'               => true,
			'stroke'          => true,
			'stroke-opacity'  => true,
			'stroke-width'    => true,
			'stroke-linecap'  => true,
			'stroke-linejoin' => true,
		],
	];

	/**
	 * Sureforms SVGs.
	 *
	 * @var mixed srfm_svgs
	 */
	private static $srfm_svgs = null;

	/**
	 * Get common error message.
	 *
	 * @since 0.0.2
	 * @return array<string>
	 */
	public static function get_common_err_msg() {
		$translator = String_Translator::get_instance();
		return [
			'required' => $translator->translate_validation_message( 'srfm_required_field', __( 'This field is required.', 'sureforms' ) ),
			'unique'   => $translator->translate_validation_message( 'srfm_unique_field', __( 'Value needs to be unique.', 'sureforms' ) ),
		];
	}

	/**
	 * Convert a file URL to a file path.
	 *
	 * @param string $file_url The URL of the file.
	 *
	 * @since 1.3.0
	 * @return string The file path.
	 */
	public static function convert_fileurl_to_filepath( $file_url ) {
		static $upload_dir = null;
		if ( ! $upload_dir ) {
			// Internally cache the upload directory.
			$upload_dir = wp_get_upload_dir();
		}
		return wp_normalize_path( str_replace( $upload_dir['baseurl'], $upload_dir['basedir'], $file_url ) );
	}

	/**
	 * Checks if current value is string or else returns default value
	 *
	 * @param mixed $data data which need to be checked if is string.
	 *
	 * @since 0.0.1
	 * @return string
	 */
	public static function get_string_value( $data ) {
		if ( is_scalar( $data ) ) {
			return (string) $data;
		}
		if ( is_object( $data ) && method_exists( $data, '__toString' ) ) {
			return $data->__toString();
		}
		if ( is_null( $data ) ) {
			return '';
		}
			return '';
	}
	/**
	 * Checks if current value is number or else returns default value
	 *
	 * @param mixed $value data which need to be checked if is string.
	 * @param int   $base value can be set is $data is not a string, defaults to empty string.
	 *
	 * @since 0.0.1
	 * @return int
	 */
	public static function get_integer_value( $value, $base = 10 ) {
		if ( is_numeric( $value ) ) {
			return (int) $value;
		}
		if ( is_string( $value ) ) {
			$trimmed_value = trim( $value );
			return intval( $trimmed_value, $base );
		}
			return 0;
	}

	/**
	 * Validate a date string in Y-m-d format.
	 *
	 * @param string $date The date string to validate.
	 * @since 2.6.0
	 * @return bool
	 */
	public static function validate_date( string $date ): bool {
		$d = \DateTime::createFromFormat( 'Y-m-d', $date );
		return $d && $d->format( 'Y-m-d' ) === $date;
	}

	/**
	 * Returns a boolean representation of the given value.
	 *
	 * @param mixed $data Data which needs to be converted to boolean.
	 *
	 * @since 2.5.2
	 * @return bool
	 */
	public static function get_boolean_value( $data ) {
		return (bool) $data;
	}

	/**
	 * Checks if current value is an array or else returns default value
	 *
	 * @param mixed $data Data which needs to be checked if it is an array.
	 *
	 * @since 0.0.3
	 * @return array
	 */
	public static function get_array_value( $data ) {
		if ( is_array( $data ) ) {
			return $data;
		}
		if ( is_null( $data ) ) {
			return [];
		}
			return (array) $data;
	}

	/**
	 * Extracts the field type from the dynamic field key ( or field slug ).
	 *
	 * @param string $field_key Dynamic field key.
	 * @since 0.0.6
	 * @return string Extracted field type.
	 */
	public static function get_field_type_from_key( $field_key ) {

		if ( false === strpos( $field_key, '-lbl-' ) ) {
			return '';
		}

		return trim( explode( '-', $field_key )[1] );
	}

	/**
	 * Extracts the field label from the dynamic field key ( or field slug ).
	 *
	 * @param string $field_key Dynamic field key.
	 * @since 1.1.1
	 * @return string Extracted field label.
	 */
	public static function get_field_label_from_key( $field_key ) {
		if ( false === strpos( $field_key, '-lbl-' ) ) {
			return '';
		}

		$label = explode( '-lbl-', $field_key )[1];
		// Getting the encrypted label. we are removing the block slug here.
		$label = explode( '-', $label )[0];

		return $label ? html_entity_decode( self::decrypt( $label ) ) : '';
	}

	/**
	 * Extracts the block ID from the dynamic field key ( or field slug ).
	 *
	 * @param string $field_key Dynamic field key.
	 * @since 1.6.1
	 * @return string Extracted block ID.
	 */
	public static function get_block_id_from_key( $field_key ) {
		// Check if the key contains the block ID identifier.
		if ( strpos( $field_key, 'srfm-' ) === 0 && strpos( $field_key, '-lbl-' ) === false ) {
			return '';  // Return empty if the key format is invalid.
		}

		$parts = explode( '-lbl-', $field_key );
		if ( isset( $parts[0] ) ) {
			$block_id = explode( '-', $parts[0] );
			if ( is_array( $block_id ) && ! empty( $block_id ) ) {
				return end( $block_id );
			}
		}
		return '';
	}

	/**
	 * Returns the proper sanitize callback functions according to the field type.
	 *
	 * @param string $field_type HTML field type.
	 * @since 0.0.6
	 * @return callable Returns sanitize callbacks according to the provided field type.
	 */
	public static function get_field_type_sanitize_function( $field_type ) {
		$callbacks = apply_filters(
			'srfm_field_type_sanitize_functions',
			[
				'url'      => 'esc_url_raw',
				'input'    => 'sanitize_text_field',
				'number'   => [ self::class, 'sanitize_number' ],
				'email'    => 'sanitize_email',
				'textarea' => [ self::class, 'sanitize_textarea' ],
			]
		);

		return $callbacks[ $field_type ] ?? 'sanitize_text_field';
	}

	/**
	 * Sanitizes a numeric value.
	 *
	 * This function checks if the input value is numeric. If it is numeric, it sanitizes
	 * the value to ensure it's a float or integer, allowing for fractions and thousand separators.
	 * If the value is not numeric, it sanitizes it as a text field.
	 *
	 * @param mixed $value The value to be sanitized.
	 * @since 0.0.6
	 * @return int|float|string The sanitized value.
	 */
	public static function sanitize_number( $value ) {
		if ( ! is_numeric( $value ) ) {
			// phpcs:ignore /** @phpstan-ignore-next-line */
			return sanitize_text_field( $value ); // If it is not numeric, then let user get some sanitized data to view.
		}

		// phpcs:ignore /** @phpstan-ignore-next-line */
		return sanitize_text_field( filter_var( $value, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION | FILTER_FLAG_ALLOW_THOUSAND ) );
	}

	/**
	 * Sanitize a CSS value to prevent injection.
	 *
	 * Strips characters that can break out of a CSS property value context
	 * and removes dangerous CSS functions while preserving safe ones
	 * (rgb, hsl, linear-gradient, etc.).
	 *
	 * @param mixed $value Raw CSS value.
	 * @return string Sanitized CSS value.
	 * @since 2.7.0
	 */
	public static function sanitize_css_value( $value ) {
		$value = self::get_string_value( $value );
		// Strip characters that can break out of a CSS property value context.
		$value = preg_replace( '/[{}<>;\\\\"\'`]/', '', $value ) ?? '';
		// Remove dangerous CSS functions (url, expression, import, etc.) while preserving safe ones (rgb, hsl, linear-gradient, etc.).
		return preg_replace( '/\b(url|expression|import|javascript)\s*\(/i', '(', $value ) ?? '';
	}

	/**
	 * This function sanitizes the submitted form data according to the field type.
	 *
	 * @param array<mixed> $form_data $form_data User submitted form data.
	 * @since 0.0.6
	 * @return array<mixed> $result Sanitized form data.
	 */
	public static function sanitize_by_field_type( $form_data ) {
		$result = [];

		if ( empty( $form_data ) || ! is_array( $form_data ) ) {
			return $result;
		}

		foreach ( $form_data as $field_key => &$value ) {
			$field_type        = self::get_field_type_from_key( $field_key );
			$sanitize_function = self::get_field_type_sanitize_function( $field_type );
			$sanitized_data    = is_array( $value ) ? self::sanitize_by_field_type( $value ) : call_user_func( $sanitize_function, $value );

			$result[ $field_key ] = $sanitized_data;
		}

		return $result;
	}

	/**
	 * Sanitize a value based on its PHP type.
	 *
	 * Recursively sanitizes arrays while preserving native PHP types (bool, int, float).
	 * Use this for complex object metas with many properties where per-field callbacks are impractical.
	 *
	 * @param mixed $value The value to sanitize.
	 * @param int   $depth Current recursion depth. Values nested beyond 10 levels are discarded.
	 * @since 2.8.0
	 * @return mixed The sanitized value.
	 */
	public static function sanitize_by_type( $value, int $depth = 0 ) {
		if ( $depth > 10 ) {
			return '';
		}
		if ( is_array( $value ) ) {
			$sanitized = [];
			foreach ( $value as $key => $val ) {
				$sanitized[ sanitize_text_field( (string) $key ) ] = self::sanitize_by_type( $val, $depth + 1 );
			}
			return $sanitized;
		}
		if ( is_bool( $value ) ) {
			return $value;
		}
		if ( is_int( $value ) ) {
			return intval( $value );
		}
		if ( is_float( $value ) ) {
			return floatval( $value );
		}
		if ( is_string( $value ) ) {
			return sanitize_text_field( $value );
		}
		return '';
	}

	/**
	 * This function performs array_map for multi dimensional array
	 *
	 * @param string       $function function name to be applied on each element on array.
	 * @param array<mixed> $data_array array on which function needs to be performed.
	 * @return array<mixed>
	 * @since 0.0.1
	 */
	public static function sanitize_recursively( $function, $data_array ) {
		$response = [];
		if ( is_array( $data_array ) ) {
			if ( ! is_callable( $function ) ) {
				return $data_array;
			}
			foreach ( $data_array as $key => $data ) {
				$val              = is_array( $data ) ? self::sanitize_recursively( $function, $data ) : $function( $data );
				$response[ $key ] = $val;
			}
		}

		return $response;
	}

	/**
	 * Generates common markup liked label, etc
	 *
	 * @param int|string $form_id form id.
	 * @param string     $type Type of form markup.
	 * @param string     $label Label for the form markup.
	 * @param string     $slug Slug for the form markup.
	 * @param string     $block_id Block id for the form markup.
	 * @param bool       $required If field is required or not.
	 * @param string     $help Help for the form markup.
	 * @param string     $error_msg Error message for the form markup.
	 * @param bool       $is_unique Check if the field is unique.
	 * @param string     $duplicate_msg Duplicate message for field.
	 * @param bool       $override Override for error markup.
	 * @return string
	 * @since 0.0.1
	 */
	public static function generate_common_form_markup( $form_id, $type, $label = '', $slug = '', $block_id = '', $required = false, $help = '', $error_msg = '', $is_unique = false, $duplicate_msg = '', $override = false ) {
		$duplicate_msg = $duplicate_msg ? ' data-unique-msg="' . esc_attr( $duplicate_msg ) . '"' : '';

		$markup                     = '';
		$show_labels_as_placeholder = get_post_meta( self::get_integer_value( $form_id ), '_srfm_use_label_as_placeholder', true );
		$show_labels_as_placeholder = $show_labels_as_placeholder ? self::get_string_value( $show_labels_as_placeholder ) : false;

		$required_sign = apply_filters( 'srfm_value_after_label_placeholder', ' *' );

		if ( ! is_string( $required_sign ) ) {
			$required_sign = ' *';
		}

		switch ( $type ) {
			case 'label':
				if ( $label ) {
					ob_start();
					?>
					<label id="srfm-label-<?php echo esc_attr( $block_id ); ?>" for="srfm-<?php echo esc_attr( $slug ); ?>-<?php echo esc_attr( $block_id ); ?>" class="srfm-block-label">
						<?php echo esc_html( $label ); ?>
						<?php if ( $required ) { ?>
							<span class="srfm-required" aria-hidden="true"> *</span>
						<?php } ?>
					</label>
					<?php
					$markup = ob_get_clean();
				}
				break;
			case 'help':
				if ( $help ) {
					ob_start();
					?>
					<div class="srfm-description" id="srfm-description-<?php echo esc_attr( $block_id ); ?>">
						<?php echo esc_html( $help ); ?>
					</div>
					<?php
					$markup = ob_get_clean();
				}
				break;
			case 'error':
				if ( $required || $override ) {
					ob_start();
					?>
					<div class="srfm-error-message" data-srfm-id="srfm-error-<?php echo esc_attr( $block_id ); ?>" data-error-msg="<?php echo esc_attr( $error_msg ); ?>"<?php echo $duplicate_msg; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
						<?php echo esc_html( $error_msg ); ?>
					</div>
					<?php
					$markup = ob_get_clean();
				}
				break;
			case 'is_unique':
				if ( $is_unique ) {
					ob_start();
					?>
					<div class="srfm-error">
						<?php echo esc_html( $duplicate_msg ); ?>
					</div>
					<?php
					$markup = ob_get_clean();
				}
				break;
			case 'placeholder':
				$markup = $label && '1' === $show_labels_as_placeholder ? esc_html( $label ) . ( $required ? esc_attr( $required_sign ) : '' ) : '';
				break;
			case 'label_text':
				// This has been added for generating label text for the form markup instead of adding it in the label tag.
				if ( $label ) {
					ob_start();
					?>
					<?php echo esc_html( $label ); ?>
					<?php if ( $required ) { ?>
						<span class="srfm-required" aria-hidden="true"> *</span>
					<?php } ?>
					<?php
					$markup = ob_get_clean();
				}
				break;
			default:
				$markup = '';
		}

		return is_string( $markup ) ? $markup : '';
	}

	/**
	 * Get an SVG Icon
	 *
	 * @since 0.0.1
	 * @param string $icon the icon name.
	 * @param string $class if the baseline class should be added.
	 * @param string $html Custom attributes inside svg wrapper.
	 * @return string
	 */
	public static function fetch_svg( $icon = '', $class = '', $html = '' ) {
		$class = $class ? ' ' . $class : '';

		if ( ! self::$srfm_svgs ) {
			ob_start();

			include_once SRFM_DIR . 'assets/svg/svgs.json';
			self::$srfm_svgs = json_decode( self::get_string_value( ob_get_clean() ), true );
			self::$srfm_svgs = apply_filters( 'srfm_svg_icons', self::$srfm_svgs );
		}

		ob_start();
		?>
		<span class="srfm-icon<?php echo esc_attr( $class ); ?>" <?php echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
			<?php echo self::$srfm_svgs[ $icon ] ?? ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
		</span>
		<?php
		$output = ob_get_clean();
		return is_string( $output ) ? $output : '';
	}

	/**
	 * Encrypt data using base64.
	 *
	 * @param string $input The input string which needs to be encrypted.
	 * @since 0.0.1
	 * @return string The encrypted string.
	 */
	public static function encrypt( $input ) {
		// If the input is empty or not a string, then abandon ship.
		if ( empty( $input ) || ! is_string( $input ) ) {
			return '';
		}

		// Strip HTML tags to prevent them from being included in IDs and field names.
		$input = wp_strip_all_tags( $input );

		// Encrypt the input and return it.
		$base_64 = base64_encode( $input ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
		return rtrim( $base_64, '=' );
	}

	/**
	 * Decrypt data using base64.
	 *
	 * @param string $input The input string which needs to be decrypted.
	 * @since 0.0.1
	 * @return string The decrypted string.
	 */
	public static function decrypt( $input ) {
		// If the input is empty or not a string, then abandon ship.
		if ( empty( $input ) || ! is_string( $input ) ) {
			return '';
		}

		// Decrypt the input and return it.
		$base_64 = $input . str_repeat( '=', strlen( $input ) % 4 );
		return base64_decode( $base_64 ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
	}

	/**
	 * Update an option from the database.
	 *
	 * @param string $key              The option key.
	 * @param mixed  $value            The value to update.
	 * @param bool   $network_override Whether to allow the network_override admin setting to be overridden on subsites.
	 * @since 0.0.1
	 * @return bool True if the option was updated, false otherwise.
	 */
	public static function update_admin_settings_option( $key, $value, $network_override = false ) {
		// Update the site-wide option if we're in the network admin, and return the updated status.
		return $network_override && is_multisite() ? update_site_option( $key, $value ) : update_option( $key, $value );
	}

	/**
	 * Update an option from the database.
	 *
	 * @param int|string $post_id post id / form id.
	 * @param string     $key meta key name.
	 * @param bool       $single single or multiple.
	 * @param mixed      $default default value.
	 *
	 * @since 0.0.1
	 * @return string Meta value.
	 */
	public static function get_meta_value( $post_id, $key, $single = true, $default = '' ) {
		$srfm_live_mode_data = self::get_instant_form_live_data();

		if ( isset( $srfm_live_mode_data[ $key ] ) ) {
			// Give priority to live mode data if we have one set from the Instant Form.
			return self::get_string_value( $srfm_live_mode_data[ $key ] );
		}

		return get_post_meta( self::get_integer_value( $post_id ), $key, $single ) ? self::get_string_value( get_post_meta( self::get_integer_value( $post_id ), $key, $single ) ) : self::get_string_value( $default );
	}

	/**
	 * Wrapper for the WordPress's get_post_meta function with the support for default values.
	 *
	 * @param int|string $post_id Post ID.
	 * @param string     $key The meta key to retrieve.
	 * @param mixed      $default Default value.
	 * @param bool       $single Optional. Whether to return a single value.
	 * @since 0.0.8
	 * @return mixed Meta value.
	 */
	public static function get_post_meta( $post_id, $key, $default = null, $single = true ) {
		$meta_value = get_post_meta( self::get_integer_value( $post_id ), $key, $single );
		return $meta_value ? $meta_value : $default;
	}

	/**
	 * Returns query params data for instant form live preview.
	 *
	 * @since 0.0.8
	 * @return array<mixed> Live preview data.
	 */
	public static function get_instant_form_live_data() {
		$srfm_live_mode_data = isset( $_GET['live_mode'] ) && self::current_user_can() ? self::sanitize_recursively( 'sanitize_text_field', wp_unslash( $_GET ) ) : []; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verification is not needed here.

		return $srfm_live_mode_data ? array_map(
			// Normalize falsy values.
			static function( $live_data ) {
				return 'false' === $live_data ? false : $live_data;
			},
			$srfm_live_mode_data
		) : [];
	}

	/**
	 * Default dynamic block value.
	 *
	 * @since 0.0.1
	 * @return array<string> Meta value.
	 */
	public static function default_dynamic_block_option() {

		$common_err_msg = self::get_common_err_msg();

		$default_values = [
			'srfm_url_block_required_text'          => $common_err_msg['required'],
			'srfm_input_block_required_text'        => $common_err_msg['required'],
			'srfm_input_block_unique_text'          => $common_err_msg['unique'],
			'srfm_address_block_required_text'      => $common_err_msg['required'],
			'srfm_phone_block_required_text'        => $common_err_msg['required'],
			'srfm_phone_block_unique_text'          => $common_err_msg['unique'],
			'srfm_number_block_required_text'       => $common_err_msg['required'],
			'srfm_textarea_block_required_text'     => $common_err_msg['required'],
			'srfm_multi_choice_block_required_text' => $common_err_msg['required'],
			'srfm_checkbox_block_required_text'     => $common_err_msg['required'],
			'srfm_gdpr_block_required_text'         => $common_err_msg['required'],
			'srfm_email_block_required_text'        => $common_err_msg['required'],
			'srfm_email_block_unique_text'          => $common_err_msg['unique'],
			'srfm_dropdown_block_required_text'     => $common_err_msg['required'],
			'srfm_rating_block_required_text'       => $common_err_msg['required'],
		];

		$default_values = array_merge( $default_values, Translatable::dynamic_validation_messages() );

		return apply_filters( 'srfm_default_dynamic_block_option', $default_values, $common_err_msg );
	}

	/**
	 * Get default dynamic block value.
	 *
	 * @param string $key meta key name.
	 * @since 0.0.1
	 * @return string Meta value.
	 */
	public static function get_default_dynamic_block_option( $key ) {
		$default_dynamic_values = self::default_dynamic_block_option();
		$option                 = get_option( 'srfm_default_dynamic_block_option', $default_dynamic_values );

		if ( is_array( $option ) && array_key_exists( $key, $option ) ) {
			return $option[ $key ];
		}
			return '';
	}

	/**
	 * Checks whether a given request has appropriate permissions.
	 *
	 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
	 * @since 0.0.1
	 */
	public static function get_items_permissions_check() {
		if ( self::current_user_can() ) {
			return true;
		}

		return new WP_Error(
			'rest_cannot_view',
			__( 'Sorry, you are not allowed to perform this action.', 'sureforms' ),
			[ 'status' => \rest_authorization_required_code() ]
		);
	}

	/**
	 * Check if the current user has a given capability.
	 *
	 * @param string       $capability The capability to check.
	 * @param array<mixed> $args Optional. Additional arguments to pass to the capability check.
	 *
	 * @since 0.0.3
	 * @return bool Whether the current user has the given capability or role.
	 */
	public static function current_user_can( $capability = '', $args = [] ) {
		if ( ! function_exists( 'current_user_can' ) ) {
			return false;
		}

		if ( ! is_string( $capability ) || empty( $capability ) ) {
			$capability = 'manage_options';
		}

		return ! empty( $args ) && is_array( $args ) && count( $args ) > 0
			? current_user_can( $capability, ...$args )
			: current_user_can( $capability );
	}

	/**
	 * Get all the entries for the given form ids. The entries are older than the given days_old.
	 *
	 * @param int        $days_old The number of days old the entries should be.
	 * @param array<int> $sf_form_ids The form ids for which the entries need to be fetched.
	 * @since 0.0.2
	 * @return array<mixed> the entries matching the criteria.
	 */
	public static function get_entries_from_form_ids( $days_old = 0, $sf_form_ids = [] ) {

		$entries       = [];
		$days_old_date = ( new \DateTime() )->modify( "-{$days_old} days" )->format( 'Y-m-d H:i:s' );

		foreach ( $sf_form_ids as $form_id ) {
			// args according to the get_all() function in the Entries class.
			$args = [
				'where' => [
					[
						[
							'key'     => 'form_id',
							'value'   => $form_id,
							'compare' => '=',
						],
						[
							'key'     => 'created_at',
							'value'   => $days_old_date,
							'compare' => '<=',
						],
					],
				],
			];

			// store all the entries in a single array.
			$entries = array_merge( $entries, Entries::get_all( $args, false ) );
		}
		return $entries;
	}

	/**
	 * Decode block attributes.
	 * The function reverses the effect of serialize_block_attributes()
	 *
	 * @link https://developer.wordpress.org/reference/functions/serialize_block_attributes/
	 * @param string $encoded_data the encoded block attribute.
	 * @since 0.0.2
	 * @return string decoded block attribute
	 */
	public static function decode_block_attribute( $encoded_data = '' ) {
		$decoded_data = preg_replace( '/\\\\u002d\\\\u002d/', '--', self::get_string_value( $encoded_data ) );
		$decoded_data = preg_replace( '/\\\\u003c/', '<', self::get_string_value( $decoded_data ) );
		$decoded_data = preg_replace( '/\\\\u003e/', '>', self::get_string_value( $decoded_data ) );
		$decoded_data = preg_replace( '/\\\\u0026/', '&', self::get_string_value( $decoded_data ) );
		$decoded_data = preg_replace( '/\\\\\\\\"/', '"', self::get_string_value( $decoded_data ) );
		return self::get_string_value( $decoded_data );
	}

	/**
	 * Map slugs to submission data.
	 *
	 * @param array<mixed> $submission_data submission_data.
	 * @since 0.0.3
	 * @return array<mixed>
	 */
	public static function map_slug_to_submission_data( $submission_data = [] ) {
		$mapped_data = [];
		foreach ( $submission_data as $key => $value ) {
			if ( false === strpos( $key, '-lbl-' ) ) {
				continue;
			}
			$label = explode( '-lbl-', $key )[1];
			$slug  = implode( '-', array_slice( explode( '-', $label ), 1 ) );
			$slug  = str_replace( ' ', '_', $slug );

			/**
			 * Filters whether a field should be skipped when mapping slugs to submission data.
			 *
			 * This filter allows plugins or custom code to determine if a field should be excluded
			 * from the mapped submission data array (such as for internal fields or extraneous meta).
			 *
			 * @since 2.0.0
			 *
			 * @param bool  $skip_this_field Whether to skip this field from processing. Default false.
			 * @param array $args {
			 *     Arguments used for this field.
			 *
			 *     @type string $key   The original key of the field in the submission data array.
			 *     @type string $slug  The mapped slug parsed from the field key.
			 *     @type mixed  $value The value assigned to this field.
			 * }
			 */
			$skip_this_field = apply_filters(
				'srfm_map_slug_to_submission_data_should_skip',
				false,
				[
					'key'   => $key,
					'slug'  => $slug,
					'value' => $value,
				]
			);

			if ( $skip_this_field ) {
				continue;
			}

			// Check if value is array to handle external package field functionality.
			// like repeater fields that need special processing.
			if ( is_array( $value ) && ! empty( $value ) ) {
				// Apply filter to allow external packages to process array values.
				// Returns processed data with 'is_processed' flag if successfully handled.
				$filtered_submission_data = apply_filters(
					'srfm_map_slug_to_submission_data_array',
					[
						'value' => $value,
						'key'   => $key,
						'slug'  => $slug,
					]
				);
				if ( isset( $filtered_submission_data['is_processed'] ) && true === $filtered_submission_data['is_processed'] ) {
					$mapped_data[ $slug ] = $filtered_submission_data['value'];
					continue;
				}
			}

			// If the value is an array (e.g. multi-upload field), decode each URL value.
			if ( is_array( $value ) ) {
				$mapped_data[ $slug ] = array_map(
					static function ( $val ) {
						return is_string( $val ) ? rawurldecode( $val ) : $val;
					},
					$value
				);
				continue;
			}

			$mapped_data[ $slug ] = is_string( $value ) ? html_entity_decode( esc_attr( $value ) ) : $value;
		}
		return $mapped_data;
	}

	/**
	 * Get forms options. Shows all the available forms in the dropdown.
	 *
	 * @since 0.0.5
	 * @param string $key Determines the type of data to return.
	 * @return array<mixed>
	 */
	public static function get_sureforms( $key = '' ) {
		$forms = get_posts(
			apply_filters(
				'srfm_get_sureforms_query_args',
				[
					'post_type'      => SRFM_FORMS_POST_TYPE,
					'posts_per_page' => -1,
					'post_status'    => 'publish',
				]
			)
		);

		$options = [];

		foreach ( $forms as $form ) {
			if ( $form instanceof WP_Post ) {
				if ( 'all' === $key ) {
					$options[ $form->ID ] = $form;
				} elseif ( ! empty( $key ) && is_string( $key ) && isset( $form->$key ) ) {
					$options[ $form->ID ] = $form->$key;
				} else {
					$options[ $form->ID ] = $form->post_title;
				}
			}
		}

		return $options;
	}

	/**
	 * Get all the forms.
	 *
	 * @since 0.0.5
	 * @return array<mixed>
	 */
	public static function get_sureforms_title_with_ids() {
		$form_options = self::get_sureforms();

		foreach ( $form_options as $key => $value ) {
			$form_options[ $key ] = $value . ' #' . $key;
		}

		return $form_options;
	}

	/**
	 * Get the CSS variables based on different field spacing sizes.
	 *
	 * @param string|null $field_spacing The field spacing size or boolean false to return complete sizes array.
	 *
	 * @since 0.0.7
	 * @return array<string|mixed>
	 */
	public static function get_css_vars( $field_spacing = null ) {
		/**
		 * $sizes - Field Spacing Sizes Variables.
		 * The array contains the CSS variables for different field spacing sizes.
		 * Each key corresponds to the field spacing size, and the value is an array of CSS variables.
		 *
		 * For future variables depending on the field spacing size, add the variable to the array respectively.
		 */
		$sizes = apply_filters(
			'srfm_css_vars_sizes',
			[
				'small'  => [
					'--srfm-row-gap-between-blocks'        => '16px',
					// Address block gap and spacing variables.
					'--srfm-address-label-font-size'       => '14px',
					'--srfm-address-label-line-height'     => '20px',
					'--srfm-address-description-font-size' => '12px',
					'--srfm-address-description-line-height' => '16px',
					'--srfm-col-gap-between-fields'        => '12px',
					'--srfm-row-gap-between-fields'        => '12px',
					'--srfm-gap-below-address-label'       => '12px',
					// Dropdown Variables.
					'--srfm-dropdown-font-size'            => '14px',
					'--srfm-dropdown-gap-between-input-menu' => '4px',
					'--srfm-dropdown-badge-padding'        => '2px 6px',
					'--srfm-dropdown-multiselect-font-size' => '12px',
					'--srfm-dropdown-multiselect-line-height' => '16px',
					'--srfm-dropdown-padding-right'        => '12px',
					// initial padding and from 20px - 12px for dropdown arrow width and 8px for gap before dropdown arrow.
					'--srfm-dropdown-padding-right-icon'   => 'calc( var( --srfm-dropdown-padding-right ) + 20px )',
					'--srfm-dropdown-multiselect-padding'  => '8px var( --srfm-dropdown-padding-right-icon ) 8px 8px',
					// Input Field Variables.
					'--srfm-input-height'                  => '40px',
					'--srfm-input-field-padding'           => '10px 12px',
					'--srfm-input-field-font-size'         => '14px',
					'--srfm-input-field-line-height'       => '20px',
					'--srfm-input-field-margin-top'        => '4px',
					'--srfm-input-field-margin-bottom'     => '4px',
					// Checkbox and GDPR Variables.
					'--srfm-checkbox-label-font-size'      => '14px',
					'--srfm-checkbox-label-line-height'    => '20px',
					'--srfm-checkbox-description-font-size' => '12px',
					'--srfm-checkbox-description-line-height' => '16px',
					'--srfm-check-ctn-width'               => '16px',
					'--srfm-check-ctn-height'              => '16px',
					'--srfm-check-svg-size'                => '10px',
					'--srfm-checkbox-margin-top-frontend'  => '2px',
					'--srfm-checkbox-margin-top-editor'    => '3px',
					'--srfm-check-gap'                     => '8px',
					'--srfm-checkbox-description-margin-left' => '24px',
					// Phone Number field variables.
					'--srfm-flag-section-padding'          => '10px 0 10px 12px',
					'--srfm-gap-between-icon-text'         => '8px',
					// Label Variables.
					'--srfm-label-font-size'               => '14px',
					'--srfm-label-line-height'             => '20px',
					// Description Variables.
					'--srfm-description-font-size'         => '12px',
					'--srfm-description-line-height'       => '16px',
					// Button Variables.
					'--srfm-btn-padding'                   => '8px 14px',
					'--srfm-btn-font-size'                 => '14px',
					'--srfm-btn-line-height'               => '20px',
					// Multi Choice Variables.
					'--srfm-multi-choice-horizontal-padding' => '16px',
					'--srfm-multi-choice-vertical-padding' => '16px',
					'--srfm-multi-choice-internal-option-gap' => '8px',
					'--srfm-multi-choice-vertical-svg-size' => '32px',
					'--srfm-multi-choice-horizontal-image-size' => '20px',
					'--srfm-multi-choice-vertical-image-size' => '100px',
					'--srfm-multi-choice-outer-padding'    => '0',
				],
				'medium' => [
					'--srfm-row-gap-between-blocks'        => '18px',
					// Address block gap and spacing variables.
					'--srfm-address-label-font-size'       => '16px',
					'--srfm-address-label-line-height'     => '24px',
					'--srfm-address-description-font-size' => '14px',
					'--srfm-address-description-line-height' => '20px',
					'--srfm-col-gap-between-fields'        => '16px',
					'--srfm-row-gap-between-fields'        => '16px',
					'--srfm-gap-below-address-label'       => '14px',
					// Input Field Variables.
					'--srfm-input-height'                  => '44px',
					'--srfm-input-field-font-size'         => '16px',
					'--srfm-input-field-line-height'       => '24px',
					'--srfm-input-field-margin-top'        => '6px',
					'--srfm-input-field-margin-bottom'     => '6px',
					// Checkbox and GDPR Variables.
					'--srfm-checkbox-label-font-size'      => '16px',
					'--srfm-checkbox-label-line-height'    => '24px',
					'--srfm-checkbox-description-font-size' => '14px',
					'--srfm-checkbox-description-line-height' => '20px',
					'--srfm-checkbox-margin-top-frontend'  => '4px',
					'--srfm-checkbox-margin-top-editor'    => '6px',
					'--srfm-checkbox-description-margin-left' => '24px',
					// Label Variables.
					'--srfm-label-font-size'               => '16px',
					'--srfm-label-line-height'             => '24px',
					// Description Variables.
					'--srfm-description-font-size'         => '14px',
					'--srfm-description-line-height'       => '20px',
					// Button Variables.
					'--srfm-btn-padding'                   => '10px 14px',
					'--srfm-btn-font-size'                 => '16px',
					'--srfm-btn-line-height'               => '24px',
					// Multi Choice Variables.
					'--srfm-multi-choice-horizontal-padding' => '20px',
					'--srfm-multi-choice-vertical-padding' => '20px',
					'--srfm-multi-choice-vertical-svg-size' => '40px',
					'--srfm-multi-choice-horizontal-image-size' => '24px',
					'--srfm-multi-choice-vertical-image-size' => '120px',
					'--srfm-multi-choice-outer-padding'    => '2px',
				],
				'large'  => [
					'--srfm-row-gap-between-blocks'        => '20px',
					// Address Block Gap and Spacing Variables.
					'--srfm-address-label-font-size'       => '18px',
					'--srfm-address-label-line-height'     => '28px',
					'--srfm-address-description-font-size' => '16px',
					'--srfm-address-description-line-height' => '24px',
					'--srfm-col-gap-between-fields'        => '16px',
					'--srfm-row-gap-between-fields'        => '20px',
					'--srfm-gap-below-address-label'       => '16px',
					// Dropdown Variables.
					'--srfm-dropdown-font-size'            => '16px',
					'--srfm-dropdown-gap-between-input-menu' => '6px',
					'--srfm-dropdown-badge-padding'        => '6px 6px',
					'--srfm-dropdown-multiselect-font-size' => '14px',
					'--srfm-dropdown-multiselect-line-height' => '20px',
					'--srfm-dropdown-padding-right'        => '14px',
					// Input Field Variables.
					'--srfm-input-height'                  => '48px',
					'--srfm-input-field-padding'           => '10px 14px',
					'--srfm-input-field-font-size'         => '18px',
					'--srfm-input-field-line-height'       => '28px',
					'--srfm-input-field-margin-top'        => '8px',
					'--srfm-input-field-margin-bottom'     => '8px',
					// Checkbox and GDPR Variables.
					'--srfm-checkbox-label-font-size'      => '18px',
					'--srfm-checkbox-label-line-height'    => '28px',
					'--srfm-checkbox-description-font-size' => '16px',
					'--srfm-checkbox-description-line-height' => '24px',
					'--srfm-check-ctn-width'               => '20px',
					'--srfm-check-ctn-height'              => '20px',
					'--srfm-check-svg-size'                => '14px',
					'--srfm-check-gap'                     => '10px',
					'--srfm-checkbox-margin-top-frontend'  => '4px',
					'--srfm-checkbox-margin-top-editor'    => '5px',
					'--srfm-checkbox-description-margin-left' => '30px',
					// Label Variables.
					'--srfm-label-font-size'               => '18px',
					'--srfm-label-line-height'             => '28px',
					// Description Variables.
					'--srfm-description-font-size'         => '16px',
					'--srfm-description-line-height'       => '24px',
					// Button Variables.
					'--srfm-btn-padding'                   => '10px 14px',
					'--srfm-btn-font-size'                 => '18px',
					'--srfm-btn-line-height'               => '28px',
					// Multi Choice Variables.
					'--srfm-multi-choice-horizontal-padding' => '24px',
					'--srfm-multi-choice-vertical-padding' => '24px',
					'--srfm-multi-choice-internal-option-gap' => '12px',
					'--srfm-multi-choice-vertical-svg-size' => '48px',
					'--srfm-multi-choice-horizontal-image-size' => '28px',
					'--srfm-multi-choice-vertical-image-size' => '140px',
					'--srfm-multi-choice-outer-padding'    => '4px',
				],
			]
		);
		// Return complete sizes array if field_spacing is false. Required in case of JS for Editor changes.
		if ( ! $field_spacing ) {
			return $sizes;
		}

		$selected_size = $sizes['small'];
		if ( 'small' !== $field_spacing && isset( $sizes[ $field_spacing ] ) ) {
			$selected_size = array_merge( $selected_size, $sizes[ $field_spacing ] );
		}

		return $selected_size;
	}

	/**
	 * Array of SureForms blocks which get have user input.
	 *
	 * @since 0.0.10
	 * @return array<string>
	 */
	public static function get_sureforms_blocks() {
		return apply_filters(
			'srfm_blocks',
			[
				'srfm/input',
				'srfm/email',
				'srfm/textarea',
				'srfm/number',
				'srfm/checkbox',
				'srfm/gdpr',
				'srfm/phone',
				'srfm/address',
				'srfm/dropdown',
				'srfm/multi-choice',
				'srfm/radio',
				'srfm/submit',
				'srfm/url',
				'srfm/payment',
			]
		);
	}

	/**
	 * Render a site key missing error message.
	 *
	 * @param string $provider_name Name of the captcha provider (e.g., HCaptcha, Google reCAPTCHA, Turnstile).
	 * @since 1.7.0
	 * @since 1.7.1 moved to inc/helper.php from inc/generate-form-markup.php
	 * @return void
	 */
	public static function render_missing_sitekey_error( $provider_name ) {
		$icon = self::fetch_svg( 'info_circle', '', 'aria-hidden="true"' );
		?>
		<p id="sitekey-error" class="srfm-common-error-message srfm-error-message">
			<?php echo wp_kses( $icon, self::$allowed_tags_svg ); ?>
			<span class="srfm-error-content">
				<?php
				echo esc_html(
					sprintf(
					/* translators: %s: Provider name like HCaptcha, Google reCAPTCHA, Turnstile */
						__( '%s sitekey is missing. Please contact your site administrator.', 'sureforms' ),
						$provider_name
					)
				);
				?>
			</span>
		</p>
		<?php
	}

	/**
	 * Parse and sanitize an email list string which may contain:
	 *
	 * @param string $input email addresses.
	 * @since 1.13.2
	 * @return string Sanitized email header string.
	 */
	public static function sanitize_email_header( $input ) {
		if ( empty( $input ) ) {
			return '';
		}

		$parts  = explode( ',', $input );
		$output = [];

		foreach ( $parts as $part ) {
			$part = trim( $part );

			// Match "Name <email>".
			if ( preg_match( '/^(.*)<(.+)>$/', $part, $matches ) ) {
				$name  = trim( $matches[1], "\" \t\n\r\0\x0B" ); // trim quotes.
				$email = sanitize_email( trim( $matches[2] ) );

				if ( is_email( $email ) ) {
					$safe_name = sanitize_text_field( $name );
					$output[]  = $safe_name . ' <' . $email . '>';
				}
			} else {
				// Plain email case.
				$email = sanitize_email( $part );
				if ( is_email( $email ) ) {
					$output[] = $email;
				}
			}
		}

		return ! empty( $output ) ? implode( ', ', $output ) : '';
	}

	/**
	 * Process blocks and inner blocks.
	 *
	 * @param array<mixed>  $blocks The block data.
	 * @param array<string> $slugs The array of existing slugs.
	 * @param bool          $updated The array of existing slugs.
	 * @param string        $prefix The array of existing slugs.
	 * @param bool          $skip_checking_existing_slug Skips the checking of existing slug if passed true. More information documented inside this function.
	 * @since 0.0.10
	 * @return array
	 */
	public static function process_blocks( $blocks, &$slugs, &$updated, $prefix = '', $skip_checking_existing_slug = false ) {

		if ( ! is_array( $blocks ) ) {
			return [ $blocks, $slugs, $updated ];
		}

		foreach ( $blocks as $index => $block ) {

			if ( ! is_array( $block ) ) {
				continue;
			}
			// Checking only for SureForms blocks which can have user input.
			if ( empty( $block['blockName'] ) || ! in_array( $block['blockName'], self::get_sureforms_blocks(), true ) ) {
				continue;
			}

			/**
			 * Lets continue if slug already exists.
			 * This will ensure that we don't update already existing slugs.
			 */
			if ( isset( $block['attrs'] ) && ! empty( $block['attrs']['slug'] ) && ! in_array( $block['attrs']['slug'], $slugs, true ) ) {

				// Made it associative array, so that we can directly check it using block_id rather than mapping or using "in_array" for the checks.
				$slugs[ $block['attrs']['block_id'] ] = self::get_string_value( $block['attrs']['slug'] );

				if ( is_array( $block['innerBlocks'] ) && ! empty( $block['innerBlocks'] ) ) {
					[ $blocks[ $index ]['innerBlocks'], $slugs, $updated ] = self::process_blocks( $block['innerBlocks'], $slugs, $updated, '' );
				}
				continue;
			}

			if ( $skip_checking_existing_slug && empty( $block['innerBlocks'] ) && isset( $slugs[ $block['attrs']['block_id'] ] ) ) {
				/**
				 * Skip re-processing of the already process or existing slugs if above parameter "$skip_checking_existing_slug" is passed as true.
				 * This is helpful in the scenarios where we need to compare and verify between already saved blocks and new unsaved blocks parsed
				 * from the contents.
				 *
				 * However, it is also necessary to make sure if that current block is not a parent / wrapper block
				 * by checking "$block['innerBlocks']" empty.
				 *
				 * And finally, checking if the block-id "$block['attrs']['block_id']" is already set in the list of "$slugs",
				 * making sure that we are only processing the new blocks.
				 */
				continue;
			}

			if ( is_array( $blocks[ $index ]['attrs'] ) ) {

				$blocks[ $index ]['attrs']['slug']    = self::generate_unique_block_slug( $block, $slugs, $prefix );
				$slugs[ $block['attrs']['block_id'] ] = $blocks[ $index ]['attrs']['slug']; // Made it associative array, so that we can directly check it using block_id rather than mapping or using "in_array" for the checks.
				$updated                              = true;
				if ( is_array( $block['innerBlocks'] ) && ! empty( $block['innerBlocks'] ) ) {

					[ $blocks[ $index ]['innerBlocks'], $slugs, $updated ] = self::process_blocks( $block['innerBlocks'], $slugs, $updated, $blocks[ $index ]['attrs']['slug'] );

				}
			}
		}
		return [ $blocks, $slugs, $updated ];
	}

	/**
	 * Generates slug based on the provided block and existing slugs.
	 *
	 * @param array<mixed>  $block The block data.
	 * @param array<string> $slugs The array of existing slugs.
	 * @param string        $prefix The array of existing slugs.
	 * @since 0.0.10
	 * @return string The generated unique block slug.
	 */
	public static function generate_unique_block_slug( $block, $slugs, $prefix ) {
		$slug = is_string( $block['blockName'] ) ? $block['blockName'] : '';

		if ( ! empty( $block['attrs']['label'] ) && is_string( $block['attrs']['label'] ) ) {
			$slug = sanitize_title( $block['attrs']['label'] );

			// If the label contains non-Latin characters (e.g. Japanese, Chinese),
			// sanitize_title() produces a percent-encoded slug like "%e3%83%95%e3%83%aa".
			// These are unstable and break conditional logic field matching.
			// Fall back to the block name to ensure a stable ASCII slug.
			if ( false !== strpos( $slug, '%' ) ) {
				$block_name = is_string( $block['blockName'] ) ? $block['blockName'] : '';
				// Strip the 'srfm/' namespace to match JS-side cleanForSlug() output.
				$block_name = (string) preg_replace( '/^srfm\//', '', $block_name );
				$slug       = sanitize_title( $block_name );
			}
		}

		if ( ! empty( $prefix ) ) {
			$slug = $prefix . '-' . $slug;
		}

		return self::generate_slug( $slug, $slugs );
	}

	/**
	 * This function ensures that the slug is unique.
	 * If the slug is already taken, it appends a number to the slug to make it unique.
	 *
	 * @param string        $slug test to be converted to slug.
	 * @param array<string> $slugs An array of existing slugs.
	 * @since 0.0.10
	 * @return string The unique slug.
	 */
	public static function generate_slug( $slug, $slugs ) {
		$slug = sanitize_title( $slug );

		if ( ! in_array( $slug, $slugs, true ) ) {
			return $slug;
		}

		$index = 1;

		while ( in_array( $slug . '-' . $index, $slugs, true ) ) {
			$index++;
		}

		return $slug . '-' . $index;
	}

	/**
	 * Encode data to JSON. This function will encode the data with JSON_UNESCAPED_SLASHES and JSON_UNESCAPED_UNICODE.
	 *
	 * @since 0.0.11
	 * @param array<mixed> $data The data to encode.
	 * @return string|false The JSON representation of the value on success or false on failure.
	 */
	public static function encode_json( $data ) {
		return wp_json_encode( $data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
	}

	/**
	 * Returns true if SureTriggers plugin is ready for the custom app.
	 *
	 * @since 1.0.3
	 * @return bool Returns true if SureTriggers plugin is ready for the custom app.
	 */
	public static function is_suretriggers_ready() {
		if ( ! defined( 'SURE_TRIGGERS_FILE' ) ) {
			// Probably plugin is de-activated or not installed at all.
			return false;
		}

		$suretriggers_data = get_option( 'suretrigger_options', [] );
		if ( ! is_array( $suretriggers_data ) || empty( $suretriggers_data['secret_key'] ) || ! is_string( $suretriggers_data['secret_key'] ) ) {
			// SureTriggers is not authenticated yet.
			return false;
		}

		return true;
	}

	/**
	 * Registers script translations for a specific handle.
	 *
	 * This function sets the script translations for a given script handle, allowing
	 * localization of JavaScript strings using the specified text domain and path.
	 *
	 * @param string $handle The script handle to apply translations to.
	 * @param string $domain Optional. The text domain for translations. Default is 'sureforms'.
	 * @param string $path   Optional. The path to the translation files. Default is the 'languages' folder in the SureForms directory.
	 *
	 * @since 1.0.5
	 * @return void
	 */
	public static function register_script_translations( $handle, $domain = 'sureforms', $path = SRFM_DIR . 'languages' ) {
		wp_set_script_translations( $handle, $domain, $path );
	}

	/**
	 * Validates whether the specified conditions or a single key-value pair exist in the request context.
	 *
	 * - If `$conditions` is provided as an array, it will validate all key-value pairs in `$conditions`
	 *   against the `$_REQUEST` superglobal.
	 * - If `$conditions` is empty, it validates a single key-value pair from `$key` and `$value`.
	 *
	 * @param string                $value      The expected value to match in the request if `$conditions` is not used.
	 * @param string                $key        The key to check for in the request if `$conditions` is not used.
	 * @param array<string, string> $conditions An optional associative array of key-value pairs to validate.
	 * @since 1.1.1
	 * @return bool Returns true if all conditions are met or the single key-value pair is valid, otherwise false.
	 */
	public static function validate_request_context( $value, $key = 'post_type', $conditions = [] ) {
		// If conditions are provided, validate all key-value pairs in the conditions array.
		if ( ! empty( $conditions ) ) {
			foreach ( $conditions as $condition_key => $condition_value ) {
				if ( ! isset( $_REQUEST[ $condition_key ] ) || $_REQUEST[ $condition_key ] !== $condition_value ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is a controlled comparison of request values.
					// Return false if any condition is not satisfied.
					return false;
				}
			}
			// Return true if all conditions are satisfied.
			return true;
		}

		// Validate $value and $key when no conditions are provided.
		if ( empty( $key ) || empty( $value ) ) {
			return false;
		}

		// Validate a single key-value pair when no conditions are provided.
		return isset( $_REQUEST[ $key ] ) && $_REQUEST[ $key ] === $value; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verification is not needed here. Input is validated via strict comparison.
	}

	/**
	 * Retrieve the list of excluded fields for form data processing.
	 *
	 * This method returns an array of field keys that should be excluded when
	 * processing form data.
	 *
	 * @since 1.1.1
	 * @return array<string> Returns the string array of excluded fields.
	 */
	public static function get_excluded_fields() {
		$excluded_fields = [ 'srfm-honeypot-field', 'g-recaptcha-response', 'srfm-sender-email-field', 'form-id' ];

		return apply_filters( 'srfm_excluded_fields', $excluded_fields );
	}

	/**
	 * Check whether the current page is a SureForms admin page.
	 *
	 * @since 1.2.2
	 * @return bool Returns true if the current page is a SureForms admin page, otherwise false.
	 */
	public static function is_sureforms_admin_page() {
		$current_screen                    = get_current_screen();
		$is_screen_sureforms_menu          = self::validate_request_context( 'sureforms_menu', 'page' );
		$is_screen_add_new_form            = self::validate_request_context( 'add-new-form', 'page' );
		$is_screen_sureforms_form_settings = self::validate_request_context( 'sureforms_form_settings', 'page' );
		$is_screen_sureforms_entries       = self::validate_request_context( SRFM_ENTRIES, 'page' );
		$is_post_type_sureforms_form       = $current_screen && SRFM_FORMS_POST_TYPE === $current_screen->post_type;

		return $is_screen_sureforms_menu || $is_screen_add_new_form || $is_screen_sureforms_form_settings || $is_screen_sureforms_entries || $is_post_type_sureforms_form;
	}

	/**
	 * Filters and concatenates valid class names from an array.
	 *
	 * @param array<string> $class_names The array containing potential class names.
	 * @since 1.4.0
	 * @return string The concatenated string of valid class names separated by spaces.
	 */
	public static function join_strings( $class_names ) {
		// Filter the array to include only valid class names.
		$valid_class_names = array_filter(
			$class_names,
			static function ( $value ) {
				return is_string( $value ) && '' !== $value && false !== $value;
			}
		);

		// Concatenate the valid class names with spaces and return.
		return implode( ' ', $valid_class_names );
	}
	/**
	 * Get SureForms Website URL.
	 *
	 * @param string                $trail The URL trail to append to SureForms website URL. The parameter should not include a leading slash as the base URL already ends with a trailing slash.
	 * @param array<string, string> $utm_args Optional. An associative array of UTM parameters to append to the URL. Default empty array. Example: [ 'utm_medium' => 'dashboard'].
	 * @since 0.0.7
	 * @return string
	 */
	public static function get_sureforms_website_url( $trail, $utm_args = [] ) {
		$url = SRFM_WEBSITE;
		if ( ! empty( $trail ) && is_string( $trail ) ) {
			$url = SRFM_WEBSITE . $trail;
		}

		if ( ! is_array( $utm_args ) ) {
			$utm_args = [];
		}

		// SRFM-2709: deterministic UTM attribution — start.
		// When the caller opts into UTM tracking by passing any utm_args, fill in
		// SureForms' deterministic source/campaign defaults. Caller-provided keys
		// (including the placement passed via utm_medium) always win.
		if ( ! empty( $utm_args ) ) {
			$utm_args = array_merge(
				[
					'utm_source'   => 'sureforms_plugin',
					'utm_campaign' => 'core_plugin',
				],
				$utm_args
			);
		}
		// SRFM-2709: deterministic UTM attribution — end.

		if ( class_exists( 'BSF_UTM_Analytics' ) ) {
			$url = \BSF_UTM_Analytics::get_utm_ready_link( $url, 'sureforms', $utm_args );
		}

		// SRFM-2709: post-BSF_UTM_Analytics fallback — start.
		// BSF_UTM_Analytics returns the URL unchanged when no install referer is
		// recorded. Merge any caller UTM keys still missing from the final URL.
		if ( ! empty( $utm_args ) ) {
			$existing = [];
			$query    = wp_parse_url( $url, PHP_URL_QUERY );
			if ( is_string( $query ) && '' !== $query ) {
				parse_str( $query, $existing );
			}
			$missing = array_diff_key( $utm_args, $existing );
			if ( ! empty( $missing ) ) {
				$url = add_query_arg( $missing, $url );
			}
		}
		// SRFM-2709: post-BSF_UTM_Analytics fallback — end.

		return esc_url( $url );
	}

	/**
	 * Validates if the given string is a valid CSS class name.
	 *
	 * A valid CSS class name:
	 * - Does not start with a digit, hyphen, or underscore.
	 * - Can contain alphanumeric characters, underscores, hyphens, and Unicode letters.
	 *
	 * @param string $class_name The class name to validate.
	 *
	 * @since 1.3.1
	 * @return bool True if the class name is valid, otherwise false.
	 */
	public static function is_valid_css_class_name( $class_name ) {
		// Regular expression to validate a Unicode-aware CSS class name.
		$class_name_regex = '/^[^\d\-_][\w\p{L}\p{N}\-_]*$/u';

		// Check if the className matches the pattern.
		return preg_match( $class_name_regex, $class_name ) === 1;
	}

	/**
	 * Get the gradient css for given gradient parameters.
	 *
	 * @param string $type The type of gradient. Default 'linear'.
	 * @param string $color1 The first color of the gradient. Default '#FFC9B2'.
	 * @param string $color2 The second color of the gradient. Default '#C7CBFF'.
	 * @param int    $loc1 The location of the first color. Default 0.
	 * @param int    $loc2 The location of the second color. Default 100.
	 * @param int    $angle The angle of the gradient. Default 90.
	 *
	 * @since 1.4.4
	 * @return string The gradient css.
	 */
	public static function get_gradient_css( $type = 'linear', $color1 = '#FFC9B2', $color2 = '#C7CBFF', $loc1 = 0, $loc2 = 100, $angle = 90 ) {
		if ( 'linear' === $type ) {
			return "linear-gradient({$angle}deg, {$color1} {$loc1}%, {$color2} {$loc2}%)";
		}
			return "radial-gradient({$color1} {$loc1}%, {$color2} {$loc2}%)";
	}

	/**
	 * Return the classes based on background and overlay type to add to the form container.
	 *
	 * @param string $background_type The background type.
	 * @param string $overlay_type The overlay type.
	 * @param string $bg_image The background image url.
	 *
	 * @since 1.4.4
	 * @return string The classes to add to the form container.
	 */
	public static function get_background_classes( $background_type, $overlay_type, $bg_image = '' ) {
		if ( empty( $background_type ) ) {
			$background_type = 'color';
		}

		$background_type_class = '';
		$overlay_class         = 'image' === $background_type && ! empty( $bg_image ) && $overlay_type ? "srfm-overlay-{$overlay_type}" : '';

		// Set the class based on the background type.
		switch ( $background_type ) {
			case 'image':
				$background_type_class = 'srfm-bg-image';
				break;
			case 'gradient':
				$background_type_class = 'srfm-bg-gradient';
				break;
			default:
				$background_type_class = 'srfm-bg-color';
				break;
		}

		return self::join_strings( [ $background_type_class, $overlay_class ] );
	}

	/**
	 * Custom escape function for the textarea with rich text support.
	 *
	 * @param string $content The content submitted by the user in the textarea block.
	 * @since 1.7.1
	 *
	 * @return string Escaped content.
	 */
	public static function esc_textarea( $content ) {
		$content = wpautop( self::sanitize_textarea( $content ) );

		return trim( str_replace( [ "\r\n", "\r", "\n" ], '', $content ) );
	}

	/**
	 * Custom sanitization function for the textarea with rich text support.
	 *
	 * @param string $content The content submitted by the user in the textarea block.
	 * @since 1.7.1
	 *
	 * @return string Sanitized content.
	 */
	public static function sanitize_textarea( $content ) {
		$count   = 1;
		$content = convert_invalid_entities( $content );

		// Remove the 'script' and 'style' tags recursively from the content.
		while ( $count ) {
			$content = preg_replace( '@<(script|style)[^>]*?>.*?</\\1>@si', '', self::get_string_value( $content ), - 1, $count );
		}

		// Disable the safe style attribute parsing for the textarea block.
		add_filter( 'safe_style_css', [ self::class, 'disable_style_attr_parsing' ], 10, 1 );
		$content = wp_kses_post( self::get_string_value( $content ) );

		// Remove the filter after sanitization to avoid affecting other blocks.
		remove_filter( 'safe_style_css', [ self::class, 'disable_style_attr_parsing' ], 10 );

		// Ensure all tags are balanced.
		return force_balance_tags( $content );
	}

	/**
	 * Disable parsing of style attributes for the textarea block.
	 *
	 * @param array<string> $allowed_styles The allowed styles.
	 * @since 1.7.1
	 *
	 * @return array An empty array to disable style attribute parsing.
	 */
	public static function disable_style_attr_parsing( $allowed_styles ) {
		unset( $allowed_styles );
		// Disable parsing of style attributes.
		return [];
	}
	/**
	 * Strips JavaScript attributes from HTML content.
	 *
	 * @param string $html               The HTML content to process.
	 * @param bool   $remove_link_target Optional. When true, removes target and strips noopener/noreferrer from rel on links. Default false.
	 * @since 1.7.1
	 * @since 2.5.2 Added $remove_link_target parameter.
	 * @return string The cleaned HTML content without JavaScript attributes.
	 */
	public static function strip_js_attributes( $html, $remove_link_target = false ) {
		$dom = new \DOMDocument();

		// Suppress warnings due to malformed HTML.
		libxml_use_internal_errors( true );
		$loaded = $dom->loadHTML( '<?xml encoding="utf-8" ?>' . $html );
		libxml_clear_errors();

		if ( ! $loaded ) {
			return $html; // Return original HTML if loading fails.
		}

		$xpath = new \DOMXPath( $dom );

		// 1. Remove all <script> tags.
		$script_nodes = $xpath->query( '//script' );
		if ( $script_nodes instanceof \DOMNodeList ) {
			foreach ( $script_nodes as $script ) {
				// phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- This is a DOM element.
				$parent_node = $script->parentNode;
				if ( $parent_node instanceof \DOMNode ) {
					$parent_node->removeChild( $script );
				}
			}
		}

		// 2. Remove all attributes that start with "on" (like onclick, onmouseover, etc.).
		$elements_with_on_attrs = $xpath->query( '//*[@*[starts-with(name(), "on")]]' );
		if ( $elements_with_on_attrs instanceof \DOMNodeList ) {
			foreach ( $elements_with_on_attrs as $element ) {
				if ( $element instanceof \DOMElement && $element->hasAttributes() ) {
					foreach ( iterator_to_array( $element->attributes ) as $attr ) {
						if ( $attr instanceof \DOMAttr && stripos( $attr->name, 'on' ) === 0 ) {
							$element->removeAttribute( $attr->name );
						}
					}
				}
			}
		}

		// 3. Optionally remove target and target-related rel values (noopener, noreferrer) from links.
		if ( $remove_link_target ) {
			$links = $xpath->query( '//a[@target]' );
			if ( $links instanceof \DOMNodeList ) {
				foreach ( $links as $link ) {
					if ( $link instanceof \DOMElement ) {
						$link->removeAttribute( 'target' );
						$rel = $link->getAttribute( 'rel' );
						if ( $rel ) {
							$cleaned_rel = trim( (string) preg_replace( '/\s+/', ' ', (string) preg_replace( '/\b(noopener|noreferrer)\b/i', '', $rel ) ) );
							if ( $cleaned_rel ) {
								$link->setAttribute( 'rel', $cleaned_rel );
							} else {
								$link->removeAttribute( 'rel' );
							}
						}
					}
				}
			}
		}

		// Return cleaned HTML.
		$body = $dom->getElementsByTagName( 'body' )->item( 0 );
		if ( $body instanceof \DOMNode ) {
			$cleaned_html = $dom->saveHTML( $body );
			return is_string( $cleaned_html ) ? $cleaned_html : '';
		}
		return '';
	}

	/**
	 * Encodes the given string with base64.
	 * Moved from admin class to here.
	 *
	 * @param  string $logo contains svg's.
	 * @return string
	 */
	public static function encode_svg( $logo ) {
		return 'data:image/svg+xml;base64,' . base64_encode( $logo ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
	}

	/**
	 * Get plugin status
	 *
	 * @since 0.0.1
	 * @since 1.7.0 moved to inc/helper.php from inc/admin-ajax.php
	 *
	 * @param  string $plugin_init_file Plugin init file.
	 * @return string
	 */
	public static function get_plugin_status( $plugin_init_file ) {

		$installed_plugins = get_plugins();

		if ( ! isset( $installed_plugins[ $plugin_init_file ] ) ) {
			return 'Install';
		}
		if ( is_plugin_active( $plugin_init_file ) ) {
			return 'Activated';
		}
			return 'Installed';
	}

	/**
	 * Return the first installed plugin from a list, or a default if none exist.
	 *
	 * @since 2.0.0
	 *
	 * @param array<string> $plugins_to_check Plugin file paths to check, in priority order.
	 * @param string        $default          Optional fallback plugin file path. Default empty string.
	 *
	 * @return string First installed plugin file path, or the default.
	 */
	public static function get_plugin_if_installed( $plugins_to_check, $default = '' ) {
		if ( ! function_exists( 'get_plugins' ) ) {
			require_once ABSPATH . 'wp-admin/includes/plugin.php';
		}

		$plugins = get_plugins();

		foreach ( self::get_array_value( $plugins_to_check ) as $plugin_file ) {
			if ( isset( $plugins[ $plugin_file ] ) ) {
				return $plugin_file;
			}
		}

		return $default;
	}

	/**
	 * Check which Starter Templates plugin is installed and return its main plugin file path.
	 *
	 * @since 1.7.3
	 *
	 * @return string The main plugin file path of the installed Starter Templates plugin.
	 */
	public static function check_starter_template_plugin() {
		return self::get_plugin_if_installed(
			[ 'astra-pro-sites/astra-pro-sites.php' ],
			'astra-sites/astra-sites.php'
		);
	}

	/**
	 * Get sureforms recommended integrations.
	 *
	 * @since 0.0.1
	 * @since 1.7.0 moved to inc/helper.php from inc/admin-ajax.php
	 *
	 * @return array<mixed>
	 */
	public static function sureforms_get_integration() {
		$suretrigger_connected  = apply_filters( 'suretriggers_is_user_connected', '' );
		$logo_sure_triggers     = file_get_contents( plugin_dir_path( SRFM_FILE ) . 'images/suretriggers.svg' );
		$logo_full              = file_get_contents( plugin_dir_path( SRFM_FILE ) . 'images/suretriggers_full.svg' );
		$logo_sure_mails        = file_get_contents( plugin_dir_path( SRFM_FILE ) . 'images/suremails.svg' );
		$logo_uae               = file_get_contents( plugin_dir_path( SRFM_FILE ) . 'images/uae.svg' );
		$logo_starter_templates = file_get_contents( plugin_dir_path( SRFM_FILE ) . 'images/starterTemplates.svg' );
		$logo_sure_rank         = file_get_contents( plugin_dir_path( SRFM_FILE ) . 'images/surerank.svg' );
		$logo_sure_contact      = file_get_contents( plugin_dir_path( SRFM_FILE ) . 'images/surecontact.svg' );

		$integrations = [
			'sure_contact'      => [
				'title'                 => __( 'SureContact', 'sureforms' ),
				'singleLineDescription' => __( 'Turn Emails Into Revenue with a CRM Built for Your Website!', 'sureforms' ),
				'subtitle'              => __( 'Send newsletters, run campaigns, set up automations, manage contacts, and see exactly how much revenue your emails generate, all in one place.', 'sureforms' ),
				'status'                => self::get_plugin_status( 'surecontact/surecontact.php' ),
				'slug'                  => 'surecontact',
				'path'                  => 'surecontact/surecontact.php',
				'logo'                  => self::encode_svg( is_string( $logo_sure_contact ) ? $logo_sure_contact : '' ),
			],
			'sure_mails'        => [
				'title'                 => __( 'SureMail', 'sureforms' ),
				'singleLineDescription' => __( 'Boost Your Email Deliverability Instantly!', 'sureforms' ),
				'subtitle'              => __( 'Access a powerful, easy-to-use email delivery service that ensures your emails land in inboxes, not spam folders. Automate your WordPress email workflows confidently with SureMail.', 'sureforms' ),
				'status'                => self::get_plugin_status( 'suremails/suremails.php' ),
				'slug'                  => 'suremails',
				'path'                  => 'suremails/suremails.php',
				'logo'                  => self::encode_svg( is_string( $logo_sure_mails ) ? $logo_sure_mails : '' ),
			],
			'sure_triggers'     => [
				'title'                 => __( 'OttoKit', 'sureforms' ),
				'singleLineDescription' => __( 'Automate your WordPress workflows effortlessly.', 'sureforms' ),
				'subtitle'              => __( 'Connect your WordPress plugins and favourite apps, automate tasks, and sync data effortlessly using OttoKit’s clean, visual workflow builder — no coding or complex setup required.', 'sureforms' ),
				'status'                => self::get_plugin_status( 'suretriggers/suretriggers.php' ),
				'slug'                  => 'suretriggers',
				'path'                  => 'suretriggers/suretriggers.php',
				'logo'                  => self::encode_svg( is_string( $logo_sure_triggers ) ? $logo_sure_triggers : '' ),
				'logo_full'             => self::encode_svg( is_string( $logo_full ) ? $logo_full : '' ),
				'connected'             => $suretrigger_connected,
				'connection_url'        => admin_url( 'admin.php?page=suretriggers' ),
			],
			'starter_templates' => [
				'title'                 => __( 'Starter Templates', 'sureforms' ),
				'singleLineDescription' => __( 'Launch Beautiful Websites in Minutes!', 'sureforms' ),
				'subtitle'              => __( 'Choose from professionally designed templates, import with one click, and customize effortlessly to match your brand.', 'sureforms' ),
				'status'                => self::get_plugin_status( self::check_starter_template_plugin() ),
				'slug'                  => 'astra-sites',
				'path'                  => self::check_starter_template_plugin(),
				'logo'                  => self::encode_svg( is_string( $logo_starter_templates ) ? $logo_starter_templates : '' ),
			],
		];

		$elementor_installed = self::get_plugin_if_installed( [ 'elementor/elementor.php' ] );

		if ( $elementor_installed ) {
			$integrations['uae'] = [
				'title'                 => __( 'Ultimate Addons for Elementor', 'sureforms' ),
				'singleLineDescription' => __( 'Power Up Elementor to Build Stunning Websites Faster!', 'sureforms' ),
				'subtitle'              => __( 'Enhance Elementor with powerful widgets and templates. Build stunning, high-performing websites faster with creative design elements and seamless customization.', 'sureforms' ),
				'status'                => self::get_plugin_status( 'header-footer-elementor/header-footer-elementor.php' ),
				'slug'                  => 'header-footer-elementor',
				'path'                  => 'header-footer-elementor/header-footer-elementor.php',
				'logo'                  => self::encode_svg( is_string( $logo_uae ) ? $logo_uae : '' ),
			];
		} else {
			$integrations['sure_rank'] = [
				'title'                 => __( 'SureRank', 'sureforms' ),
				'singleLineDescription' => __( 'Elevate Your SEO and Climb Search Rankings Effortlessly!', 'sureforms' ),
				'subtitle'              => __( 'Boost your website\'s visibility with smart SEO automation. Optimize content, track keyword performance, and get actionable insights, all inside WordPress.', 'sureforms' ),
				'status'                => self::get_plugin_status( 'surerank/surerank.php' ),
				'slug'                  => 'surerank',
				'path'                  => 'surerank/surerank.php',
				'logo'                  => self::encode_svg( is_string( $logo_sure_rank ) ? $logo_sure_rank : '' ),
			];
		}

		return apply_filters( 'srfm_integrated_plugins', $integrations );
	}

	/**
	 * Get the current rotating plugin for the banner.
	 *
	 * Plugins rotate every 2 days. Only non-activated plugins are shown.
	 * Returns false if all plugins are activated.
	 *
	 * @since 2.0.0
	 * @return array<string, mixed>|false The current plugin data or false if all plugins are activated.
	 */
	public static function get_rotating_plugin_banner() {
		$all_plugins = self::sureforms_get_integration();

		if ( ! is_array( $all_plugins ) ) {
			return false;
		}

		$available_plugins = [];

		// Only include non-activated plugins.
		foreach ( $all_plugins as $plugin ) {
			if ( ! is_array( $plugin ) ) {
				continue;
			}
			if ( isset( $plugin['status'] ) && is_string( $plugin['status'] ) && 'Activated' !== $plugin['status'] ) {
				$available_plugins[] = $plugin;
			}
		}

		// Re-index the array to have sequential numeric keys.
		$available_plugins = array_values( $available_plugins );
		$total_plugins     = count( $available_plugins );

		// Hide section if all plugins are active.
		if ( 0 === $total_plugins ) {
			return false;
		}

		// Get stored rotation data.
		$rotation_data = self::get_srfm_option( 'plugin_banner_rotation', [] );

		if ( ! is_array( $rotation_data ) ) {
			$rotation_data = [];
		}

		// Initialize rotation data if empty.
		if ( empty( $rotation_data ) ) {
			$current_time = time();
			self::update_srfm_option(
				'plugin_banner_rotation',
				[
					'last_rotation_date' => $current_time,
					'plugin_index'       => 0,
				]
			);
			return isset( $available_plugins[0] ) && is_array( $available_plugins[0] ) ? $available_plugins[0] : false;
		}

		$last_rotation_date = isset( $rotation_data['last_rotation_date'] ) && is_int( $rotation_data['last_rotation_date'] ) ? $rotation_data['last_rotation_date'] : 0;
		$plugin_index       = isset( $rotation_data['plugin_index'] ) && is_numeric( $rotation_data['plugin_index'] ) ? intval( $rotation_data['plugin_index'] ) : 0;

		$current_time        = time();
		$days_since_rotation = ( $current_time - $last_rotation_date ) / DAY_IN_SECONDS;

		// Rotate every 2 days.
		if ( $days_since_rotation >= 2 ) {
			// Rotate to next plugin.
			++$plugin_index;
			$plugin_index %= $total_plugins;

			// Update the rotation data.
			self::update_srfm_option(
				'plugin_banner_rotation',
				[
					'last_rotation_date' => $current_time,
					'plugin_index'       => $plugin_index,
				]
			);
		}

		// Ensure the index is within bounds.
		if ( $plugin_index >= $total_plugins ) {
			$plugin_index = 0;
		}

		return isset( $available_plugins[ $plugin_index ] ) && is_array( $available_plugins[ $plugin_index ] ) ? $available_plugins[ $plugin_index ] : false;
	}

	/**
	 * Get a value from the srfm_options array.
	 *
	 * @param string $key The key to retrieve.
	 * @param mixed  $default The default value to return if the key does not exist.
	 * @since 1.8.0
	 * @return mixed
	 */
	public static function get_srfm_option( $key, $default = null ) {
		$options = get_option( 'srfm_options', [] );
		if ( ! is_array( $options ) ) {
			$options = [];
		}
		return array_key_exists( $key, $options ) ? $options[ $key ] : $default;
	}

	/**
	 * Update a value in the srfm_options array.
	 *
	 * @param string $key   The key to update.
	 * @param mixed  $value The value to set.
	 * @since 1.8.0
	 * @return void
	 */
	public static function update_srfm_option( $key, $value ) {
		$options = get_option( 'srfm_options', [] );
		if ( ! is_array( $options ) ) {
			$options = [];
		}
		$options[ $key ] = $value;
		update_option( 'srfm_options', $options );
	}

	/**
	 * Get the WordPress file types.
	 *
	 * @since 1.7.4
	 * @return array<string,mixed> An associative array representing the file types.
	 */
	public static function get_wp_file_types() {
		$formats = [];
		$mimes   = get_allowed_mime_types();
		$maxsize = wp_max_upload_size() / 1048576;
		if ( ! empty( $mimes ) ) {
			foreach ( $mimes as $type => $mime ) {
				$multiple = explode( '|', $type );
				foreach ( $multiple as $single ) {
					$formats[] = $single;
				}
			}
		}

		return [
			'formats' => $formats,
			'maxsize' => $maxsize,
		];
	}

	/**
	 * Determines if the SureForms Pro plugin is installed and active.
	 *
	 * Checks for the presence of the SRFM_PRO_VER constant.
	 *
	 * @since 1.8.0
	 *
	 * @return bool True if the Pro plugin is active; false otherwise.
	 */
	public static function has_pro() {
		return defined( 'SRFM_PRO_VER' );
	}

	/**
	 * Verifies the request by checking the nonce and user capabilities.
	 *
	 * @param string $request_type The type of request, either 'rest' or 'ajax'.
	 * @param string $nonce_action The action name for the nonce.
	 * @param string $nonce_name   The name of the nonce field.
	 * @param string $capability   The capability required to perform the action. Default is 'manage_options'.
	 *
	 * @since 1.10.0
	 * @return void
	 */
	public static function verify_nonce_and_capabilities( $request_type, $nonce_action, $nonce_name, $capability = 'manage_options' ) {

		if ( ! is_string( $nonce_action ) || ! is_string( $nonce_name ) || empty( $nonce_action ) || empty( $nonce_name ) ) {
			wp_send_json_error(
				[ 'message' => __( 'Invalid nonce action or name.', 'sureforms' ) ],
				400
			);
		}

		// Verify nonce for security.
		if ( 'rest' === $request_type ) {
			// For REST API requests, use the WP_REST_Request object to verify the nonce.
			if ( ! wp_verify_nonce( $nonce_action, $nonce_name ) ) {
				wp_send_json_error(
					[ 'message' => __( 'Invalid security token.', 'sureforms' ) ],
					403
				);
			}
		} elseif ( 'ajax' === $request_type ) {
			// For non-REST requests, use the standard nonce verification.
			if ( ! check_ajax_referer( $nonce_action, $nonce_name, false ) ) {
				wp_send_json_error(
					[ 'message' => __( 'Invalid security token.', 'sureforms' ) ],
					403
				);
			}
		} else {
			// If the request type is not recognized, return an error.
			wp_send_json_error(
				[ 'message' => __( 'Invalid request type.', 'sureforms' ) ],
				400
			);
		}

		// Check user capabilities.
		if ( ! current_user_can( $capability ) ) {
			wp_send_json_error(
				[ 'message' => esc_html__( 'You do not have permission to perform this action.', 'sureforms' ) ],
				403
			);
		}
	}

	/**
	 * Get the block name from a field name by extracting the first two parts.
	 *
	 * @param string $field_name The full field name (e.g., 'srfm-text-lbl-123').
	 *
	 * @since 1.11.0
	 * @return string The block name (e.g., 'srfm-text').
	 */
	public static function get_block_name_from_field( $field_name ) {
		return implode( '-', array_slice( explode( '-', explode( '-lbl-', $field_name )[0] ), 0, 2 ) );
	}

	/**
	 * Check if any of the top 10 popular WordPress SMTP plugins is active using array_intersect.
	 *
	 * @since 1.9.1
	 * @return bool True if any SMTP plugin is active, false otherwise.
	 */
	public static function is_any_smtp_plugin_active() {
		$smtp_plugins = [
			'wp-mail-smtp/wp_mail_smtp.php',
			'post-smtp/postman-smtp.php',
			'easy-wp-smtp/easy-wp-smtp.php',
			'wp-smtp/wp-smtp.php',
			'newsletter/plugin.php',
			'fluent-smtp/fluent-smtp.php',
			'pepipost-smtp/pepipost-smtp.php',
			'mail-bank/wp-mail-bank.php',
			'smtp-mailer/smtp-mailer.php',
			'suremails/suremails.php',
			'site-mailer/site-mailer.php',
		];

		$active_plugins = (array) get_option( 'active_plugins', [] );
		// For multisite, merge sitewide active plugins.
		if ( is_multisite() ) {
			$network_plugins = (array) get_site_option( 'active_sitewide_plugins', [] );
			$active_plugins  = array_merge( $active_plugins, array_keys( $network_plugins ) );
		}

		return (bool) array_intersect( $smtp_plugins, $active_plugins );
	}

	/**
	 * Apply a filter and return the filtered value only if it's a non-empty array.
	 * Otherwise, return the default array.
	 *
	 * @param string $filter_name The name of the filter to apply.
	 * @param mixed  $default     The default array to return if the filtered result is invalid.
	 * @param mixed  ...$args     Additional arguments to pass to the filter.
	 *
	 * @return array The filtered array if valid, otherwise the default.
	 */
	public static function apply_filters_as_array( $filter_name, $default, ...$args ) {
		// Ensure $default is an array.
		if ( ! is_array( $default ) ) {
			$default = [];
		}

		// Validate the filter name.
		if ( ! is_string( $filter_name ) || empty( $filter_name ) ) {
			return $default;
		}

		// Apply the filter with additional arguments.
		$filtered = apply_filters( $filter_name, $default, ...$args );

		// Return filtered result if it's a non-empty array.
		return is_array( $filtered ) && ! empty( $filtered ) ? $filtered : $default;
	}

	/**
	 * Get forms with entry counts for a specific time period.
	 *
	 * @param int  $timestamp The timestamp to get entries after.
	 * @param int  $limit     Maximum number of forms to return (0 for all).
	 * @param bool $sort      Whether to sort by entry count descending.
	 * @return array Array of form data with entry counts.
	 * @since 1.9.1
	 */
	public static function get_forms_with_entry_counts( $timestamp, $limit = 0, $sort = true ) {
		// Get all published forms with post objects for bulk title access.
		$args = [
			'post_type'              => SRFM_FORMS_POST_TYPE,
			'posts_per_page'         => -1,
			'post_status'            => 'publish',
			'orderby'                => 'ID',
			'order'                  => 'DESC',
			'no_found_rows'          => true,
			'update_post_term_cache' => false,
			'update_post_meta_cache' => false,
		];

		$query = new \WP_Query( $args );

		if ( ! $query->have_posts() ) {
			return [];
		}

		$all_forms = [];

		// Process posts directly from the query results without touching global $post.
		foreach ( $query->posts as $form ) {
			// Ensure we have a valid post object.
			if ( ! $form instanceof \WP_Post ) {
				continue;
			}

			$form_id = (int) $form->ID;
			if ( $form_id <= 0 ) {
				continue;
			}

			// Get entries count after the timestamp for this specific form.
			$entry_count = Entries::get_entries_count_after( $timestamp, $form_id );

			// Get form title directly from post object, use "Blank Form" if empty.
			$form_title = $form->post_title;
			if ( empty( trim( self::get_string_value( $form_title ) ) ) ) {
				$form_title = __( 'Blank Form', 'sureforms' );
			}

			$all_forms[] = [
				'form_id' => $form_id,
				'title'   => $form_title,
				'count'   => $entry_count,
			];
		}

		// Sort by count descending, then by form_id descending for consistency.
		if ( $sort ) {
			usort(
				$all_forms,
				static function( $a, $b ) {
					if ( $a['count'] === $b['count'] ) {
						return $b['form_id'] - $a['form_id'];
					}
					return $b['count'] - $a['count'];
				}
			);
		}

		// Return limited results if specified.
		if ( $limit > 0 ) {
			return array_slice( $all_forms, 0, $limit );
		}

		return $all_forms;
	}

	/**
	 * Check if the given form ID is valid SureForms form ID.
	 * A valid form ID is a numeric value that corresponds to an existing SureForms form in the database.
	 *
	 * @since 1.9.1
	 *
	 * @param int|string|mixed $form_id The form ID to validate.
	 * @return bool True if the form ID is valid, false otherwise.
	 */
	public static function is_valid_form( $form_id ) {

		// Check for a valid form ID.
		if ( empty( $form_id ) || ! is_numeric( $form_id ) ) {
			return false;
		}

		// Check if the form ID exists in the database.
		$form = get_post( self::get_integer_value( $form_id ) );

		// If the form does not exist or is not of the correct post type, return false.
		if ( ! $form || ! is_a( $form, 'WP_Post' ) || SRFM_FORMS_POST_TYPE !== $form->post_type ) {
			return false;
		}

		return true;
	}

	/**
	 * Get the timestamp from a string.
	 *
	 * This function uses WordPress's configured timezone (from Settings → General → Timezone)
	 * to ensure consistent behavior regardless of the server's timezone settings.
	 *
	 * @param string $date The date in YYYY-MM-DD format (e.g., '2026-01-10').
	 * @param string $hours The hours in 12-hour format (e.g., '12', '01'-'12').
	 * @param string $minutes The minutes (e.g., '00', '00'-'59').
	 * @param string $meridiem The meridiem (e.g., 'AM' or 'PM').
	 *
	 * @since 1.10.1
	 * @return int|false The timestamp if successful, false otherwise.
	 */
	public static function get_timestamp_from_string( $date, $hours = '12', $minutes = '00', $meridiem = 'AM' ) {

		if ( empty( $date ) || ! is_string( $date ) ) {
			return false; // Invalid input.
		}

		// Ensure the date is in a valid format of YYYY-MM-DD.
		if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date ) ) {
			return false; // Invalid date format.
		}

		$time_string = $date . ' ' . $hours . ':' . $minutes . ' ' . $meridiem;

		// Convert to timestamp using WordPress timezone.
		// This ensures the date/time is interpreted in the site's configured timezone,
		// not the server's timezone or PHP's default timezone.
		try {
			$datetime = date_create( $time_string, wp_timezone() );

			if ( false === $datetime ) {
				return false;
			}

			$timestamp = $datetime->getTimestamp();

			if ( is_int( $timestamp ) && $timestamp > 0 ) {
				return $timestamp;
			}
		} catch ( \Exception $e ) {
			// If timezone conversion fails, return false.
			return false;
		}

		// If conversion fails, return false.
		return false;
	}

	/**
	 * Generate a unique ID for the saved form.
	 * Also ensures that the generated ID does not already exist in the database table.
	 *
	 * @param class-string $class  The class name where the get method is defined to check for existing IDs.
	 * @param int<1, max>  $length The length of the random bytes to generate. Default is 8.
	 * @return string
	 * @since 2.2.0
	 */
	public static function generate_unique_id( $class, $length = 8 ) {
		// Ensure length is at least 1.
		$length = max( 1, $length );

		do {
			$id = bin2hex( random_bytes( $length ) );
		} while ( is_callable( [ $class, 'get' ] ) && call_user_func( [ $class, 'get' ], $id ) );
		return $id;
	}

	/**
	 * Log error messages to the error log.
	 *
	 * This function checks if error_log function exists, validates the message,
	 * and logs it with the print_r second argument set to true.
	 *
	 * Logging is disabled by default. To enable logging, add this to wp-config.php:
	 * define( 'SRFM_LOG', true );
	 *
	 * @param mixed  $message The error message to log. Can be string or any type.
	 * @param string $prefix Optional prefix to add before the message. Default: 'Log :'.
	 *
	 * @since 2.0.0
	 * @return void
	 */
	public static function srfm_log( $message, $prefix = 'Log :' ) {
		// Check if logging is enabled via SRFM_LOG constant.
		if ( ! defined( 'SRFM_LOG' ) ) {
			return;
		}
		unset( $message, $prefix );
	}

	/**
	 * Encodes data to base64 after JSON encoding with validation.
	 *
	 * This function checks if the data is non-empty and valid for JSON encoding.
	 * If data is not valid, returns an empty string.
	 * Otherwise, it attempts to JSON encode and then base64 encode the result.
	 *
	 * @param mixed $data The data to JSON encode and then base64 encode.
	 * @return string The base64-encoded JSON string, or empty string on failure.
	 */
	public static function srfm_base64_json_encode( $data ) {
		if ( empty( $data ) || ! is_array( $data ) ) {
			return '';
		}

		$json = wp_json_encode( $data );
		if ( false === $json || '' === $json ) {
			return '';
		}

		// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
		return base64_encode( $json );
	}

	/**
	 * Get the visitor's IP address.
	 *
	 * Centralised IP detection that checks common proxy headers before
	 * falling back to REMOTE_ADDR. Handles comma-separated IPs that
	 * load-balancers / CDNs may append (takes the first, i.e. client IP).
	 *
	 * NOTE: Existing callers (Smart_Tags::get_the_user_ip, Front_End::get_user_ip,
	 * inline reads in Form_Submit) can be migrated to this method in the future
	 * to avoid duplicating the same header-chain logic.
	 *
	 * @since 2.8.0
	 * @return string Validated IP address, or empty string if unavailable.
	 */
	public static function get_visitor_ip() {
		$headers = [
			'HTTP_CLIENT_IP',
			'HTTP_X_FORWARDED_FOR',
			'HTTP_X_REAL_IP',
			'HTTP_X_FORWARDED',
			'HTTP_FORWARDED_FOR',
			'HTTP_FORWARDED',
			'REMOTE_ADDR',
		];

		foreach ( $headers as $header ) {
			if ( empty( $_SERVER[ $header ] ) ) {
				continue;
			}

			// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Validated by FILTER_VALIDATE_IP below.
			$raw = wp_unslash( $_SERVER[ $header ] );

			// Proxies may send comma-separated IPs; the first is the original client.
			if ( false !== strpos( $raw, ',' ) ) {
				$raw = trim( explode( ',', $raw )[0] );
			}

			$ip = filter_var( $raw, FILTER_VALIDATE_IP );
			if ( false !== $ip ) {
				/**
				 * Filters the detected visitor IP address.
				 *
				 * @since 2.8.0
				 *
				 * @param string $ip Validated IP address.
				 */
				return apply_filters( 'srfm_visitor_ip', $ip );
			}
		}

		return '';
	}

}
