<?php
/**
 * Manage Contact Module database related operations.
 *
 * @package Mint\MRM\DataBase\Models
 * @namespace Mint\MRM\DataBase\Models
 * @author [MRM Team]
 * @email [support@getwpfunnels.com]
 * @create date 2022-08-09 11:03:17
 * @modify date 2022-08-09 11:03:17
 */

namespace Mint\MRM\DataBase\Models;

use Mint\MRM\DataBase\Tables\ContactGroupPivotSchema;
use Mint\MRM\DataBase\Tables\ContactMetaSchema;
use Mint\MRM\DataBase\Tables\ContactNoteSchema;
use Mint\MRM\DataBase\Tables\ContactSchema;
use Mint\MRM\DataBase\Tables\EmailSchema;
use Mint\MRM\DataBase\Tables\FormMetaSchema;
use Mint\MRM\DataStores\ContactData;
use Mint\MRM\Utilites\Helper\Contact;
use MRM\Common\MrmCommon;
use Mint\Mrm\Internal\Traits\Singleton;

/**
 * ContactModel class
 *
 * Manage Contact Module database related operations.
 *
 * CRUD methods (insert, update, get, get_all, destroy, destroy_all, is_contact_exist,
 * is_contact_exist_by_id) have moved to ContactRepository. This class is preserved
 * for non-CRUD consumers (automation, forms, WP-CLI, Pro plugin hooks).
 *
 * @package Mint\MRM\DataBase\Models
 * @namespace Mint\MRM\DataBase\Models
 *
 * @version 1.0.0
 */
class ContactModel {

	use Singleton;

	/**
	 * Insert contact information to database
	 *
	 * @param ContactData $contact contact.
	 *
	 * @return bool|int
	 * @since 1.0.0
	 */
	public static function insert( ContactData $contact ) {
		global $wpdb;

		$contacts_table = $wpdb->prefix . ContactSchema::$table_name;

		try {
			$wpdb->insert(
				$contacts_table,
				array(
					'email'      => $contact->get_email(),
					'first_name' => $contact->get_first_name(),
					'last_name'  => $contact->get_last_name(),
					'status'     => $contact->get_status(),
					'source'     => $contact->get_source(),
					'hash'       => MrmCommon::get_rand_hash( $contact->get_email() ),
					'created_by' => $contact->get_created_by(),
					'wp_user_id' => $contact->get_wp_user_id(),
					'created_at' => current_time( 'mysql' ),
				)
			); // db call ok. ; no-cache ok.

			$insert_id = $wpdb->insert_id;

			$meta_fields['meta_fields'] = array();

			if ( ! empty( $contact->get_meta_fields() ) ) {
				$meta_fields['meta_fields'] = $contact->get_meta_fields();
			}

			if ( 'subscribed' === $contact->get_status() ) {
				$meta_fields[ 'meta_fields' ][ 'status_changed' ] = $contact->get_status();
			}

			if ( !empty( $meta_fields['meta_fields'] ) ) {
				self::update_meta_fields( $insert_id, $meta_fields );
			}

			/**
			 * Fires after a contact is created.
			 *
			 * @param int $insert_id The ID of the newly created contact.
			 * @since 1.14.4
			 */
			do_action('mint_after_contact_creation', $insert_id);
			return $insert_id;
		} catch ( \Exception $e ) {
			return false;
		}
	}


	/**
	 * Update a contact information
	 *
	 * @param mixed $args Entity and value to update.
	 * @param mixed $contact_id Contact ID.
	 *
	 * @return bool
	 * @since 1.0.0
	 */
	public static function update( $args, $contact_id ) {
		global $wpdb;
		$contacts_table = $wpdb->prefix . ContactSchema::$table_name;

		if ( !empty( $args[ 'status' ] ) && 'subscribed' === $args[ 'status' ] ) {
			$args[ 'meta_fields' ][ 'status_changed' ] = $args[ 'status' ];
		}

		if ( ! empty( $args['meta_fields'] ) ) {
			self::update_meta_fields( $contact_id, $args );
		}

		$args['updated_at'] = current_time( 'mysql' );
		unset(
			$args[ 'meta_fields' ],
			$args[ 'contact_id' ],
			$args[ 'total_spent' ],
			$args[ 'total_orders' ],
			$args[ 'click_rate' ],
			$args[ 'open_rate' ],
			$args[ 'activities' ],
			$args[ 'messages' ],
			$args[ 'notes' ],
			$args[ 'avatar_url' ],
			$args[ 'added_by_login' ],
			$args[ 'created_at' ],
			$args[ 'tags' ],
			$args[ 'lists' ],
			$args[ 'total_clicked' ],
			$args[ 'last_opened' ],
			$args[ 'total_sent' ],
			$args[ 'total_opened' ],
			$args[ '_locale' ],
			$args[ 'general_fields' ],
			$args[ 'customer_summery' ],
			$args[ 'groups' ]
		);

		try {
			$wpdb->update(
				$contacts_table,
				$args,
				array( 'ID' => $contact_id )
			); // db call ok. ; no-cache ok.
		} catch ( \Exception $e ) {
			return false;
		}
		return true;
	}

	/**
	 * Update a contact information
	 *
	 * @param mixed $contact_id     Contact ID.
	 * @param mixed $args           Entity and value to update.
	 *
	 * @return bool|void
	 * @since 1.0.0
	 */
	public static function update_meta_fields( $contact_id, $args ) {
		if ( !$contact_id || empty( $args ) || empty( $args['meta_fields'] ) ) {
			return false;
		}
		global $wpdb;
		$contacts_meta_table = $wpdb->prefix . ContactMetaSchema::$table_name;
		$now                 = current_time( 'mysql' );

		// Prepare values (may serialize checkbox fields) — still O(F) but no DB writes yet.
		$prepared = array();
		foreach ( $args['meta_fields'] as $key => $value ) {
			$prepared[ $key ] = CustomFieldModel::prepare_custom_select_field_data( $key, $value );
		}

		$keys        = array_keys( $prepared );
		$key_count   = count( $keys );
		$placeholders = implode( ', ', array_fill( 0, $key_count, '%s' ) );

		// One SELECT to find which keys already exist for this contact.
		// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.SlowDBQuery.slow_db_query_meta_key
		$existing_keys = $wpdb->get_col(
			$wpdb->prepare(
				"SELECT meta_key FROM {$contacts_meta_table} WHERE contact_id = %d AND meta_key IN ({$placeholders})",
				array_merge( array( $contact_id ), $keys )
			)
		); // db call ok. ; no-cache ok.
		// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.SlowDBQuery.slow_db_query_meta_key

		$existing_keys = array_flip( $existing_keys );
		$insert_rows   = array();

		foreach ( $prepared as $key => $value ) {
			if ( isset( $existing_keys[ $key ] ) ) {
				// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_value, WordPress.DB.SlowDBQuery.slow_db_query_meta_key
				$wpdb->update(
					$contacts_meta_table,
					array(
						'meta_value' => $value,
						'updated_at' => $now,
					),
					array(
						'contact_id' => $contact_id,
						'meta_key'   => $key,
					)
				); // db call ok. ; no-cache ok.
				// phpcs:enable WordPress.DB.SlowDBQuery.slow_db_query_meta_value, WordPress.DB.SlowDBQuery.slow_db_query_meta_key
			} else {
				$insert_rows[] = $wpdb->prepare( '(%d, %s, %s, %s, %s)', $contact_id, $key, $value, $now, $now );
			}
		}

		// Bulk-insert all new keys in one query.
		if ( ! empty( $insert_rows ) ) {
			$values_sql = implode( ', ', $insert_rows );
			// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value
			$wpdb->query(
				"INSERT INTO {$contacts_meta_table} (contact_id, meta_key, meta_value, created_at, updated_at) VALUES {$values_sql}"
			); // db call ok. ; no-cache ok.
			// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value
		}
	}


	/**
	 * Check existing contact through an email address
	 *
	 * @param string $email email.
	 *
	 * @return string|null
	 * @since 1.0.0
	 */
	public static function is_contact_exist( $email ) {
		global $wpdb;
		$contacts_table = $wpdb->prefix . ContactSchema::$table_name;
		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$select_query = $wpdb->prepare( "SELECT `id` FROM $contacts_table WHERE email = %s", array( $email ) );

		// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
		return $wpdb->get_var( $select_query ); // db call ok. ; no-cache ok.
	}

	/**
	 * Check existing contact through an email address
	 *
	 * @param array $ids contact id array.
	 *
	 * @return bool
	 * @since 1.0.4
	 */
	public static function is_contact_ids_exists( $ids ) {
		if ( empty( $ids ) ) {
			return false;
		}
		global $wpdb;
		$contacts_table = $wpdb->prefix . ContactSchema::$table_name;

		$placeholders = array_fill( 0, count( $ids ), '%d' ); // Create placeholders for the prepare statement.

		// Generate the query string.
		$query = $wpdb->prepare( "SELECT COUNT(`id`) FROM {$contacts_table} WHERE `id` IN (" . implode( ', ', $placeholders ) . ")", $ids ); //phpcs:ignore

		$total = $wpdb->get_var( $query ); //phpcs:ignore

		if ( count( $ids ) === intval( $total ) ) {
			// All IDs exist in the database.
			return true;
		}
		return false;
	}

	/**
	 * Check existing contact through an email address and contact ID
	 *
	 * @param string $email email.
	 * @param int    $contact_id contact id.
	 *
	 * @return bool
	 * @since 1.0.0
	 */
	public static function is_contact_exist_by_id( $email, $contact_id ) {
		global $wpdb;
		$contacts_table = $wpdb->prefix . ContactSchema::$table_name;

		$select_query = $wpdb->prepare( "SELECT id FROM $contacts_table WHERE email = %s AND id = %d", array( $email, $contact_id ) );
		$results      = $wpdb->get_results( $select_query ); // db call ok. ; no-cache ok.
		if ( $results ) {
			return true;
		}
		return false;
	}


	/**
	 * Delete a contact
	 *
	 * @param mixed $id contact id.
	 *
	 * @return bool
	 * @since 1.0.0
	 */
	public static function destroy( $id ) {
		global $wpdb;
		$contacts_table            = $wpdb->prefix . ContactSchema::$table_name;
		$contact_meta_table        = $wpdb->prefix . ContactMetaSchema::$table_name;
		$contact_note_table        = $wpdb->prefix . ContactNoteSchema::$table_name;
		$contact_group_pivot_table = $wpdb->prefix . ContactGroupPivotSchema::$table_name;

		try {
			do_action('mailmint_after_delete_contact', $id);
			self::remove_form_entries_for_deleted_contact( $id );
			$wpdb->delete( $contact_meta_table, array( 'contact_id' => $id ) ); // db call ok. ; no-cache ok.
			$wpdb->delete( $contact_note_table, array( 'contact_id' => $id ) ); // db call ok. ; no-cache ok.
			$wpdb->delete( $contact_group_pivot_table, array( 'contact_id' => $id ) ); // db call ok. ; no-cache ok.
			$wpdb->delete( $contacts_table, array( 'id' => $id ) ); // db call ok. ; no-cache ok.
			return true;
		} catch ( \Exception $e ) {
			return false;
		}
	}


	/**
	 * Delete multiple contacts
	 *
	 * @param array $contact_ids contact id.
	 *
	 * @return bool
	 * @since 1.0.0
	 */
	public static function destroy_all( $contact_ids ) {
		global $wpdb;
		$contacts_table            = $wpdb->prefix . ContactSchema::$table_name;
		$contact_meta_table        = $wpdb->prefix . ContactMetaSchema::$table_name;
		$contact_note_table        = $wpdb->prefix . ContactNoteSchema::$table_name;
		$contact_group_pivot_table = $wpdb->prefix . ContactGroupPivotSchema::$table_name;
		$broadcast_emails_table    = $wpdb->prefix . EmailSchema::$table_name;

		try {
			if ( !empty( $contact_ids ) ) {
				self::batch_decrement_form_entries( $contact_ids );
				foreach ( $contact_ids as $id ) {
					do_action('mailmint_after_delete_contact', $id);
				}
			}
			$contact_ids      = implode( ',', array_map( 'intval', $contact_ids ) );
			$ids_placeholder  = implode( ',', array_fill( 0, substr_count( $contact_ids, ',' ) + 1, '%d' ) );
			$ids_array        = array_map( 'intval', explode( ',', $contact_ids ) );

			$wpdb->query( "DELETE FROM $contacts_table WHERE id IN($contact_ids)" ); // db call ok. ; no-cache ok.
			$wpdb->query( "DELETE FROM $contact_meta_table WHERE contact_id IN($contact_ids)" ); // db call ok. ; no-cache ok.
			$wpdb->query( "DELETE FROM $contact_note_table WHERE contact_id IN($contact_ids)" ); // db call ok. ; no-cache ok.
			$wpdb->query( "DELETE FROM $contact_group_pivot_table WHERE contact_id IN($contact_ids)" ); // db call ok. ; no-cache ok.
			$wpdb->query( $wpdb->prepare( "UPDATE $broadcast_emails_table SET contact_id = NULL WHERE contact_id IN($ids_placeholder)", ...$ids_array ) ); // db call ok. ; no-cache ok.
			return true;
		} catch ( \Exception $e ) {
			return false;
		}
	}


	/**
	 * Run SQL query to get or search contacts from database
	 *
	 * @param int    $offset offset.
	 * @param int    $limit limit.
	 * @param string $search search.
	 * @param array  $selected_contacts selected contact ids.
	 * @return array
	 * @since 1.0.0
	 */
	public static function get_all( $offset = 0, $limit = 10, $search = '', $selected_contacts = array() ) {
		global $wpdb;
		$contact_table = $wpdb->prefix . ContactSchema::$table_name;
		$search_terms  = null;
		$export_terms  = null;
		// Search contacts by email, first name or last name.
		if ( ! empty( $search ) ) {
			$search       = $wpdb->esc_like( $search );
			$search_terms = "WHERE (`hash` LIKE '%%$search%%' 
             OR `email` LIKE '%%$search%%' OR `first_name` LIKE '%%$search%%' OR `last_name` LIKE '%%$search%%'
             OR concat(`first_name`, ' ', `last_name`) LIKE '%%$search%%'
             OR `source` LIKE '%%$search%%' 
             OR `status` LIKE '%%$search%%' 
             OR `stage` LIKE '%%$search%%')";
		}

		if ( !empty( $selected_contacts ) ) {
			$contacts     = implode( ',', $selected_contacts );
			$export_terms = 'WHERE id IN (' . $contacts . ')';
		}

		// Prepare sql results for list view.
		$query_results = $wpdb->get_results( $wpdb->prepare( "SELECT id, email, first_name, last_name, status, stage, source, scores, created_at, updated_at FROM $contact_table $search_terms $export_terms ORDER BY id DESC  LIMIT %d, %d", array( $offset, $limit ) ), ARRAY_A ); // db call ok. ; no-cache ok.
		$results       = array();

		if ( ! empty( $query_results ) ) {
			$ids      = wp_list_pluck( $query_results, 'id' );
			$meta_map = self::get_meta_for_contacts( $ids );

			$form_ids = array();
			foreach ( $query_results as $row ) {
				if ( isset( $row['source'] ) && preg_match( '/Form-(\d+)/', $row['source'], $m ) ) {
					$form_ids[] = (int) $m[1];
				}
			}
			$form_titles = ! empty( $form_ids ) ? FormModel::get_form_titles_by_ids( array_unique( $form_ids ) ) : array();

			foreach ( $query_results as $query_result ) {
				$q_id     = isset( $query_result['id'] ) ? (int) $query_result['id'] : 0;
				$new_meta = isset( $meta_map[ $q_id ] ) ? $meta_map[ $q_id ] : array( 'meta_fields' => array() );

				if ( isset( $query_result['source'] ) && preg_match( '/Form-(\d+)/', $query_result['source'], $matches ) ) {
					$form_id                = (int) $matches[1];
					$query_result['source'] = 'Mail Mint Form - ' . ( isset( $form_titles[ $form_id ] ) ? $form_titles[ $form_id ] : '' );
				}

				$results[] = array_merge( $query_result, $new_meta );
			}
		}

		$count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(id) as total FROM $contact_table $search_terms" ) ); // db call ok. ; no-cache ok.

		return array(
			'data'        => $results,
			'total_pages' => ceil( $count / $limit ),
			'total_count' => $count,
		);
	}


	/**
	 * Run SQL Query to get a single contact information
	 *
	 * @param mixed $id Contact ID.
	 *
	 * @return array
	 * @since 1.0.0
	 */
	public static function get( $id ) {
		global $wpdb;
		$contacts_table = $wpdb->prefix . ContactSchema::$table_name;

		try {
			$contacts_result = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $contacts_table WHERE id = %d", array( $id ) ), ARRAY_A ); // db call ok. ; no-cache ok.
			$new_meta        = self::get_meta( $id );

			if ( is_array( $contacts_result ) && is_array( $new_meta ) ) {
				$contacts_result = array_merge( $contacts_result, $new_meta );
			}

			return is_array( $contacts_result ) ? $contacts_result : array();
		} catch ( \Exception $e ) {
			return false;
		}
	}


	/**
	 * Batch-load contacts and their meta in exactly 2 queries.
	 *
	 * Returns an associative array keyed by contact ID, where each value
	 * matches the data shape returned by get($id) — contact fields merged
	 * with a 'meta_fields' key. Missing IDs are silently omitted.
	 *
	 * @param int[] $contact_ids Array of contact IDs to load.
	 *
	 * @return array<int, array> Keyed by contact ID.
	 * @since 1.20.0
	 */
	public static function getMany( array $contact_ids ): array {
		if ( empty( $contact_ids ) ) {
			return array();
		}

		global $wpdb;
		$contacts_table      = $wpdb->prefix . ContactSchema::$table_name;
		$contacts_meta_table = $wpdb->prefix . ContactMetaSchema::$table_name;

		// Sanitize IDs to integers.
		$contact_ids  = array_map( 'absint', $contact_ids );
		$contact_ids  = array_unique( array_filter( $contact_ids ) );

		if ( empty( $contact_ids ) ) {
			return array();
		}

		// Query 1: Fetch all contact rows.
		$placeholders = implode( ',', array_fill( 0, count( $contact_ids ), '%d' ) );
		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$contacts_sql = $wpdb->prepare( "SELECT * FROM $contacts_table WHERE id IN ($placeholders)", $contact_ids ); // db call ok. ; no-cache ok.
		$contact_rows = $wpdb->get_results( $contacts_sql, ARRAY_A ); // db call ok. ; no-cache ok.

		if ( empty( $contact_rows ) ) {
			return array();
		}

		// Index contacts by ID.
		$results = array();
		foreach ( $contact_rows as $row ) {
			$results[ (int) $row['id'] ] = $row;
			$results[ (int) $row['id'] ]['meta_fields'] = array();
		}

		// Query 2: Fetch all meta for these contacts.
		$found_ids         = array_keys( $results );
		$meta_placeholders = implode( ',', array_fill( 0, count( $found_ids ), '%d' ) );
		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$meta_sql     = $wpdb->prepare( "SELECT contact_id, meta_key, meta_value FROM $contacts_meta_table WHERE contact_id IN ($meta_placeholders)", $found_ids ); // db call ok. ; no-cache ok.
		$meta_results = $wpdb->get_results( $meta_sql, ARRAY_A ); // db call ok. ; no-cache ok.

		// Group meta by contact_id, matching get_meta() shape.
		if ( ! empty( $meta_results ) ) {
			foreach ( $meta_results as $meta_row ) {
				$cid = (int) $meta_row['contact_id'];
				if ( isset( $results[ $cid ] ) ) {
					// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
					$results[ $cid ]['meta_fields'][ $meta_row['meta_key'] ] = self::safe_unserialize_meta( $meta_row['meta_value'] );
				}
			}
		}

		return $results;
	}


	/**
	 * Run SQL Query to get a single contact email only
	 *
	 * @param mixed $ids Contact IDs.
	 *
	 * @return array
	 * @since 1.0.0
	 */
	public static function get_single_email( $ids ) {
		global $wpdb;
		$contacts_table = $wpdb->prefix . ContactSchema::$table_name;

		if ( is_array( $ids ) ) {
			$ids = ! empty( $ids ) ? implode( ', ', $ids ) : 0;
		}

		$sql = $wpdb->prepare( "SELECT `id`, `email` FROM %1s WHERE `id` IN( %1s ) AND `status` = %s", $contacts_table, $ids, 'subscribed' ); //phpcs:ignore
		return $wpdb->get_results( $sql, ARRAY_A ); // db call ok. ; no-cache ok.
	}


	/**
	 * Returns contact meta data
	 *
	 * @param int $id   Contact ID.
	 * @return array
	 * @since 1.0.0
	 */
	public static function get_meta( $id ) {
		global $wpdb;
		$contacts_meta_table = $wpdb->prefix . ContactMetaSchema::$table_name;

		$meta_results = $wpdb->get_results( $wpdb->prepare( "SELECT meta_key, meta_value FROM $contacts_meta_table  WHERE contact_id = %d", array( $id ) ), ARRAY_A ); // db call ok. ; no-cache ok.

		$new_meta['meta_fields'] = array();
		foreach ( $meta_results as $result ) {
			// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key
			$new_meta['meta_fields'][ $result['meta_key'] ] = self::safe_unserialize_meta( $result['meta_value'] );
		}

		return $new_meta;
	}


	/**
	 * Batch-fetch meta for multiple contacts in a single query.
	 *
	 * @param array $contact_ids Array of contact IDs.
	 * @return array Keyed by contact_id; each value matches the shape returned by get_meta().
	 * @since 1.0.0
	 */
	public static function get_meta_for_contacts( array $contact_ids ) {
		if ( empty( $contact_ids ) ) {
			return array();
		}

		global $wpdb;
		$contacts_meta_table = $wpdb->prefix . ContactMetaSchema::$table_name;

		$contact_ids  = array_unique( array_filter( array_map( 'absint', $contact_ids ) ) );
		$placeholders = implode( ',', array_fill( 0, count( $contact_ids ), '%d' ) );

		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$meta_results = $wpdb->get_results(
			$wpdb->prepare( "SELECT contact_id, meta_key, meta_value FROM $contacts_meta_table WHERE contact_id IN ($placeholders)", $contact_ids ), // db call ok. ; no-cache ok.
			ARRAY_A
		);

		$meta_map = array();
		foreach ( $meta_results as $row ) {
			$cid = (int) $row['contact_id'];
			if ( ! isset( $meta_map[ $cid ] ) ) {
				$meta_map[ $cid ] = array( 'meta_fields' => array() );
			}
			// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key
			$meta_map[ $cid ]['meta_fields'][ $row['meta_key'] ] = self::safe_unserialize_meta( $row['meta_value'] );
			// phpcs:enable WordPress.DB.SlowDBQuery.slow_db_query_meta_key
		}

		return $meta_map;
	}


	/**
	 * Check existing contact through an email address
	 *
	 * @param string $contact_id contact id.
	 * @param string $key meta key.
	 *
	 * @return bool
	 * @since 1.0.0
	 */
	public static function is_contact_meta_exist( $contact_id, $key ) {
		global $wpdb;
		$table_name = $wpdb->prefix . ContactMetaSchema::$table_name;

		try {
			$select_query = $wpdb->prepare( "SELECT id FROM $table_name WHERE contact_id = %d AND meta_key=%s", array( $contact_id, $key ) );
			$results      = $wpdb->get_results( $select_query ); // db call ok. ; no-cache ok.
			if ( ! empty( $results ) ) {
				return true;
			}
		} catch ( \Throwable $th ) {
			return false;
		}
	}


	/**
	 * Check existing contact through an email address
	 *
	 * @param string $contact_id contact id.
	 * @param string $meta_key meta key.
	 *
	 * @return bool
	 * @since 1.0.0
	 */
	public static function is_contact_meta_key_exist( $contact_id, $meta_key ) {
		global $wpdb;
		$table_name = $wpdb->prefix . ContactMetaSchema::$table_name;

		try {
			$select_query = $wpdb->prepare( "SELECT id FROM $table_name WHERE contact_id = %d AND meta_key=%s", array( $contact_id, $meta_key ) );
			$results      = $wpdb->get_results( $select_query ); // db call ok. ; no-cache ok.
			if ( ! empty( $results ) ) {
				return true;
			}
		} catch ( \Throwable $th ) {
			return false;
		}
	}


	/**
	 * Run SQL Query to get filtered Contacts
	 *
	 * @param mixed  $status status.
	 * @param mixed  $tags_ids tags ids.
	 * @param mixed  $lists_ids lists ids.
	 * @param int    $limit limit.
	 * @param int    $offset offset.
	 * @param string $search search.
	 *
	 * @return array|bool
	 * @since 1.0.0
	 */
	public static function get_filtered_contacts( $status, $tags_ids, $lists_ids, $limit = 10, $offset = 0, $search = '' ) {
		global $wpdb;
		$contact_table = $wpdb->prefix . ContactSchema::$table_name;
		$pivot_table   = $wpdb->prefix . ContactGroupPivotSchema::$table_name;

		// Prepare sql results for list view.
		try {
			$tags     = implode( ',', array_map( 'intval', $tags_ids ) );
			$lists    = implode( ',', array_map( 'intval', $lists_ids ) );
			$statuses = implode( '","', $status );

			$status_arr = 'status IN ("' . $statuses . '")';

			$and = 'AND';

			$contact_filter_query = "( $pivot_table.group_id IN ($tags) AND  tt1.group_id IN ($lists)
            AND $status_arr )";

			if ( count( $tags_ids ) === 0 && count( $lists_ids ) === 0 && count( $status ) === 0 ) {
				$and                  = '';
				$contact_filter_query = '';
			} elseif ( count( $tags_ids ) === 0 && count( $lists_ids ) === 0 && count( $status ) !== 0 ) {
				$contact_filter_query = "( $status_arr )";
			} elseif ( count( $tags_ids ) === 0 && count( $lists_ids ) !== 0 && count( $status ) === 0 ) {
				$contact_filter_query = " (tt1.group_id IN ($lists))";
			} elseif ( count( $tags_ids ) === 0 && count( $lists_ids ) !== 0 && count( $status ) !== 0 ) {
				$contact_filter_query = " (tt1.group_id IN ($lists) AND $status_arr)";
			} elseif ( count( $tags_ids ) !== 0 && count( $lists_ids ) === 0 && count( $status ) === 0 ) {
				$contact_filter_query = " ($pivot_table.group_id IN ($tags))";
			} elseif ( count( $tags_ids ) !== 0 && count( $lists_ids ) === 0 && count( $status ) !== 0 ) {
				$contact_filter_query = "( $pivot_table.group_id IN ($tags) AND $status_arr )";
			} elseif ( count( $tags_ids ) !== 0 && count( $lists_ids ) !== 0 && count( $status ) === 0 ) {
				$contact_filter_query = "( $pivot_table.group_id IN ($tags) AND  tt1.group_id IN ($lists))";
			}

			$search = $wpdb->esc_like( $search );
			// phpcs:disable WordPress.DB.PreparedSQLPlaceholders.LikeWildcardsInQuery
			$select_query = $wpdb->prepare(
				"SELECT $contact_table.id, $contact_table.email, $contact_table.first_name, $contact_table.last_name, $contact_table.status, $contact_table.stage, $contact_table.source, $contact_table.scores, $contact_table.created_at, $contact_table.updated_at FROM $contact_table
            LEFT JOIN $pivot_table ON ($contact_table.id = $pivot_table.contact_id)
            LEFT JOIN $pivot_table AS tt1 ON ($contact_table.id = tt1.contact_id)
            WHERE (`hash` LIKE '%%$search%%' OR `email` LIKE '%%$search%%' OR
                 `first_name` LIKE '%%$search%%' OR `last_name` LIKE '%%$search%%' OR concat(`first_name`, ' ', `last_name`) LIKE '%%$search%%'
                 OR `source` LIKE '%%$search%%' OR `status` LIKE '%%$search%%' OR 
                 `stage` LIKE '%%$search%%') $and $contact_filter_query
                 GROUP BY $contact_table.id
                LIMIT $offset, $limit"
			);

			$query_results = $wpdb->get_results( $select_query, ARRAY_A ); // db call ok. ; no-cache ok.
			$results       = array();

			if ( ! empty( $query_results ) ) {
				$ids      = wp_list_pluck( $query_results, 'id' );
				$meta_map = self::get_meta_for_contacts( $ids );

				$form_ids = array();
				foreach ( $query_results as $row ) {
					if ( isset( $row['source'] ) && preg_match( '/Form-(\d+)/', $row['source'], $m ) ) {
						$form_ids[] = (int) $m[1];
					}
				}
				$form_titles = ! empty( $form_ids ) ? FormModel::get_form_titles_by_ids( array_unique( $form_ids ) ) : array();

				foreach ( $query_results as $query_result ) {
					$q_id     = isset( $query_result['id'] ) ? (int) $query_result['id'] : 0;
					$new_meta = isset( $meta_map[ $q_id ] ) ? $meta_map[ $q_id ] : array( 'meta_fields' => array() );

					if ( isset( $query_result['source'] ) && preg_match( '/Form-(\d+)/', $query_result['source'], $matches ) ) {
						$form_id                = (int) $matches[1];
						$query_result['source'] = 'Mail Mint Form - ' . ( isset( $form_titles[ $form_id ] ) ? $form_titles[ $form_id ] : '' );
					}

					$results[] = array_merge( $query_result, $new_meta );
				}
			}

			$count_query = $wpdb->prepare(
				"SELECT COUNT(*) AS total FROM $contact_table
            LEFT JOIN $pivot_table ON ($contact_table.id = $pivot_table.contact_id)  
            LEFT JOIN $pivot_table AS tt1 ON ($contact_table.id = tt1.contact_id)
            WHERE 
            (`hash` LIKE '%%$search%%' OR `email` LIKE '%%$search%%' OR
                 `first_name` LIKE '%%$search%%' OR `last_name` LIKE '%%$search%%' 
                 OR `source` LIKE '%%$search%%' OR `status` LIKE '%%$search%%' OR 
                 `stage` LIKE '%%$search%%') $and $contact_filter_query
                GROUP BY $contact_table.id
            "
			);
			// phpcs:enable WordPress.DB.PreparedSQLPlaceholders.LikeWildcardsInQuery

			$count_result = $wpdb->get_results( $count_query ); // db call ok. ; no-cache ok.

			$count = (int) count( $count_result );

			$total_pages = ceil( $count / $limit );

			$data = array(
				'data'        => json_decode( wp_json_encode( $results ), true ),
				'total_pages' => $total_pages,
				'count'       => $count,
			);

			return $data;
		} catch ( \Exception $e ) {
			return null;
		}
	}



	/**
	 * Return total number of contacts
	 *
	 * @return int
	 * @since 1.0.0
	 */
	public static function get_contacts_count() {
		global $wpdb;
		$table_name = $wpdb->prefix . ContactSchema::$table_name;
		return absint( $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(`id`) FROM %1s', $table_name ) ) ); //phpcs:ignore
	}


	/**
	 * Return total number of contacts based on status
	 *
	 * @param string $status status.
	 * @return int
	 * @since 1.0.0
	 */
	public static function get_contacts_status_count( $status ) {
		global $wpdb;
		$table_name = $wpdb->prefix . ContactSchema::$table_name;
		return absint( $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(id) FROM $table_name WHERE status= %s", array( $status ) ) ) ); // db call ok. ; no-cache ok.
	}

	/**
	 * Return total number of contacts based on status
	 *
	 * @return array
	 * @since 1.4.10
	 */
	public static function get_contact_total() {
		global $wpdb;
		$table_name = $wpdb->prefix . ContactSchema::$table_name;
        $get_total = $wpdb->get_row(  //phpcs:ignore
			$wpdb->prepare(
				'SELECT
            COUNT(CASE WHEN status = %s THEN 1 END) AS subscribed,
            COUNT(CASE WHEN status = %s THEN 1 END) AS pending,
            COUNT(CASE WHEN status = %s THEN 1 END) AS unsubscribed,
            COUNT(CASE WHEN status = %s THEN 1 END) AS bounced,
            COUNT(CASE WHEN status = %s THEN 1 END) AS complained,
            COUNT(CASE WHEN status = %s THEN 1 END) AS inactive
        FROM %1s', array( 'subscribed', 'pending', 'unsubscribed', 'bounced', 'complained', 'inactive', $table_name )), ARRAY_A);  //phpcs:ignore
		if ( !empty( $get_total ) ) {
			return $get_total;
		}
		return array();
	}


	/**
	 * Run SQL Query to get a single contact information by hash
	 *
	 * @param mixed $hash Contact email address hash.
	 *
	 * @return object
	 * @since 1.0.0
	 */
	public static function get_by_hash( $hash ) {
		global $wpdb;
		$contacts_table = $wpdb->prefix . ContactSchema::$table_name;

		return $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $contacts_table WHERE hash = %s", array( $hash ) ), ARRAY_A ); // db call ok. ; no-cache ok.
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
	}


	/**
	 * Run SQL Query to get a single contact email address
	 *
	 * @param mixed $contact_id Contact ID.
	 *
	 * @return string
	 * @since 1.0.0
	 */
	public static function get_email_by_id( $contact_id ) {
		global $wpdb;
		$contacts_table = $wpdb->prefix . ContactSchema::$table_name;

		return $wpdb->get_row( $wpdb->prepare( "SELECT email FROM $contacts_table WHERE id = %d", array( $contact_id ) ), ARRAY_A ); //phpcs:ignore
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
	}


	/**
	 * Update contact subscription status
	 *
	 * @param int    $contact_id Contact ID.
	 * @param string $status Updated contact status.
	 *
	 * @return void
	 */
	public static function update_subscription_status( $contact_id, $status ) {
		global $wpdb;
		$table_name = $wpdb->prefix . ContactSchema::$table_name;

		$wpdb->update( //phpcs:ignore
			$table_name,
			array( 'status' => $status ),
			array( 'id' => $contact_id )
		);
	}


	/**
	 * Run SQL Query to get a single contact email address
	 *
	 * @param string $email Contact ID.
	 *
	 * @return array
	 * @since 1.0.0
	 */
	public static function get_id_by_email( $email ) {
		global $wpdb;
		$contacts_table = $wpdb->prefix . ContactSchema::$table_name;
		$contact_id =  $wpdb->get_var( $wpdb->prepare( "SELECT id FROM $contacts_table WHERE email = %s", array( $email ) ) ); //phpcs:ignore
		return $contact_id;
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
	}

	/**
	 * Get user by wp user id
	 *
	 * @param string|int $user_id WP User ID.
	 *
	 * @return string|null
	 */
	public static function get_user_id_by_wp_user_id( $user_id ) {
		global $wpdb;
		$contacts_table = $wpdb->prefix . ContactSchema::$table_name;

		$query  = 'SELECT `id` FROM %1s ';
		$query .= 'WHERE wp_user_id=%d';

		$mailmint_user_id = $wpdb->get_var( $wpdb->prepare( $query, $contacts_table, (int)$user_id ) ); //phpcs:ignore

		if ( $mailmint_user_id ) {
			return $mailmint_user_id;
		}

		$contacts_meta_table = $wpdb->prefix . ContactMetaSchema::$table_name;

		$query  = 'SELECT `contact_id` FROM %1s ';
		$query .= 'WHERE `meta_key`=%s ';
		$query .= 'AND `meta_value`=%d';

		return $wpdb->get_var( $wpdb->prepare( $query, $contacts_meta_table, '_wc_customer_id', (int)$user_id ) ); //phpcs:ignore
	}


	/**
	 * Return contact information via email address
	 *
	 * @param mixed $email contact email address.
	 *
	 * @return array
	 * @since 1.0.0
	 */
	public static function get_contact_by_email( $email ) {
		global $wpdb;
		$contacts_table = $wpdb->prefix . ContactSchema::$table_name;
		return $wpdb->get_row( $wpdb->prepare( "SELECT id, first_name, last_name, status, created_at, email ,hash FROM $contacts_table WHERE email = %s", array( $email ) ), ARRAY_A ); //phpcs:ignore
	}

	/**
	 * Prepare contact information for WooCommerce order edit page
	 *
	 * @param string $email contact email address.
	 * @param string $shop_name Shop name.
	 *
	 * @return array
	 * @since 1.0.0
	 * @since 1.9.1 Retrieve contact meta fields through contact_id.
	 */
	public function contact_information_to_shop_order( $email, $shop_name ) {
		$contact    = self::get_contact_by_email( $email );
		$contact_id = isset( $contact['id'] ) ? $contact['id'] : '';
		$contact    = self::get( $contact_id );

		// Get and merge tags and lists.
		if ( $contact ) {
			$contact = ContactGroupModel::get_tags_to_contact( $contact );
			$contact = ContactGroupModel::get_lists_to_contact( $contact );

			if ( isset( $contact[ 'created_at' ] ) ) {
				$contact[ 'created_at' ] = MrmCommon::date_time_format_with_core( $contact[ 'created_at' ] );
			}

			$contact['avatar_url'] = Contact::get_avatar_url( $contact );

			if ( 'wc' === $shop_name ) {
				$contact = MrmCommon::get_wc_customer_revenue_history( $email, $contact );
			} elseif ( 'edd' === $shop_name ) {
				$contact = MrmCommon::get_edd_customer_revenue_history( $email, $contact );
			}
		}

		return $contact;
	}

	/**
	 * Return contact source via id
	 *
	 * @param mixed $id contact id.
	 *
	 * @return array
	 * @since 1.0.0
	 */
	public static function get_contact_source_by_id( $id ) {
		global $wpdb;
		$contacts_table = $wpdb->prefix . ContactSchema::$table_name;
        return $wpdb->get_row( $wpdb->prepare( "SELECT source FROM $contacts_table WHERE id = %d", array( $id ) ), ARRAY_A ); //phpcs:ignore
	}

	/**
	 *  /**
	 * Get source form id which is added by form.
	 *
	 * @param string $source Get Contact source.
	 * @return string
	 */
	public static function get_form_id_by_source( $source ) {
		if ( !$source ) {
			return null;
		}
		$source_form = explode( '-', $source );
		$form_id     = isset( $source_form['1'] ) ? $source_form['1'] : null;
		return $form_id;
	}

	/**
	 * Check contact source is form or not.
	 *
	 * @param string $source Get contact source.
	 * @return bool|null
	 */
	public static function is_contact_source_by_form( $source ) {
		if ( !$source ) {
			return null;
		}
		if ( preg_match( '/form/i', $source ) ) {
			return true;
		}
		return false;
	}

	/**
	 * Remove form entries when contact is deleted
	 *
	 * @param int $id Contact id.
	 * @return bool
	 */
	public static function remove_form_entries_for_deleted_contact( $id ) {
		if ( !$id && !is_numeric( $id ) ) {
			return false;
		}

		$source = self::get_contact_source_by_id( $id );
		if ( !isset( $source['source'] ) ) {
			return false;
		}

		$is_source_form = self::is_contact_source_by_form( $source['source'] );
		if ( !$is_source_form ) {
			return false;
		}

		$form_id = self::get_form_id_by_source( $source['source'] );
		if ( !$form_id ) {
			return false;
		}

		$entries = FormModel::get_form_meta_value_with_key( $form_id, 'entries' );
		if ( isset( $entries['meta_fields']['entries'] ) ) {
			$entries_count       = isset( $entries['meta_fields']['entries'] ) ? $entries['meta_fields']['entries'] : 0;
			$args['meta_fields'] = array(
				'entries' => $entries_count - 1,
			);
			return FormModel::update_meta_fields( $form_id, $args );
		}
		return false;
	}

	/**
	 * Batch-decrement form entry counts for a set of contacts being deleted.
	 * Replaces per-contact remove_form_entries_for_deleted_contact() calls in destroy_all()
	 * to reduce O(N·4) queries down to O(F·2) where F = number of distinct form sources.
	 *
	 * @param array $contact_ids Contact IDs being deleted.
	 * @return void
	 */
	private static function batch_decrement_form_entries( array $contact_ids ) {
		global $wpdb;

		$contacts_table   = $wpdb->prefix . ContactSchema::$table_name;
		$form_meta_table  = $wpdb->prefix . FormMetaSchema::$table_name;

		$ids_placeholder = implode( ',', array_fill( 0, count( $contact_ids ), '%d' ) );
		$rows = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT source FROM $contacts_table WHERE id IN ($ids_placeholder) AND source IS NOT NULL AND source != ''", // db call ok. ; no-cache ok.
				...$contact_ids
			),
			ARRAY_A
		);

		if ( empty( $rows ) ) {
			return;
		}

		// Count deletions per form_id parsed from source strings like "form-123".
		$decrements = array();
		foreach ( $rows as $row ) {
			if ( ! preg_match( '/form/i', $row['source'] ) ) {
				continue;
			}
			$parts   = explode( '-', $row['source'] );
			$form_id = isset( $parts[1] ) ? (int) $parts[1] : 0;
			if ( $form_id > 0 ) {
				$decrements[ $form_id ] = ( $decrements[ $form_id ] ?? 0 ) + 1;
			}
		}

		foreach ( $decrements as $form_id => $count ) {
			$current = $wpdb->get_var(
				$wpdb->prepare(
					"SELECT meta_value FROM $form_meta_table WHERE form_id = %d AND meta_key = 'entries'", // db call ok. ; no-cache ok.
					$form_id
				)
			);
			if ( null === $current ) {
				continue;
			}
			$new_value = max( 0, (int) $current - $count );
			$wpdb->update(
				$form_meta_table,
				array( 'meta_value' => $new_value ),
				array( 'form_id' => $form_id, 'meta_key' => 'entries' )
			); // db call ok. ; no-cache ok.
		}
	}

	/**
	 * Updates the status of multiple contacts.
	 *
	 * This function updates the status of the specified contacts with the provided status.
	 *
	 * @access public
	 *
	 * @param array  $contact_ids An array containing the IDs of the contacts to update.
	 * @param string $status      The status to set for the contacts.
	 *
	 * @return int|false The number of rows affected by the update operation or false on failure.
	 * @since 1.5.1
	 */
	public static function update_contact_status( $contact_ids, $status ) {
		global $wpdb;
		$contacts_table = $wpdb->prefix . ContactSchema::$table_name;

		// Convert the IDs array into a comma-separated string.
		$ids_str      = implode( ',', $contact_ids );
		$update_query = $wpdb->prepare( "UPDATE {$contacts_table} SET status = %s WHERE id IN ($ids_str)", $status ); //phpcs:ignore
		return $wpdb->query( $update_query ); //phpcs:ignore
	}

	/**
	 * Return contact email address by contact id
	 *
	 * @param mixed $contact_id contact id.
	 *
	 * @return array
	 * @since 1.7.0
	 */
	public static function get_contact_email_by_id( $contact_id ) {
		global $wpdb;
		$contacts_table = $wpdb->prefix . ContactSchema::$table_name;
		return $wpdb->get_var( $wpdb->prepare( "SELECT email FROM $contacts_table WHERE id = %d", array( $contact_id ) ) ); //phpcs:ignore
	}

	/**
	 * Summary: Insert form submission into contact meta table.
	 *
	 * Description: This static method inserts a form submission record into the contact meta table.
	 *
	 * @access public
	 *
	 * @param int    $contact_id The ID of the contact associated with the form submission.
	 * @param string $form_id    The ID or identifier of the submitted form.
	 *
	 * @return int|false Returns the number of rows affected on success; otherwise, false.
	 *
	 * @since 1.7.1
	 */
	public static function insert_form_submission( $contact_id, $form_id ) {
		global $wpdb;
		$table = $wpdb->prefix . ContactMetaSchema::$table_name;
		$args  = array(
			'contact_id' => $contact_id,
			'meta_key'   => '_form_id',
			'meta_value' => $form_id,
			'created_at' => current_time( 'mysql' ),
		);
		return $wpdb->insert( $table, $args ); //phpcs:ignore
	}

	/**
	 * Retrieves the full name of a contact.
	 *
	 * This method retrieves the contact associated with the provided email address and returns the contact's full name.
	 * If no contact is found, the method returns null.
	 *
	 * @param string $email The email address of the contact.
	 * @return string|null The full name of the contact, or null if no contact is found.
	 *
	 * @since 1.9.0
	 */
	public static function get_contact_full_name( $email ) {
		$contact = self::get_contact_by_email( $email );
		if ( !empty( $contact ) ) {
			return $contact['first_name'] . ' ' . $contact['last_name'];
		}
	}

	public static function get_meta_value_by_key( $meta_key, $contact_id ){
		global $wpdb;
		$table_name = $wpdb->prefix . ContactMetaSchema::$table_name;
    	return $wpdb->get_var( $wpdb->prepare( "SELECT meta_value FROM $table_name WHERE meta_key = %s AND contact_id = %d", $meta_key, $contact_id ) );
	}

	/**
	 * Run SQL Query to get a single contact information by email.
	 *
	 * @param mixed $email Contact email address.
	 *
	 * @return array
	 * @since 1.14.5
	 */
	public static function get_contact_data_by_email( $email ) {
		global $wpdb;
		$contacts_table = $wpdb->prefix . ContactSchema::$table_name;

		$contact    = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $contacts_table WHERE email = %s", array( $email ) ), ARRAY_A ); // db call ok. ; no-cache ok.
		$contact_id = isset( $contact['id'] ) ? $contact['id'] : '';
		$new_meta   = self::get_meta( $contact_id );

		if ( is_array( $contact ) && is_array( $new_meta ) ) {
			$contact = array_merge( $contact, $new_meta );
		}

		if (isset($contact['meta_fields']) && is_array($contact['meta_fields'])) {
			$contact = array_merge($contact, $contact['meta_fields']);
			unset($contact['meta_fields']);
		}

		return is_array( $contact ) ? $contact : array();
	}

	/**
	 * Records an unsubscribe event for a contact based on the provided data.
	 *
	 * This function updates the contact's status and records the reason for the unsubscribe event.
	 * If the contact does not exist, it creates a new contact record with the provided data.
	 *
	 * @param array $data {
	 *     The data for the unsubscribe event.
	 *
	 *     @type string $email    The email address of the contact.
	 *     @type string $status   The new status of the contact (e.g., 'bounced', 'unsubscribed'). Default 'bounced'.
	 *     @type string $reason   The reason for the status change. Default empty string.
	 *     @type string $provider The email service provider (e.g., 'Mailgun', 'SendGrid'). Default empty string.
	 * }
	 * @return bool True if the unsubscribe event was recorded successfully, false otherwise.
	 * @since 1.15.0
	 */
	public static function record_unsubscribe($data){
		if(empty($data['email']) || !is_email($data['email'])) {
			return false;
		}

		$contact  = self::get_contact_by_email($data['email']);
		$status   = isset($data['status']) ? $data['status'] : 'bounced';
		$reason   = isset($data['reason']) ? $data['reason'] : '';
		$provider = isset($data['provider']) ? $data['provider'] : '';

		$key = $status === 'unsubscribed' ? 'unsubscribe_reason' : 'reason';

		$user_data = array(
			'meta_fields' => array(
				$key => $reason,
			),
			'status' => $status,
			'source' => $provider,
		);

		if ($contact) {
			$old_status = isset($contact['status']) ? $contact['status'] : 'subscribed';
			ContactModel::update($user_data, $contact['id']);

			/*
			 * Fires when a contact's status is changed to 'bounced', 'unsubscribed', or 'pending'.
			 *
			 * @param int    $contact_id The ID of the contact.
			 * @param string $old_status The previous status of the contact.
			 * @since 1.15.0
			 */
			do_action('mint_subscriber_status_to_' . $status, $contact['id'], $old_status);
		} else {
			$contact = new ContactData($data['email'], $user_data);
			ContactModel::insert($contact);
		}

		return true;
	}

	/**
	 * Get contact meta value by key
	 *
	 * @param string $email Contact email address.
	 * @param string $meta_key Meta key.
	 *
	 * @return string
	 * @since 1.16.5
	 */
	public static function get_contact_meta_value_by_key( $email, $meta_key ) {
		global $wpdb;
		$main_table = $wpdb->prefix . ContactSchema::$table_name;
		$meta_table = $wpdb->prefix . ContactMetaSchema::$table_name;
		return $wpdb->get_var(
			$wpdb->prepare("SELECT meta.meta_value FROM {$main_table} AS t1 INNER JOIN {$meta_table} AS meta ON t1.id = meta.contact_id WHERE t1.email = %s AND meta.meta_key = %s", $email, $meta_key)
		);
	}

	/**
	 * Retrieve a contact meta value directly by contact ID and meta key.
	 *
	 * @param int    $contact_id Contact ID.
	 * @param string $meta_key   Meta key to look up.
	 * @return string|null       Meta value, or null if not found.
	 * @since 1.20.0
	 */
	public static function get_meta_value_by_contact_id( $contact_id, $meta_key ) {
		global $wpdb;
		$meta_table = $wpdb->prefix . ContactMetaSchema::$table_name;
		return $wpdb->get_var( // db call ok. ; no-cache ok.
			$wpdb->prepare( "SELECT meta_value FROM {$meta_table} WHERE contact_id = %d AND meta_key = %s LIMIT 1", $contact_id, $meta_key ) //phpcs:ignore
		);
	}

	/**
	 * Delete a contact meta row by contact ID and meta key.
	 *
	 * @param int    $contact_id Contact ID.
	 * @param string $meta_key   Meta key to remove.
	 * @return int|false Number of rows deleted, or false on error.
	 * @since 1.20.0
	 */
	public static function delete_contact_meta_by_key( $contact_id, $meta_key ) {
		global $wpdb;
		$meta_table = $wpdb->prefix . ContactMetaSchema::$table_name;
		return $wpdb->delete( // db call ok. ; no-cache ok.
			$meta_table,
			array(
				'contact_id' => (int) $contact_id,
				'meta_key'   => $meta_key,
			),
			array( '%d', '%s' )
		);
	}

	/**
	 * Get contacts by list ID with pagination
	 *
	 * @param int $item_id Group ID.
	 * @param int $offset Offset for pagination.
	 * @param int $batch_size Number of contacts to return.
	 * @return array Contacts associated with the group.
	 */
	public static function get_contacts_by_group( $item_id, $offset = 0, $batch_size = 100 ) {
		global $wpdb;
		$contact_table = $wpdb->prefix . ContactSchema::$table_name;
		$pivot_table = $wpdb->prefix . ContactGroupPivotSchema::$table_name;

		$contact_ids = $wpdb->get_col( $wpdb->prepare( "SELECT contact_id FROM $pivot_table WHERE group_id = %d LIMIT %d OFFSET %d", $item_id, $batch_size, $offset ) );

		if ( empty( $contact_ids ) ) {
			return array();
		}
		$placeholders = implode( ',', array_fill( 0, count( $contact_ids ), '%d' ) );
		$query = $wpdb->prepare( "SELECT id, email FROM $contact_table WHERE id IN ($placeholders)", $contact_ids );
		$results = $wpdb->get_results( $query, ARRAY_A );
		return $results;
	}

	/**
	 * Get contacts to verify with pagination
	 *
	 * @param int $offset Offset for pagination.
	 * @param int $batch_size Number of contacts to return.
	 * @return array Contacts to verify.
	 */
	public static function get_contacts_to_verify( $offset = 0, $batch_size = 100 ) {
		global $wpdb;
		$contact_table = $wpdb->prefix . ContactSchema::$table_name;

		// Prepare sql results for list view.
		$query_results = $wpdb->get_results( $wpdb->prepare( "SELECT id, email FROM $contact_table LIMIT %d, %d", array( $offset, $batch_size ) ), ARRAY_A ); // db call ok. ; no-cache ok.
		return $query_results;
	}

	/**
	 * Safely deserialize a contact meta value.
	 *
	 * Scalar values and plain strings are returned as-is. For serialized
	 * strings, only arrays are accepted as the result — this preserves
	 * legitimate checkbox/multi-select data while preventing PHP Object
	 * Injection: any deserialized object is discarded and the raw string
	 * returned instead.
	 *
	 * @param mixed $value Raw meta_value from the database.
	 * @return mixed
	 * @since 1.24.0
	 */
	private static function safe_unserialize_meta( $value ) {
		if ( ! is_serialized( $value ) ) {
			return $value;
		}
		$unserialized = maybe_unserialize( $value );
		return is_array( $unserialized ) ? $unserialized : $value;
	}
}
