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

class FrmEntry {

	/**
	 * @since 6.16.3
	 *
	 * @var array
	 */
	private static $unique_id_match_checks = array();

	/**
	 * Create a new entry
	 *
	 * @param array $values
	 *
	 * @return bool|int Entry ID.
	 */
	public static function create( $values ) {
		return self::create_entry( $values, 'standard' );
	}

	/**
	 * Create a new entry with some differences depending on type
	 *
	 * @param array  $values
	 * @param string $type
	 *
	 * @return bool|int Entry ID.
	 */
	private static function create_entry( $values, $type ) {
		$new_values = self::before_insert_entry_in_database( $values, $type );

		// Don't check XML entries for duplicates
		if ( $type !== 'xml' && self::is_duplicate( $new_values, $values ) ) {
			return false;
		}

		return self::continue_to_create_entry( $values, $new_values );
	}

	/**
	 * Flag the memoized unique id check after a new entry is created.
	 * This prevents possibly DB requests and helps avoid issues when creating repeater entries.
	 *
	 * @since 6.16.3
	 *
	 * @param string $unique_id
	 *
	 * @return void
	 */
	private static function flag_new_unique_key( $unique_id ) {
		if ( ! isset( self::$unique_id_match_checks[ $unique_id ] ) ) {
			self::$unique_id_match_checks[ $unique_id ] = false;
		}
	}

	/**
	 * Check for duplicate entries created in the last minute
	 *
	 * @param array $new_values New values.
	 * @param array $values     Values.
	 *
	 * @return bool
	 */
	public static function is_duplicate( $new_values, $values ) {
		$duplicate_entry_time = apply_filters( 'frm_time_to_check_duplicates', 60, $new_values );

		if ( false === self::is_duplicate_check_needed( $values, $duplicate_entry_time ) ) {
			return false;
		}

		if ( self::maybe_check_for_unique_id_match( $values, $new_values['created_at'] ) ) {
			return true;
		}

		$check_val                 = $new_values;
		$check_val['created_at >'] = gmdate( 'Y-m-d H:i:s', strtotime( $new_values['created_at'] ) - absint( $duplicate_entry_time ) );

		unset( $check_val['created_at'], $check_val['updated_at'], $check_val['is_draft'], $check_val['id'], $check_val['item_key'] );

		// phpcs:ignore Universal.Operators.StrictComparisons
		if ( $new_values['item_key'] == $new_values['name'] ) {
			unset( $check_val['name'] );
		}

		$check_val = apply_filters( 'frm_duplicate_check_val', $check_val );

		if ( ! isset( $values['item_meta'] ) ) {
			return false;
		}

		$entry_exists = FrmDb::get_col( 'frm_items', $check_val, 'id', array( 'order_by' => 'created_at DESC' ) );

		if ( ! $entry_exists ) {
			return false;
		}

		global $frm_vars;
		$frm_vars['checking_duplicates'] = true;
		$is_duplicate                    = false;

		foreach ( $entry_exists as $entry_exist ) {
			$is_duplicate = true;

			// Make sure it's a duplicate
			$metas       = FrmEntryMeta::get_entry_meta_info( $entry_exist );
			$field_metas = array();

			foreach ( $metas as $meta ) {
				if ( 0 === (int) $meta->field_id ) {
					continue;
				}

				$field_metas[ $meta->field_id ] = $meta->meta_value;
			}

			$filtered_vals = array_filter( $values['item_meta'] );
			$filtered_vals = self::convert_values_to_their_saved_value( $filtered_vals, $entry_exist );
			$field_metas   = array_filter( $field_metas );

			// If prev entry is empty and current entry is not, they are not duplicates
			if ( ! $field_metas && $filtered_vals ) {
				return false;
			}

			// Compare serialized values and not arrays
			$new_meta = array_map( 'maybe_serialize', $filtered_vals );

			if ( $field_metas === $new_meta ) {
				$is_duplicate = true;
				break;
			}

			if ( count( $field_metas ) !== count( $new_meta ) ) {
				// TODO: compare values saved in the post also
				$is_duplicate = false;
				continue;
			}

			$diff = array_diff_assoc( $field_metas, $new_meta );

			foreach ( $diff as $meta_value ) {
				if ( $meta_value ) {
					$is_duplicate = false;
				}
			}

			if ( $is_duplicate ) {
				break;
			}
		}//end foreach

		$frm_vars['checking_duplicates'] = false;

		return $is_duplicate;
	}

	/**
	 * @since 6.16.3
	 *
	 * @param array  $values     POST request data.
	 * @param string $created_at The timestamp of the entry we are checking for.
	 *
	 * @return bool
	 */
	private static function maybe_check_for_unique_id_match( $values, $created_at ) {
		if ( ! self::should_check_for_unique_id_match() ) {
			return false;
		}

		if ( empty( $values['unique_id'] ) ) {
			return false;
		}

		$unique_id = sanitize_key( $values['unique_id'] );

		if ( ! $unique_id ) {
			// Only continue if a unique ID was generated on form submit.
			return false;
		}

		if ( isset( self::$unique_id_match_checks[ $unique_id ] ) ) {
			return self::$unique_id_match_checks[ $unique_id ];
		}

		$timestamp = strtotime( $created_at );

		if ( false === $timestamp ) {
			$timestamp = time();
		}

		self::$unique_id_match_checks[ $unique_id ] = (bool) FrmDb::get_var(
			'frm_item_metas',
			array(
				'field_id'     => 0,
				'meta_value'   => serialize( compact( 'unique_id' ) ),
				'created_at >' => gmdate( 'Y-m-d H:i:s', $timestamp - MONTH_IN_SECONDS ),
			),
			'id'
		);

		return self::$unique_id_match_checks[ $unique_id ];
	}

	/**
	 * @since 6.16.3
	 *
	 * @return bool
	 */
	private static function should_check_for_unique_id_match() {
		/**
		 * Allow users to opt out of the DB query, in case it causes performance issues.
		 *
		 * @since 6.16.3
		 *
		 * @param bool $should_extend
		 */
		$should_check = apply_filters( 'frm_check_for_unique_id_match', true );
		return (bool) $should_check;
	}

	/**
	 * Convert form data to the actual value that would be saved into the database.
	 *
	 * This is important for the duplicate check as something like 'a:2:{s:5:"typed";s:0:"";s:6:"output";s:0:"";}'
	 * (a signature value) is actually an empty string and does not get saved.
	 *
	 * @param array $filter_vals
	 * @param int   $entry_id
	 *
	 * @return array
	 */
	private static function convert_values_to_their_saved_value( $filter_vals, $entry_id ) {
		$reduced = array();

		foreach ( $filter_vals as $field_id => $value ) {
			$field                = FrmFieldFactory::get_field_object( $field_id );
			$reduced[ $field_id ] = $field->get_value_to_save( $value, array( 'entry_id' => $entry_id ) );
			$reduced[ $field_id ] = $field->set_value_before_save( $reduced[ $field_id ] );

			if ( '' === $reduced[ $field_id ] || array() === $reduced[ $field_id ] ) {
				unset( $reduced[ $field_id ] );
			}
		}

		return $reduced;
	}

	/**
	 * Determine if an entry needs to be checked as a possible duplicate
	 *
	 * @since 2.0.23
	 *
	 * @param array $values
	 * @param int   $duplicate_entry_time
	 *
	 * @return bool
	 */
	private static function is_duplicate_check_needed( $values, $duplicate_entry_time ) {
		// If time for checking duplicates is set to an empty value, don't check for duplicates
		if ( ! $duplicate_entry_time ) {
			return false;
		}

		// If CSV is importing, don't check for duplicates
		if ( defined( 'WP_IMPORTING' ) && WP_IMPORTING ) {
			return false;
		}

		// If repeating field entries are getting created, don't check for duplicates
		return empty( $values['parent_form_id'] );
	}

	/**
	 * @param int|string $id
	 *
	 * @return false|int
	 */
	public static function duplicate( $id ) {
		global $wpdb;

		$values                   = self::getOne( $id );
		$new_values               = array();
		$new_values['item_key']   = FrmAppHelper::get_unique_key( '', $wpdb->prefix . 'frm_items', 'item_key' );
		$new_values['name']       = $values->name;
		$new_values['is_draft']   = $values->is_draft;
		$new_values['user_id']    = (int) $values->user_id;
		$new_values['updated_by'] = (int) $values->user_id;
		$new_values['form_id']    = $values->form_id ? (int) $values->form_id : null;
		$new_values['created_at'] = current_time( 'mysql', 1 );
		$new_values['updated_at'] = $new_values['created_at'];

		$query_results = $wpdb->insert( $wpdb->prefix . 'frm_items', $new_values );

		if ( ! $query_results ) {
			return false;
		}

		$entry_id = $wpdb->insert_id;

		global $frm_vars;

		if ( ! isset( $frm_vars['saved_entries'] ) ) {
			$frm_vars['saved_entries'] = array();
		}
		$frm_vars['saved_entries'][] = (int) $entry_id;

		FrmEntryMeta::duplicate_entry_metas( $id, $entry_id );
		self::clear_cache();

		do_action( 'frm_after_duplicate_entry', $entry_id, $new_values['form_id'], array( 'old_id' => $id ) );

		return $entry_id;
	}

	/**
	 * Update an entry (not via XML)
	 *
	 * @param int   $id
	 * @param array $values
	 *
	 * @return bool|int Update results.
	 */
	public static function update( $id, $values ) {
		return self::update_entry( $id, $values, 'standard' );
	}

	/**
	 * Update an entry with some differences depending on the update type
	 *
	 * @since 2.0.16
	 *
	 * @param int    $id
	 * @param array  $values
	 * @param string $update_type
	 *
	 * @return bool|int Query results.
	 */
	private static function update_entry( $id, $values, $update_type ) {
		global $wpdb;

		$update = self::before_update_entry( $id, $values, $update_type );

		if ( ! $update ) {
			return false;
		}

		$new_values    = self::package_entry_to_update( $id, $values );
		$query_results = $wpdb->update( $wpdb->prefix . 'frm_items', $new_values, compact( 'id' ) );

		self::after_update_entry( $query_results, $id, $values, $new_values );

		return $query_results;
	}

	/**
	 * Delete an entry.
	 *
	 * @param int|string $id
	 *
	 * @return bool True on success, false if nothing was deleted.
	 */
	public static function destroy( $id ) {
		global $wpdb;
		$id = (int) $id;

		// Item meta is required for conditional logic in actions with 'delete' events.
		$entry = self::getOne( $id, true );

		if ( ! $entry ) {
			return false;
		}

		/**
		 * Trigger an action to run custom logic before the entry is deleted.
		 *
		 * @param int      $id    The id of the entry that was destroyed.
		 * @param stdClass $entry The entry object.
		 */
		do_action( 'frm_before_destroy_entry', $id, $entry );

		$wpdb->query( $wpdb->prepare( 'DELETE FROM ' . $wpdb->prefix . 'frm_item_metas WHERE item_id=%d', $id ) );
		$result = $wpdb->query( $wpdb->prepare( 'DELETE FROM ' . $wpdb->prefix . 'frm_items WHERE id=%d', $id ) );

		self::clear_cache();

		/**
		 * Trigger an action to run custom logic after the entry is deleted.
		 * Use this hook if you need to update caching after an entry is deleted.
		 *
		 * @since 5.4.1
		 *
		 * @param int      $id    The id of the entry that was destroyed.
		 * @param stdClass $entry The entry object.
		 */
		do_action( 'frm_after_destroy_entry', $id, $entry );

		return $result;
	}

	/**
	 * @param int        $id
	 * @param mixed      $value
	 * @param int|string $form_id
	 *
	 * @return false|int
	 */
	public static function update_form( $id, $value, $form_id ) {
		global $wpdb;
		$form_id = isset( $value ) ? $form_id : null;
		$result  = $wpdb->update( $wpdb->prefix . 'frm_items', array( 'form_id' => $form_id ), array( 'id' => $id ) );

		if ( $result ) {
			self::clear_cache();
		}

		return $result;
	}

	/**
	 * Clear entry caching
	 * Called when an entry is changed
	 *
	 * @since 2.0.5
	 *
	 * @return void
	 */
	public static function clear_cache() {
		FrmDb::cache_delete_group( 'frm_entry' );
		FrmDb::cache_delete_group( 'frm_item' );
		FrmDb::cache_delete_group( 'frm_entry_meta' );
		FrmDb::cache_delete_group( 'frm_item_meta' );
	}

	/**
	 * After switching to the wp_loaded hook for processing entries,
	 * we can no longer use 'name', but check it as a fallback
	 *
	 * @since 2.0.11
	 *
	 * @param array        $values
	 * @param array|string $default
	 *
	 * @return string
	 */
	public static function get_new_entry_name( $values, $default = '' ) {
		$name = $values['item_name'] ?? $values['name'] ?? $default;
		return is_array( $name ) ? reset( $name ) : $name;
	}

	/**
	 * If $entry is numeric, get the entry object
	 *
	 * @since 2.0.9
	 *
	 * @param int|object|string $entry By reference.
	 *
	 * @return void
	 */
	public static function maybe_get_entry( &$entry ) {
		if ( $entry && is_numeric( $entry ) ) {
			$entry = self::getOne( $entry );
		} elseif ( ! $entry || 'false' === $entry ) {
			$entry = false;
		}
	}

	/**
	 * @param int|string $id
	 * @param bool       $meta
	 *
	 * @return object|null
	 */
	public static function getOne( $id, $meta = false ) {
		global $wpdb;

		$query = "SELECT it.*, fr.name as form_name, fr.form_key as form_key FROM {$wpdb->prefix}frm_items it
                  LEFT OUTER JOIN {$wpdb->prefix}frm_forms fr ON it.form_id=fr.id WHERE ";

		$query     .= is_numeric( $id ) ? 'it.id=%d' : 'it.item_key=%s';
		$query_args = array( $id );
		$query      = $wpdb->prepare( $query, $query_args ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

		if ( ! $meta ) {
			$entry = FrmDb::check_cache( $id . '_nometa', 'frm_entry', $query, 'get_row' );
			self::prepare_entry( $entry );
			return $entry;
		}

		$entry = FrmDb::check_cache( $id, 'frm_entry' );

		if ( $entry !== false ) {
			self::prepare_entry( $entry );
			return $entry;
		}

		$entry = $wpdb->get_row( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$entry = self::get_meta( $entry );
		self::prepare_entry( $entry );

		return $entry;
	}

	/**
	 * @since 4.02.03
	 *
	 * @param object $entry
	 *
	 * @return void
	 */
	private static function prepare_entry( &$entry ) {
		if ( ! $entry ) {
			return;
		}

		FrmAppHelper::unserialize_or_decode( $entry->description );
		// TODO: Remove slashes on input only, not output.
		$entry = wp_unslash( $entry );
	}

	/**
	 * @since 4.02.03
	 *
	 * @param array $entries
	 *
	 * @return void
	 */
	private static function prepare_entries( &$entries ) {
		foreach ( $entries as $k => $entry ) {
			self::prepare_entry( $entry );
			$entries[ $k ] = $entry;
		}
	}

	/**
	 * @param object|null $entry
	 *
	 * @return object|null
	 */
	public static function get_meta( $entry ) {
		if ( ! $entry ) {
			return $entry;
		}

		global $wpdb;
		$metas = FrmDb::get_results(
			$wpdb->prefix . 'frm_item_metas m LEFT JOIN ' . $wpdb->prefix . 'frm_fields f ON m.field_id=f.id',
			array(
				'item_id'    => $entry->id,
				'field_id !' => 0,
			),
			'field_id, meta_value, field_key, item_id, f.type'
		);

		$entry->metas = array();

		$include_key = apply_filters( 'frm_include_meta_keys', false, array( 'form_id' => $entry->form_id ) );

		foreach ( $metas as $meta_val ) {
			FrmFieldsHelper::prepare_field_value( $meta_val->meta_value, $meta_val->type );

			if ( (int) $meta_val->item_id === (int) $entry->id ) {
				$entry->metas[ $meta_val->field_id ] = $meta_val->meta_value;

				if ( $include_key ) {
					$entry->metas[ $meta_val->field_key ] = $entry->metas[ $meta_val->field_id ];
				}
				continue;
			}

			// Include sub entries in an array
			if ( ! isset( $entry->metas[ $meta_val->field_id ] ) ) {
				$entry->metas[ $meta_val->field_id ] = array();
			}

			$entry->metas[ $meta_val->field_id ][] = $meta_val->meta_value;

			unset( $meta_val );
		}//end foreach
		unset( $metas );

		FrmDb::set_cache( $entry->id, $entry, 'frm_entry' );

		return $entry;
	}

	/**
	 * @param string $id
	 *
	 * @return bool
	 */
	public static function exists( $id ) {
		if ( FrmDb::check_cache( $id, 'frm_entry' ) ) {
			return true;
		}

		$where = is_numeric( $id ) ? array( 'id' => $id ) : array( 'item_key' => $id );
		$id    = FrmDb::get_var( 'frm_items', $where );

		return $id && $id > 0;
	}

	/**
	 * @param array|string $where
	 * @param string       $order_by
	 * @param string       $limit
	 * @param bool         $meta
	 * @param bool         $inc_form
	 *
	 * @return array
	 */
	public static function getAll( $where, $order_by = '', $limit = '', $meta = false, $inc_form = true ) {
		global $wpdb;

		$limit     = FrmDb::esc_limit( $limit );
		$cache_key = FrmAppHelper::maybe_json_encode( $where ) . $order_by . $limit . $inc_form;
		$entries   = wp_cache_get( $cache_key, 'frm_entry' );

		if ( false === $entries ) {
			$fields = 'it.id, it.item_key, it.name, it.ip, it.form_id, it.post_id, it.user_id, it.parent_item_id, it.updated_by, it.created_at, it.updated_at, it.is_draft, it.description'; // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
			$table  = $wpdb->prefix . 'frm_items it ';

			if ( $inc_form ) {
				$fields = 'it.*, fr.name as form_name,fr.form_key as form_key';
				$table .= 'LEFT OUTER JOIN ' . $wpdb->prefix . 'frm_forms fr ON it.form_id=fr.id ';
			}

			if ( preg_match( '/ meta_([0-9]+)/', $order_by, $order_matches ) ) {
				$fields .= self::sort_by_field( $order_matches[1] );
				unset( $order_matches );
			}

			// Prepare the query
			$query = 'SELECT ' . $fields . ' FROM ' . $table . FrmDb::prepend_and_or_where( ' WHERE ', $where ) . $order_by . $limit;

			$entries = $wpdb->get_results( $query, OBJECT_K ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			unset( $query );

			FrmDb::set_cache( $cache_key, $entries, 'frm_entry' );
		}//end if

		if ( ! $meta || ! $entries ) {
			self::prepare_entries( $entries );
			return $entries;
		}
		unset( $meta );

		if ( ! is_array( $where ) && preg_match( '/^it\.form_id=\d+$/', $where ) ) {
			$where = array( 'it.form_id' => substr( $where, 11 ) );
		}

		$meta_where = array( 'field_id !' => 0 );

		// phpcs:ignore Universal.Operators.StrictComparisons
		if ( $limit == '' && is_array( $where ) && count( $where ) === 1 && isset( $where['it.form_id'] ) ) {
			$meta_where['fi.form_id'] = $where['it.form_id'];
		} else {
			$meta_where['item_id'] = array_keys( $entries );
		}

		$metas = FrmDb::get_results(
			$wpdb->prefix . 'frm_item_metas it LEFT OUTER JOIN ' . $wpdb->prefix . 'frm_fields fi ON it.field_id = fi.id',
			$meta_where,
			'item_id, meta_value, field_id, field_key, form_id, fi.type'
		);

		unset( $meta_where );

		if ( ! $metas ) {
			self::prepare_entries( $entries );
			return $entries;
		}

		foreach ( $metas as $m_key => $meta_val ) {
			if ( ! isset( $entries[ $meta_val->item_id ] ) ) {
				continue;
			}

			if ( ! isset( $entries[ $meta_val->item_id ]->metas ) ) {
				$entries[ $meta_val->item_id ]->metas = array();
			}

			FrmFieldsHelper::prepare_field_value( $meta_val->meta_value, $meta_val->type );
			$entries[ $meta_val->item_id ]->metas[ $meta_val->field_id ] = $meta_val->meta_value;
			unset( $m_key, $meta_val );
		}

		if ( ! FrmAppHelper::prevent_caching() ) {
			foreach ( $entries as $entry ) {
				FrmDb::set_cache( $entry->id, $entry, 'frm_entry' );
				unset( $entry );
			}
		}

		self::prepare_entries( $entries );
		return $entries;
	}

	/**
	 * @param int|string $field_id
	 *
	 * @return string
	 */
	private static function sort_by_field( $field_id ) {
		global $wpdb;
		$field_id      = (int) $field_id;
		$field_options = FrmDb::get_var( 'frm_fields', array( 'id' => $field_id ), 'field_options' );
		FrmAppHelper::unserialize_or_decode( $field_options );

		if ( empty( $field_options['post_field'] ) ) {
			$sort = ', (SELECT meta_value FROM ' . $wpdb->prefix . 'frm_item_metas WHERE field_id = ' . $field_id . ' AND item_id = it.id) as meta_' . $field_id;
		} else {
			$sort = '';
		}

		return apply_filters( 'frm_handle_field_column_sort', $sort, $field_id, $field_options );
	}

	// Pagination Methods
	/**
	 * @param array|int|string $where If int, use the form id.
	 *
	 * @return int|string
	 */
	public static function getRecordCount( $where = '' ) {
		global $wpdb;
		$table_join = $wpdb->prefix . 'frm_items it JOIN ' . $wpdb->prefix . 'frm_forms fr ON it.form_id=fr.id';

		if ( is_numeric( $where ) ) {
			$table_join = 'frm_items';
			$where      = array( 'form_id' => $where );
		}

		if ( is_array( $where ) ) {
			return FrmDb::get_count( $table_join, $where );
		}

		$cache_key = 'count_' . FrmAppHelper::maybe_json_encode( $where );
		$query     = 'SELECT COUNT(*) FROM ' . $table_join . FrmDb::prepend_and_or_where( ' WHERE ', $where );

		return FrmDb::check_cache( $cache_key, 'frm_entry', $query, 'get_var' );
	}

	/**
	 * @param int|string       $p_size
	 * @param array|int|string $where
	 *
	 * @return int
	 */
	public static function getPageCount( $p_size, $where = '' ) {
		$p_size = (int) $p_size;

		if ( $p_size ) {
			if ( ! is_numeric( $where ) ) {
				$where = self::getRecordCount( $where );
			}

			return ceil( (int) $where / $p_size );
		}

		return 1;
	}

	/**
	 * Prepare the data before inserting it into the database
	 *
	 * @since 2.0.16
	 *
	 * @param array  $values
	 * @param string $type
	 *
	 * @return array New values.
	 */
	private static function before_insert_entry_in_database( &$values, $type ) {
		self::sanitize_entry_post( $values );

		if ( $type !== 'xml' ) {
			$values = apply_filters( 'frm_pre_create_entry', $values );
		}

		return self::package_entry_data( $values );
	}

	/**
	 * Create an entry and perform after create actions
	 *
	 * @since 2.0.16
	 *
	 * @param array $values
	 * @param array $new_values
	 *
	 * @return bool|int Entry ID.
	 */
	private static function continue_to_create_entry( $values, $new_values ) {
		$entry_id = self::insert_entry_into_database( $new_values );

		if ( ! $entry_id ) {
			return false;
		}

		self::after_insert_entry_in_database( $values, $new_values, $entry_id );

		return $entry_id;
	}

	/**
	 * Sanitize the POST values before we use them
	 *
	 * @since 2.0
	 *
	 * @param array $values The POST values by reference.
	 *
	 * @return void
	 */
	public static function sanitize_entry_post( &$values ) {
		$sanitize_method = array(
			'form_id'          => 'absint',
			'frm_action'       => 'sanitize_title',
			'form_key'         => 'sanitize_title',
			'item_key'         => 'sanitize_title',
			'item_name'        => 'sanitize_text_field',
			'frm_saving_draft' => 'absint',
			'is_draft'         => 'absint',
			'post_id'          => 'absint',
			'parent_item_id'   => 'absint',
			'created_at'       => 'sanitize_text_field',
			'updated_at'       => 'sanitize_text_field',
		);

		FrmAppHelper::sanitize_request( $sanitize_method, $values );
	}

	/**
	 * Prepare the new values for inserting into the database
	 *
	 * @since 2.0.16
	 *
	 * @param array $values
	 *
	 * @return array New values.
	 */
	private static function package_entry_data( &$values ) {
		global $wpdb;

		if ( ! isset( $values['item_key'] ) ) {
			$values['item_key'] = '';
		}

		$item_name  = self::get_new_entry_name( $values, $values['item_key'] );
		$new_values = array(
			'item_key'       => FrmAppHelper::get_unique_key( $values['item_key'], $wpdb->prefix . 'frm_items', 'item_key' ),
			'name'           => FrmAppHelper::truncate( $item_name, 255, 1, '', true ),
			'ip'             => self::get_ip( $values ),
			'is_draft'       => self::get_is_draft_value( $values ),
			'form_id'        => (int) self::get_entry_value( $values, 'form_id', null ),
			'post_id'        => (int) self::get_entry_value( $values, 'post_id', 0 ),
			'parent_item_id' => (int) self::get_entry_value( $values, 'parent_item_id', 0 ),
			'created_at'     => self::get_created_at( $values ),
			'updated_at'     => self::get_updated_at( $values ),
			'description'    => self::get_entry_description( $values ),
			'user_id'        => self::get_entry_user_id( $values ),
		);

		$new_values['updated_by'] = $values['updated_by'] ?? $new_values['user_id'];

		return $new_values;
	}

	/**
	 * @param array  $values
	 * @param string $name
	 * @param mixed  $default
	 *
	 * @return mixed
	 */
	private static function get_entry_value( $values, $name, $default ) {
		return $values[ $name ] ?? $default;
	}

	/**
	 * Get the ip for a new entry.
	 * Allow the import to override the value.
	 *
	 * @since 2.03.10
	 *
	 * @param array $values
	 *
	 * @return string
	 */
	private static function get_ip( $values ) {
		if ( ! FrmAppHelper::ips_saved() ) {
			return '';
		}

		$ip = FrmAppHelper::get_ip_address();

		if ( defined( 'WP_IMPORTING' ) && WP_IMPORTING ) {
			return self::get_entry_value( $values, 'ip', $ip );
		}

		return $ip;
	}

	/**
	 * Get the is_draft value for a new entry
	 *
	 * @since 2.0.16
	 *
	 * @param array $values
	 *
	 * @return int
	 */
	private static function get_is_draft_value( $values ) {
		if ( isset( $values['frm_saving_draft'] ) && FrmEntriesHelper::DRAFT_ENTRY_STATUS === (int) $values['frm_saving_draft'] ) {
			return FrmEntriesHelper::DRAFT_ENTRY_STATUS;
		}

		return isset( $values['is_draft'] ) ? absint( $values['is_draft'] ) : FrmEntriesHelper::SUBMITTED_ENTRY_STATUS;
	}

	/**
	 * Get the created_at value for a new entry
	 *
	 * @since 2.0.16
	 *
	 * @param array $values
	 *
	 * @return string
	 */
	private static function get_created_at( $values ) {
		return self::get_entry_value( $values, 'created_at', current_time( 'mysql', 1 ) );
	}

	/**
	 * Get the updated_at value for a new entry
	 *
	 * @since 2.0.16
	 *
	 * @param array $values
	 *
	 * @return string
	 */
	private static function get_updated_at( $values ) {
		return $values['updated_at'] ?? self::get_created_at( $values );
	}

	/**
	 * Get the description value for a new entry
	 *
	 * @since 2.0.16
	 *
	 * @param array $values
	 *
	 * @return string
	 */
	private static function get_entry_description( $values ) {
		if ( ! empty( $values['description'] ) ) {
			return FrmAppHelper::maybe_json_encode( $values['description'] );
		}

		return json_encode(
			array(
				'browser'  => FrmAppHelper::get_server_value( 'HTTP_USER_AGENT' ),
				'referrer' => FrmAppHelper::get_server_value( 'HTTP_REFERER' ),
			)
		);
	}

	/**
	 * Get the user_id value for a new entry
	 *
	 * @since 2.0.16
	 *
	 * @param array $values
	 *
	 * @return int
	 */
	private static function get_entry_user_id( $values ) {
		if ( isset( $values['frm_user_id'] ) && ( is_numeric( $values['frm_user_id'] ) || FrmAppHelper::is_admin() ) ) {
			return $values['frm_user_id'];
		}

		$current_user_id = get_current_user_id();
		return $current_user_id ? $current_user_id : 0;
	}

	/**
	 * Insert new entry into the database
	 *
	 * @since 2.0.16
	 *
	 * @param array $new_values
	 *
	 * @return bool|int Entry ID.
	 */
	private static function insert_entry_into_database( $new_values ) {
		global $wpdb;

		$query_results = $wpdb->insert( $wpdb->prefix . 'frm_items', $new_values );

		return $query_results ? $wpdb->insert_id : false;
	}

	/**
	 * Add the new entry to global $frm_vars
	 *
	 * @since 2.0.16
	 *
	 * @param int|string $entry_id
	 *
	 * @return void
	 */
	private static function add_new_entry_to_frm_vars( $entry_id ) {
		global $frm_vars;

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

		$frm_vars['saved_entries'][] = (int) $entry_id;
	}

	/**
	 * Add entry metas, if there are any
	 *
	 * @since 2.0.16
	 *
	 * @param array      $values
	 * @param int|string $entry_id
	 *
	 * @return void
	 */
	private static function maybe_add_entry_metas( $values, $entry_id ) {
		if ( isset( $values['item_meta'] ) ) {
			FrmEntryMeta::update_entry_metas( $entry_id, $values['item_meta'] );
			self::maybe_add_unique_id_meta( $values, $entry_id );
		}
		self::maybe_add_captcha_meta( (int) $values['form_id'], (int) $entry_id );
	}

	/**
	 * @since 6.16.3
	 *
	 * @param array $values
	 * @param int   $entry_id
	 *
	 * @return void
	 */
	private static function maybe_add_unique_id_meta( $values, $entry_id ) {
		if ( ! empty( $values['parent_form_id'] ) || empty( $values['unique_id'] ) || ! self::should_check_for_unique_id_match() ) {
			return;
		}

		// This unique ID is inserted with JS on form submit.
		// It is used to check for duplicate entries.
		$unique_id = sanitize_key( $values['unique_id'] );

		if ( ! $unique_id ) {
			return;
		}

		FrmEntryMeta::add_entry_meta( $entry_id, 0, '', compact( 'unique_id' ) );
		self::flag_new_unique_key( $unique_id );
	}

	/**
	 * @since 5.0.15
	 *
	 * @param int $form_id
	 * @param int $entry_id
	 *
	 * @return void
	 */
	private static function maybe_add_captcha_meta( $form_id, $entry_id ) {
		global $frm_vars;

		if ( ! array_key_exists( 'captcha_scores', $frm_vars ) || ! array_key_exists( $form_id, $frm_vars['captcha_scores'] ) ) {
			return;
		}

		$captcha_score_meta = array( 'captcha_score' => $frm_vars['captcha_scores'][ $form_id ] );
		FrmEntryMeta::add_entry_meta( $entry_id, 0, '', maybe_serialize( $captcha_score_meta ) );
	}

	/**
	 * Trigger frm_after_create_entry hooks
	 *
	 * @since 2.0.16
	 *
	 * @param int   $entry_id
	 * @param array $values
	 * @param array $new_values
	 *
	 * @return void
	 */
	private static function after_entry_created_actions( $entry_id, $values, $new_values ) {
		// This is a child entry.
		$is_child = isset( $values['parent_nonce'] ) && ! empty( $values['parent_form_id'] ) && wp_verify_nonce( $values['parent_nonce'], 'parent' );

		do_action( 'frm_after_create_entry', $entry_id, $new_values['form_id'], compact( 'is_child' ) );
		do_action( 'frm_after_create_entry_' . $new_values['form_id'], $entry_id, compact( 'is_child' ) );

		if ( ! empty( $values['form_key'] ) ) {
			/**
			 * @since 6.30
			 *
			 * @param int   $entry_id
			 * @param array $is_child
			 */
			do_action( 'frm_after_create_entry_' . $values['form_key'], $entry_id, compact( 'is_child' ) );
		}
	}

	/**
	 * Actions to perform immediately after an entry is inserted in the frm_items database
	 *
	 * @since 2.0.16
	 *
	 * @param array $values
	 * @param array $new_values
	 * @param int   $entry_id
	 *
	 * @return void
	 */
	private static function after_insert_entry_in_database( $values, $new_values, $entry_id ) {
		self::add_new_entry_to_frm_vars( $entry_id );

		self::maybe_add_entry_metas( $values, $entry_id );

		self::clear_cache();

		self::after_entry_created_actions( $entry_id, $values, $new_values );
	}

	/**
	 * Perform some actions right before updating an entry
	 *
	 * @since 2.0.16
	 *
	 * @param int|string $id
	 * @param array      $values
	 * @param string     $update_type
	 *
	 * @return bool Update.
	 */
	private static function before_update_entry( $id, &$values, $update_type ) {
		$update = true;

		global $frm_vars;

		// phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
		if ( isset( $frm_vars['saved_entries'] ) && is_array( $frm_vars['saved_entries'] ) && in_array( (int) $id, $frm_vars['saved_entries'] ) ) {
			$update = false;
		}

		if ( $update && $update_type !== 'xml' ) {
			$values = apply_filters( 'frm_pre_update_entry', $values, $id );
		}

		return $update;
	}

	/**
	 * Package the entry data for updating
	 *
	 * @since 2.0.16
	 *
	 * @param int   $id
	 * @param array $values
	 *
	 * @return array New values.
	 */
	private static function package_entry_to_update( $id, $values ) {
		global $wpdb;

		$new_values = array(
			'name'       => FrmAppHelper::truncate( self::get_new_entry_name( $values ), 255, 1, '', true ),
			'form_id'    => (int) self::get_entry_value( $values, 'form_id', null ),
			'is_draft'   => self::get_is_draft_value( $values ),
			'updated_at' => current_time( 'mysql', 1 ),
			'updated_by' => $values['updated_by'] ?? get_current_user_id(),
		);

		if ( isset( $values['post_id'] ) ) {
			$new_values['post_id'] = (int) $values['post_id'];
		}

		if ( isset( $values['item_key'] ) ) {
			$new_values['item_key'] = FrmAppHelper::get_unique_key( $values['item_key'], $wpdb->prefix . 'frm_items', 'item_key', $id );
		}

		if ( isset( $values['parent_item_id'] ) ) {
			$new_values['parent_item_id'] = (int) $values['parent_item_id'];
		}

		if ( isset( $values['frm_user_id'] ) && is_numeric( $values['frm_user_id'] ) ) {
			$new_values['user_id'] = $values['frm_user_id'];
		}

		return apply_filters( 'frm_update_entry', $new_values, $id );
	}

	/**
	 * Perform some actions right after updating an entry
	 *
	 * @since 2.0.16
	 *
	 * @param bool|int   $query_results
	 * @param int|string $id
	 * @param array      $values
	 * @param array      $new_values
	 *
	 * @return void
	 */
	private static function after_update_entry( $query_results, $id, $values, $new_values ) {
		if ( $query_results ) {
			self::clear_cache();
		}

		global $frm_vars;

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

		$frm_vars['saved_entries'][] = (int) $id;

		if ( isset( $values['item_meta'] ) ) {
			FrmEntryMeta::update_entry_metas( $id, $values['item_meta'] );
		}

		do_action( 'frm_after_update_entry', $id, $new_values['form_id'] );
		do_action( 'frm_after_update_entry_' . $new_values['form_id'], $id );

		if ( ! empty( $values['form_key'] ) ) {
			/**
			 * @since 6.30
			 *
			 * @param int $entry_id
			 */
			do_action( 'frm_after_update_entry_' . $values['form_key'], $id );
		}
	}

	/**
	 * Create entry from an XML import
	 * Certain actions aren't necessary when importing (like saving sub entries, checking for duplicates, etc.)
	 *
	 * @since 2.0.16
	 *
	 * @param array $values
	 *
	 * @return bool|int Entry ID.
	 */
	public static function create_entry_from_xml( $values ) {
		return self::create_entry( $values, 'xml' );
	}

	/**
	 * Update entry from an XML import
	 * Certain actions aren't necessary when importing (like saving sub entries and modifying other vals)
	 *
	 * @since 2.0.16
	 *
	 * @param int   $id
	 * @param array $values
	 *
	 * @return bool|int Updated.
	 */
	public static function update_entry_from_xml( $id, $values ) {
		return self::update_entry( $id, $values, 'xml' );
	}

	/**
	 * @param string $key
	 *
	 * @return int entry_id
	 */
	public static function get_id_by_key( $key ) {
		$entry_id = FrmDb::get_var( 'frm_items', array( 'item_key' => sanitize_title( $key ) ) );
		return (int) $entry_id;
	}

	/**
	 * Get entries count.
	 *
	 * @since 6.8
	 *
	 * @return int|string
	 */
	public static function get_entries_count() {
		$args = array(
			'or'               => 1,
			'parent_form_id'   => null,
			'parent_form_id <' => 1,
		);
		return self::getRecordCount( $args );
	}
}
