<?php
/**
 * Campaigns advanced logic.
 *
 * @package automation-web-platform
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}


/**
 * Class WAWP_Campaigns_Advanced
 * Handles bulk campaigns and segmentation.
 */
class WAWP_Campaigns_Advanced {
	use WAWP_Security_Trait;
	use WAWP_Phone_Helper_Trait;
	use WAWP_Logging_Trait;

	/**
	 * Campaigns table name.
	 *
	 * @var string
	 */
	const CAMP_TABLE = 'wawp_campaigns';

	/**
	 * Campaigns queue table name.
	 *
	 * @var string
	 */
	const QUEUE_TABLE = 'wawp_campaigns_queue';

	/**
	 * Table name with prefix.
	 *
	 * @var string
	 */
	private $camp_table;

	/**
	 * Queue table name with prefix.
	 *
	 * @var string
	 */
	private $queue_table;

	/**
	 * WordPress DB instance.
	 *
	 * @var wpdb
	 */
	public $wpdb;

	/**
	 * Cron hook name.
	 *
	 * @var string
	 */
	private $cron_hook = 'wp_campaigns_cron_send_advanced';

	/**
	 * Check if segmentation is enabled.
	 *
	 * @return bool Enabled status.
	 */
	private function is_segmentation_enabled(): bool {
		$user_data = wawp_get_user_data();
		$status    = $user_data['segmented_notifications_enabled'] ?? 'disabled';

		return 'enabled' === $status;
	}

	/**
	 * Constructor.
	 */
	public function __construct() {
		global $wpdb;
		$this->wpdb        = $wpdb;
		$this->camp_table  = $wpdb->prefix . self::CAMP_TABLE;
		$this->queue_table = $wpdb->prefix . self::QUEUE_TABLE;

		add_action( $this->cron_hook, array( $this, 'cron_send' ) );
	}

	/**
	 * Get the private directory for audience files.
	 *
	 * @return string Absolute path with trailing slash.
	 */
	private function get_audience_dir() {
		global $wp_filesystem;
		if ( empty( $wp_filesystem ) ) {
			require_once ABSPATH . 'wp-admin/includes/file.php';
			WP_Filesystem();
		}
		$upload_dir = wp_upload_dir();
		$dir        = $upload_dir['basedir'] . '/wawp-private/audiences/';
		if ( ! $wp_filesystem->is_dir( $dir ) ) {
			wp_mkdir_p( $dir );
			$htaccess = $upload_dir['basedir'] . '/wawp-private/.htaccess';
			if ( ! $wp_filesystem->exists( $htaccess ) ) {
				$wp_filesystem->put_contents( $htaccess, "deny from all\n", FS_CHMOD_FILE );
			}
		}
		return $dir;
	}

	/**
	 * Write an audience array to a compressed file (one ID per line).
	 *
	 * @param int    $cid      Campaign ID.
	 * @param string $channel  Channel key: 'wa', 'email', 'ext'.
	 * @param array  $ids      Array of IDs or phone strings.
	 * @return bool Success.
	 */
	private function write_audience_file( $cid, $channel, $ids ) {
		global $wp_filesystem;
		if ( empty( $wp_filesystem ) ) {
			require_once ABSPATH . 'wp-admin/includes/file.php';
			WP_Filesystem();
		}
		$path = $this->get_audience_dir() . 'camp_' . (int) $cid . '_' . sanitize_key( $channel ) . '.json.gz';
		$data = implode( "\n", $ids );
		return $wp_filesystem->put_contents( $path, gzencode( $data, 6 ), FS_CHMOD_FILE );
	}

	/**
	 * Read a slice of an audience file without loading the whole file into memory.
	 *
	 * @param int    $cid     Campaign ID.
	 * @param string $channel Channel key.
	 * @param int    $offset  Line offset to start from.
	 * @param int    $length  Number of lines to read.
	 * @return array Slice of IDs.
	 */
	private function read_audience_chunk( $cid, $channel, $offset, $length ) {
		global $wp_filesystem;
		if ( empty( $wp_filesystem ) ) {
			require_once ABSPATH . 'wp-admin/includes/file.php';
			WP_Filesystem();
		}
		$path = $this->get_audience_dir() . 'camp_' . (int) $cid . '_' . sanitize_key( $channel ) . '.json.gz';
		if ( ! $wp_filesystem->exists( $path ) ) {
			return array();
		}
		$raw   = $wp_filesystem->get_contents( $path );
		$lines = explode( "\n", gzdecode( $raw ) );
		return array_slice( $lines, $offset, $length );
	}

	/**
	 * Get the total count of lines in an audience file.
	 *
	 * @param int    $cid     Campaign ID.
	 * @param string $channel Channel key.
	 * @return int Count.
	 */
	private function get_audience_count( $cid, $channel ) {
		global $wp_filesystem;
		if ( empty( $wp_filesystem ) ) {
			require_once ABSPATH . 'wp-admin/includes/file.php';
			WP_Filesystem();
		}
		$path = $this->get_audience_dir() . 'camp_' . (int) $cid . '_' . sanitize_key( $channel ) . '.json.gz';
		if ( ! $wp_filesystem->exists( $path ) ) {
			return 0;
		}
		$raw = $wp_filesystem->get_contents( $path );
		return substr_count( gzdecode( $raw ), "\n" ) + 1;
	}

	/**
	 * Delete all audience files for a campaign.
	 *
	 * @param int $cid Campaign ID.
	 */
	private function delete_audience_files( $cid ) {
		global $wp_filesystem;
		if ( empty( $wp_filesystem ) ) {
			require_once ABSPATH . 'wp-admin/includes/file.php';
			WP_Filesystem();
		}
		$dir      = $this->get_audience_dir();
		$channels = array( 'wa', 'email', 'ext' );
		foreach ( $channels as $ch ) {
			$path = $dir . 'camp_' . (int) $cid . '_' . $ch . '.json.gz';
			if ( $wp_filesystem->exists( $path ) ) {
				$wp_filesystem->delete( $path );
			}
		}
	}

	/**
	 * AJAX handler to get campaigns data.
	 *
	 * @return void
	 */
	public function ajax_get_campaigns_data() {
		$this->check_permissions( 'wawp_get_campaigns_data_nonce' );
		$res = $this->handle_get_campaigns_data();
		$this->send_success( '', $res );
	}

	/**
	 * AJAX handler to toggle campaign status.
	 *
	 * @return void
	 */
	public function ajax_toggle_campaign() {
		$this->check_permissions( 'wawp_toggle_campaign_nonce' );
		$id        = (int) filter_input( INPUT_POST, 'id', FILTER_SANITIZE_NUMBER_INT );
		$pause_val = (int) filter_input( INPUT_POST, 'pause_val', FILTER_SANITIZE_NUMBER_INT );
		$res       = $this->handle_toggle_campaign( $id, $pause_val );
		if ( $res['success'] ) {
			$this->send_success();
		} else {
			$this->send_error( $res['message'] );
		}
	}

	/**
	 * AJAX handler to rerun a campaign.
	 *
	 * @return void
	 */
	public function ajax_rerun_campaign() {
		$this->check_permissions( 'wawp_rerun_campaign_nonce' );
		$id  = (int) filter_input( INPUT_POST, 'id', FILTER_SANITIZE_NUMBER_INT );
		$res = $this->handle_rerun_campaign( $id );
		if ( $res['success'] ) {
			$this->send_success();
		} else {
			$this->send_error( $res['message'] );
		}
	}

	/**
	 * Handle getting campaigns data.
	 *
	 * @return array Campaigns data and stats.
	 */
	public function handle_get_campaigns_data() {
		global $wpdb;

		$cache_key = 'wawp_campaigns_data_list';
		$rows      = wp_cache_get( $cache_key, 'wawp_campaigns' );

		if ( false === $rows ) {

				$rows = $wpdb->get_results(
					$wpdb->prepare(
						"SELECT c.*, 
					(SELECT COUNT(*) FROM %i q WHERE q.campaign_id = c.id AND q.status = 'sent') as success_count,
					(SELECT COUNT(*) FROM %i q WHERE q.campaign_id = c.id AND q.status = 'error') as failed_count
					FROM %i c ORDER BY c.id DESC",
						$this->queue_table,
						$this->queue_table,
						$this->camp_table
					),
					ARRAY_A
				);

			foreach ( $rows as &$r ) {
				$r                       = (object) $r;
				$r->instances            = maybe_unserialize( $r->instances );
				$r->role_ids             = maybe_unserialize( $r->role_ids );
				$r->user_ids             = maybe_unserialize( $r->user_ids );
				$r->woo_ordered_products = maybe_unserialize( $r->woo_ordered_products );
				$r->woo_order_statuses   = maybe_unserialize( $r->woo_order_statuses );
				$r->billing_countries    = maybe_unserialize( $r->billing_countries );
				$r->wp_profile_languages = maybe_unserialize( $r->wp_profile_languages );
				$r->type                 = $r->send_type;
				$r->success_count        = (int) $r->success_count;
				$r->failed_count         = (int) $r->failed_count;
			}
				wp_cache_set( $cache_key, $rows, 'wawp_campaigns', 30 );
		}

		// 2. Snapshot Stats
		$total_campaigns  = count( $rows );
		$active_campaigns = 0;
		$total_recipients = 0;
		foreach ( $rows as $r ) {
			if ( 'active' === $r->status && ( empty( $r->paused ) || 0 === $r->paused ) ) {
				++$active_campaigns;
			}
			$total_recipients += (int) $r->total_count;
		}

		$counts_cache_key = 'wawp_campaign_global_counts';
		$counts           = wp_cache_get( $counts_cache_key, 'wawp_campaigns' );

		if ( false === $counts ) {

				$stats_raw = $wpdb->get_results( $wpdb->prepare( 'SELECT status, count(*) as cnt FROM %i WHERE campaign_id > 0 GROUP BY status', $this->queue_table ), ARRAY_A );
				$counts    = array(
					'sent'    => 0,
					'error'   => 0,
					'pending' => 0,
				);
				foreach ( $stats_raw as &$s ) {
					$s = (object) $s;
					if ( isset( $counts[ $s->status ] ) ) {
						$counts[ $s->status ] += $s->cnt;
					}
				}
				wp_cache_set( $counts_cache_key, $counts, 'wawp_campaigns', 30 );
		}

		$total_processed = ( $counts['sent'] + $counts['error'] );
		$total_pending   = $counts['pending'];
		$total_failed    = $counts['error'];

		// 3. Metadata for Wizard
		global $wp_roles;
		$roles = array();
		if ( $wp_roles && isset( $wp_roles->roles ) ) {
			foreach ( $wp_roles->roles as $key => $role_details ) {
				$roles[] = array(
					'id'    => $key,
					'label' => $role_details['name'],
				);
			}
		}

		$instances_raw = WAWP()->instance_manager->get_online_instances_cached();
		$instances     = array();
		foreach ( $instances_raw as $inst ) {
			$instances[] = array(
				'id'              => $inst->id,
				'instance_api_id' => $inst->instance_id,
				'label'           => "{$inst->name} (ID: {$inst->id})",
			);
		}

		$woo_countries = array();
		$products      = array();
		if ( class_exists( 'WooCommerce' ) ) {
			$woo_countries   = WC()->countries->get_countries();
			$cache_key_prods = 'wawp_campaign_wizard_products';
			$products        = wp_cache_get( $cache_key_prods, 'wawp_database' );
			if ( false === $products ) {
				$prods_raw = get_posts(
					array(
						'post_type'   => 'product',
						'numberposts' => 100, // Limit for wizard performance.
						'post_status' => 'publish',
						'fields'      => 'ids',
					)
				);
				$products  = array_map(
					function ( $pid ) {
						return array(
							'id'    => $pid,
							'label' => get_the_title( $pid ),
						);
					},
					$prods_raw
				);
				wp_cache_set( $cache_key_prods, $products, 'wawp_database', HOUR_IN_SECONDS );
			}
		}

		$cache_key_users = 'wawp_campaign_wizard_users';
		$users           = wp_cache_get( $cache_key_users, 'wawp_database' );
		if ( false === $users ) {
			$users = get_transient( $cache_key_users );
			if ( false === $users ) {
				$users_raw = get_users(
					array(
						'number'  => 500,
						'orderby' => 'ID',
						'order'   => 'DESC',
						'fields'  => array( 'ID', 'display_name', 'user_email' ),
					)
				);
				$users     = array_map(
					function ( $u ) {
						return array(
							'id'    => (string) $u->ID,
							'label' => "{$u->display_name} ({$u->user_email})",
						);
					},
					$users_raw
				);
				set_transient( $cache_key_users, $users, 6 * HOUR_IN_SECONDS );
			}
			wp_cache_set( $cache_key_users, $users, 'wawp_database', 6 * HOUR_IN_SECONDS );
		}

		$meta_templates = class_exists( 'WAWP_Meta_Sender' ) ? WAWP_Meta_Sender::get_meta_templates() : array();

		$cache_key_posts = 'wawp_campaign_wizard_posts';
		$posts_data      = wp_cache_get( $cache_key_posts, 'wawp_database' );
		if ( false === $posts_data ) {
			$posts      = get_posts(
				array(
					'post_type'   => array( 'post', 'page' ),
					'numberposts' => 100,
					'fields'      => 'ids',
				)
			); // Limit to last 100 for wizard performance.
			$posts_data = array_map(
				function ( $pid ) {
					return array(
						'id'    => $pid,
						'label' => get_the_title( $pid ),
					);
				},
				$posts
			);
			wp_cache_set( $cache_key_posts, $posts_data, 'wawp_database', HOUR_IN_SECONDS );
		}

		$email_templates_cache_key = 'wawp_email_templates_id_name';
		$table                     = $wpdb->prefix . 'wawp_email_templates';
		$email_templates           = WAWP()->db->get_results_cached( $wpdb->prepare( 'SELECT id, name FROM %i', $table ), $email_templates_cache_key, 'wawp_notif', HOUR_IN_SECONDS );

		return array(
			'campaigns' => $rows,
			'stats'     => array(
				'total'      => $total_campaigns,
				'active'     => $active_campaigns,
				'recipients' => $total_recipients,
				'processed'  => $total_processed,
				'pending'    => $total_pending,
				'failed'     => $total_failed,
			),
			'meta'      => array(
				'roles'           => $roles,
				'users'           => $users,
				'products'        => $products,
				'posts'           => $posts_data,
				'email_templates' => $email_templates,
				'instances'       => $instances,
				'woo_countries'   => $woo_countries,
				'meta_templates'  => $meta_templates,
				'icon_base_url'   => WAWP_PLUGIN_URL . 'assets/icons/',
				'nonces'          => array(
					'calc'    => wp_create_nonce( 'camp_calc_recip_nonce' ),
					'preview' => wp_create_nonce( 'camp_preview_nonce' ),
					'rerun'   => wp_create_nonce( 'wawp_rerun_campaign_nonce' ),
					'delete'  => wp_create_nonce( 'wawp_bulk_delete_campaigns_nonce' ),
					'save'    => wp_create_nonce( 'camp_wizard_save_nonce' ),
					'fetch'   => wp_create_nonce( 'camp_fetch_nonce' ),
					'toggle'  => wp_create_nonce( 'wawp_toggle_campaign_nonce' ),
					'risky'   => wp_create_nonce( 'camp_run_risky_nonce' ),
				),
			),
		);
	}

	/**
	 * Handle toggling a campaign.
	 *
	 * @param int $id Campaign ID.
	 * @param int $pause_val Pause value (0 or 1).
	 * @return array Result.
	 */
	public function handle_toggle_campaign( $id, $pause_val ) {
		if ( ! $id ) {
			return array(
				'success' => false,
				'message' => 'CAMPAIGN_ID_MISSING',
			);
		}

		$update_values = array(
			'paused' => $pause_val,
		);

		if ( 0 === $pause_val ) {
			// Resuming.
			$campaign = WAWP()->db->get_row_by_id( 'campaigns', $id );
			if ( $campaign && 'active' === $campaign->status ) {
				$update_values['next_run'] = current_time( 'mysql' );
			}
		}

		WAWP()->db->execute_update( $this->camp_table, $update_values, array( 'id' => $id ) );
		wp_cache_delete( 'wawp_campaigns_data_list', 'wawp_campaigns' );
		wp_cache_delete( 'wawp_campaign_global_counts', 'wawp_campaigns' );
		wp_cache_delete( 'wawp_campaign_data_' . $id, 'wawp_campaigns' );
		delete_transient( 'wawp_campaigns_data_list' );
		delete_transient( 'wawp_campaign_global_counts' );

		if ( 0 === $pause_val && isset( $update_values['next_run'] ) ) {
			if ( ! wp_next_scheduled( $this->cron_hook ) ) {
				wp_schedule_single_event( time(), $this->cron_hook );
			}
		}

		return array( 'success' => true );
	}

	/**
	 * Handle rerunning a campaign.
	 *
	 * @param int $id Campaign ID.
	 * @return array Result.
	 */
	public function handle_rerun_campaign( $id ) {
		if ( ! $id ) {
			return array(
				'success' => false,
				'message' => 'Invalid Campaign ID',
			);
		}

		global $wpdb;

		// Reset queue items so they can be re-sent on rerun.
		WAWP()->db->execute_query( $wpdb->prepare( "UPDATE %i SET status = 'pending', retry_count = 0, sent_at = NULL, last_error = NULL WHERE campaign_id = %d", $this->queue_table, $id ) );
		$this->delete_audience_files( $id );
		delete_option( 'wawp_camp_gen_queue_' . $id );

		// Reset processed_count and set status to active.
		WAWP()->db->execute_update(
			$this->camp_table,
			array(
				'processed_count' => 0,
				'status'          => 'active',
				'paused'          => 0,
				'next_run'        => current_time( 'mysql' ),
			),
			array( 'id' => $id )
		);
		wp_cache_delete( 'wawp_campaign_data_' . $id, 'wawp_campaigns' );
		wp_cache_delete( 'wawp_campaigns_data_list', 'wawp_campaigns' );
		wp_cache_delete( 'wawp_campaign_global_counts', 'wawp_campaigns' );
		delete_transient( 'wawp_campaigns_data_list' );
		delete_transient( 'wawp_campaign_global_counts' );

		return array( 'success' => true );
	}

	/**
	 * AJAX handler for bulk deleting campaigns.
	 *
	 * @return void
	 */
	public function ajax_bulk_delete_campaigns() {
		$nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
		if ( ! wp_verify_nonce( $nonce, 'wawp_bulk_delete_campaigns_nonce' ) ) {
			$this->send_error( esc_html__( 'Security check failed.', 'automation-web-platform' ), 403 );
		}
		$this->check_permissions( 'wawp_bulk_delete_campaigns_nonce' );
		$campaign_id = isset( $_POST['campaign_id'] ) ? (int) $_POST['campaign_id'] : 0;
		$ids         = isset( $_POST['ids'] ) ? array_map( 'intval', (array) wp_unslash( $_POST['ids'] ) ) : array();

		if ( ! $campaign_id || empty( $ids ) ) {
			$ids = array();
		}
		$ids = array_map( 'absint', $ids );
		$res = $this->handle_bulk_delete_campaigns( $ids );
		if ( $res['success'] ) {
			$this->send_success();
		} else {
			$this->send_error( $res['message'] );
		}
	}

	/**
	 * Handle bulk deleting campaigns.
	 *
	 * @param array $ids Campaign IDs.
	 * @return array Result.
	 */
	public function handle_bulk_delete_campaigns( $ids ) {
		if ( empty( $ids ) ) {
			return array(
				'success' => false,
				'message' => 'No IDs provided',
			);
		}

		global $wpdb;
		$ids = array_map( 'intval', (array) $ids );
		if ( empty( $ids ) ) {
			return array(
				'success' => false,
				'message' => 'Invalid IDs',
			);
		}

		foreach ( $ids as $id ) {
			WAWP()->db->execute_delete( $this->camp_table, array( 'id' => $id ) );
			WAWP()->db->execute_delete( $this->queue_table, array( 'campaign_id' => $id ) );
			$this->delete_audience_files( $id );
			delete_option( 'wawp_camp_gen_queue_' . $id );
		}

		wp_cache_delete( 'wawp_campaigns_data_list', 'wawp_campaigns' );
		wp_cache_delete( 'wawp_campaign_global_counts', 'wawp_campaigns' );
		delete_transient( 'wawp_campaigns_data_list' );
		delete_transient( 'wawp_campaign_global_counts' );

		return array( 'success' => true );
	}

	/**
	 * AJAX handler for saving a campaign.
	 *
	 * @return void
	 */
	public function ajax_save_campaign() {
		$nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
		if ( ! wp_verify_nonce( $nonce, 'wawp_save_campaign_nonce' ) ) {
			$this->send_error( esc_html__( 'Security check failed.', 'automation-web-platform' ), 403 );
		}
		$this->check_permissions( 'wawp_save_campaign_nonce' );
		$params = filter_input_array( INPUT_POST, FILTER_SANITIZE_SPECIAL_CHARS );
		$res    = $this->handle_save_campaign( $params ? $params : array() );
		if ( $res['success'] ) {
			$this->send_success( '', $res );
		} else {
			$this->send_error( $res['message'] );
		}
	}

	/**
	 * Handle saving a campaign.
	 *
	 * @param array $data Campaign data.
	 * @return array Result.
	 */
	public function handle_save_campaign( $data ) {
		global $wpdb;
		$id = isset( $data['id'] ) ? intval( wp_unslash( $data['id'] ) ) : 0;

		// Idempotency guard: prevent double insert from rapid duplicate requests (new campaigns only).
		if ( 0 === $id ) {
			$idempotency_key = 'wawp_camp_save_' . get_current_user_id() . '_' . md5( wp_json_encode( array_intersect_key( $data, array_flip( array( 'name', 'send_whatsapp', 'send_email', 'instances', 'start_datetime' ) ) ) ) );
			if ( get_transient( $idempotency_key ) ) {
				return array(
					'success' => false,
					'message' => esc_html__( 'Duplicate request detected. Please wait a moment.', 'automation-web-platform' ),
				);
			}
			set_transient( $idempotency_key, 1, 10 );
		}

		if ( ! $this->is_segmentation_enabled() ) {
			return array(
				'success' => false,
				'message' => esc_html__( 'Bulk Campaigns feature is not active for your account.', 'automation-web-platform' ),
			);
		}

		// Map React fields.
		$name              = sanitize_text_field( wp_unslash( $data['name'] ?? 'New Campaign' ) );
		$is_save_for_later = ( isset( $data['save_for_later'] ) && ( '1' === $data['save_for_later'] || 'true' === $data['save_for_later'] || true === $data['save_for_later'] ) );
		$send_whatsapp     = (int) ( $data['send_whatsapp'] ?? 0 );
		$send_email        = (int) ( $data['send_email'] ?? 0 );
		$send_meta         = (int) ( $data['send_meta'] ?? 0 );
		$only_verified     = (int) ( $data['only_verified_phone'] ?? 0 );

		$min_wa_interval    = intval( wp_unslash( $data['min_whatsapp_interval'] ?? 30 ) );
		$max_wa_interval    = intval( wp_unslash( $data['max_whatsapp_interval'] ?? 60 ) );
		$min_email_interval = intval( wp_unslash( $data['min_email_interval'] ?? 30 ) );
		$max_email_interval = intval( wp_unslash( $data['max_email_interval'] ?? 60 ) );

		$max_wa_per_day    = intval( wp_unslash( $data['wa_daily_limit'] ?? 0 ) );
		$max_email_per_day = intval( wp_unslash( $data['email_daily_limit'] ?? 0 ) );

		$start_datetime_raw = sanitize_text_field( wp_unslash( $data['start_datetime'] ?? '' ) );
		$repeat_type        = sanitize_text_field( wp_unslash( $data['repeat_campaign'] ?? 'no_repeat' ) );
		$repeat_days        = intval( $data['repeat_days'] ?? 1 );

		$message_type     = sanitize_text_field( wp_unslash( $data['message_type'] ?? 'text' ) );
		$media_url        = sanitize_text_field( wp_unslash( $data['media_url'] ?? '' ) );
		$share_post_id    = intval( wp_unslash( $data['share_post_id'] ?? 0 ) );
		$share_product_id = intval( wp_unslash( $data['share_product_id'] ?? 0 ) );

		$whatsapp_message   = wp_kses_post( wp_unslash( $data['message'] ?? '' ) );
		$email_subject      = sanitize_text_field( wp_unslash( $data['email_subject'] ?? '' ) );
		$email_message      = wp_kses_post( wp_unslash( $data['email_message'] ?? '' ) );
		$email_content_json = wp_unslash( $data['email_content_json'] ?? '' );

		$append_post    = (int) wp_unslash( $data['append_post'] ?? 0 );
		$append_product = (int) wp_unslash( $data['append_product'] ?? 0 );

		$instances            = isset( $data['instances'] ) ? array_map( 'sanitize_text_field', (array) $data['instances'] ) : array();
		$role_ids             = isset( $data['role_ids'] ) ? array_map( 'sanitize_text_field', (array) $data['role_ids'] ) : array();
		$user_ids             = isset( $data['user_ids'] ) ? array_map( 'intval', (array) $data['user_ids'] ) : array();
		$billing_countries    = isset( $data['billing_countries'] ) ? array_map( 'sanitize_text_field', (array) $data['billing_countries'] ) : array();
		$wp_profile_languages = isset( $data['wp_profile_languages'] ) ? array_map( 'sanitize_text_field', (array) $data['wp_profile_languages'] ) : array();
		$woo_ordered_products = isset( $data['woo_ordered_products'] ) ? array_map( 'intval', (array) $data['woo_ordered_products'] ) : array();
		$woo_order_statuses   = isset( $data['woo_order_statuses'] ) ? array_map( 'sanitize_text_field', (array) $data['woo_order_statuses'] ) : array();

		$woo_spent_over  = floatval( $data['total_spent_min'] ?? 0 );
		$woo_orders_over = intval( $data['orders_count_min'] ?? 0 );

		// Calculate recipients.
		$all_wp_user_ids            = $this->merge_roles_users( $role_ids, $user_ids );
		$whatsapp_recipients        = array();
		$email_recipients           = array();
		$external_numbers_for_queue = array();

		if ( $send_whatsapp || $send_meta ) {
			$whatsapp_recipients  = $this->apply_audience_filters( $all_wp_user_ids, true, false, false, $billing_countries, $wp_profile_languages, $woo_spent_over, $woo_orders_over, $woo_ordered_products, $woo_order_statuses, $only_verified );
			$external_numbers_raw = sanitize_textarea_field( wp_unslash( $data['external_numbers'] ?? '' ) );
			if ( $external_numbers_raw ) {
				$lines = preg_split( '/[\r\n]+/', $external_numbers_raw );
				foreach ( $lines as $ln ) {
					list( $digits, ) = self::format_phone_number( trim( $ln ) );
					if ( $digits ) {
						$external_numbers_for_queue[] = $digits;
					}
				}
			}
		}

		if ( $send_email ) {
			$email_recipients = $this->apply_audience_filters( $all_wp_user_ids, false, true, false, $billing_countries, $wp_profile_languages, $woo_spent_over, $woo_orders_over, $woo_ordered_products, $woo_order_statuses, false );
		}

		$total_wa    = count( $whatsapp_recipients ) + count( $external_numbers_for_queue );
		$total_email = count( $email_recipients );
		$total_count = $total_wa + $total_email;

		$target_status = 'active';
		$status        = 'saved';
		if ( isset( $data['status'] ) && 'draft' === $data['status'] ) {
			$status = 'draft';
		} elseif ( ! $is_save_for_later && $total_count > 0 ) {
			$is_risky      = ( $total_wa > 500 && ( 0 === $max_wa_per_day || $max_wa_per_day > 500 ) );
			$target_status = $is_risky ? 'risky' : 'active';
			$status        = 'queueing';
		}

		$start_datetime = current_time( 'mysql' );
		if ( ! empty( $start_datetime_raw ) ) {
			$start_datetime = wp_date( 'Y-m-d H:i:s', strtotime( $start_datetime_raw ) );
		}

		$post_info = isset( $data['post_info'] ) ? $data['post_info'] : array();
		if ( is_string( $post_info ) ) {
			$post_info = json_decode( stripslashes( $post_info ), true );
		}
		if ( ! is_array( $post_info ) ) {
			$post_info = array();
		}

		$product_info = isset( $data['product_info'] ) ? $data['product_info'] : array();
		if ( is_string( $product_info ) ) {
			$product_info = json_decode( stripslashes( $product_info ), true );
		}
		if ( ! is_array( $product_info ) ) {
			$product_info = array();
		}

		$post_include_title   = isset( $post_info['title'] ) ? (int) $post_info['title'] : 0;
		$post_include_excerpt = isset( $post_info['excerpt'] ) ? (int) $post_info['excerpt'] : 0;
		$post_include_link    = isset( $post_info['link'] ) ? (int) $post_info['link'] : 0;
		$post_include_image   = isset( $post_info['image'] ) ? (int) $post_info['image'] : 0;

		$product_include_title   = isset( $product_info['title'] ) ? (int) $product_info['title'] : 0;
		$product_include_price   = isset( $product_info['price'] ) ? (int) $product_info['price'] : 0;
		$product_include_excerpt = isset( $product_info['description'] ) ? (int) $product_info['description'] : 0;
		$product_include_link    = isset( $product_info['link'] ) ? (int) $product_info['link'] : 0;
		$product_include_image   = isset( $product_info['image'] ) ? (int) $product_info['image'] : 0;

		$db_data = array(
			'name'                    => $name,
			'instances'               => maybe_serialize( $instances ),
			'role_ids'                => maybe_serialize( $role_ids ),
			'user_ids'                => maybe_serialize( $user_ids ),
			'external_numbers'        => $data['external_numbers'] ?? '',
			'external_emails'         => $data['external_emails'] ?? '',
			'message'                 => $whatsapp_message,
			'media_url'               => $media_url,
			'min_whatsapp_interval'   => $min_wa_interval,
			'max_whatsapp_interval'   => $max_wa_interval,
			'min_email_interval'      => $min_email_interval,
			'max_email_interval'      => $max_email_interval,
			'start_datetime'          => $start_datetime,
			'repeat_type'             => $repeat_type,
			'repeat_days'             => $repeat_days,
			'post_id'                 => $share_post_id,
			'product_id'              => $share_product_id,
			'append_post'             => $append_post,
			'append_product'          => $append_product,
			'send_type'               => $message_type,
			'total_count'             => $total_count,
			'status'                  => $status,
			'next_run'                => ( 'queueing' === $status || 'active' === $status ) ? $start_datetime : null,
			'woo_spent_over'          => $woo_spent_over,
			'woo_orders_over'         => $woo_orders_over,
			'only_verified_phone'     => $only_verified,
			'woo_ordered_products'    => maybe_serialize( $woo_ordered_products ),
			'woo_order_statuses'      => maybe_serialize( $woo_order_statuses ),
			'max_wa_per_day'          => $max_wa_per_day,
			'max_email_per_day'       => $max_email_per_day,
			'billing_countries'       => maybe_serialize( $billing_countries ),
			'wp_profile_languages'    => maybe_serialize( $wp_profile_languages ),
			'send_whatsapp'           => $send_whatsapp,
			'send_email'              => $send_email,
			'send_meta'               => $send_meta,
			'email_subject'           => $email_subject,
			'email_message'           => $email_message,
			'email_content_json'      => $email_content_json,
			'meta_message_type'       => sanitize_text_field( wp_unslash( $data['meta_message_type'] ?? 'text' ) ),
			'meta_template_name'      => sanitize_text_field( wp_unslash( $data['meta_template_id'] ?? '' ) ),
			'meta_template_lang'      => sanitize_text_field( wp_unslash( $data['meta_template_lang'] ?? '' ) ),
			'meta_message_text'       => wp_kses_post( wp_unslash( $data['meta_message_text'] ?? '' ) ),
			'post_include_title'      => $post_include_title,
			'post_include_excerpt'    => $post_include_excerpt,
			'post_include_link'       => $post_include_link,
			'post_include_image'      => $post_include_image,
			'product_include_title'   => $product_include_title,
			'product_include_excerpt' => $product_include_excerpt,
			'product_include_link'    => $product_include_link,
			'product_include_image'   => $product_include_image,
			'product_include_price'   => $product_include_price,
		);

		if ( $id > 0 ) {

			WAWP()->db->execute_update( $this->camp_table, $db_data, array( 'id' => $id ) );

			$cid = $id;
		} else {

			WAWP()->db->execute_insert( $this->camp_table, $db_data );
			$cid = $wpdb->insert_id;
		}

		if ( ! $cid ) {
			return array(
				'success' => false,
				'message' => esc_html__( 'Failed to save to database.', 'automation-web-platform' ),
			);
		}

		if ( 'queueing' === $status ) {
			WAWP()->db->execute_delete( $this->queue_table, array( 'campaign_id' => $cid ) );

			$this->write_audience_file( $cid, 'wa', $whatsapp_recipients );
			$this->write_audience_file( $cid, 'email', $email_recipients );
			$this->write_audience_file( $cid, 'ext', $external_numbers_for_queue );

			$gen_state = array(
				'campaign_id'     => $cid,
				'target_status'   => $target_status,
				'whatsapp_count'  => count( $whatsapp_recipients ),
				'email_count'     => count( $email_recipients ),
				'external_count'  => count( $external_numbers_for_queue ),
				'whatsapp_offset' => 0,
				'email_offset'    => 0,
				'external_offset' => 0,
				'meta_offset'     => 0,
			);
			update_option( 'wawp_camp_gen_queue_' . $cid, $gen_state, false );

			if ( ! wp_next_scheduled( $this->cron_hook ) ) {
				wp_schedule_single_event( time(), $this->cron_hook );
			}
		}

		// Clear caches.
		wp_cache_delete( 'wawp_campaigns_data_list', 'wawp_campaigns' );
		wp_cache_delete( 'wawp_campaign_global_counts', 'wawp_campaigns' );
		delete_transient( 'wawp_campaigns_data_list' );
		delete_transient( 'wawp_campaign_global_counts' );

		return array(
			'success' => true,
			'id'      => $cid,
		);
	}

	/**
	 * AJAX handler to run a risky campaign.
	 *
	 * @return void
	 */
	public function ajax_run_risky() {
		$nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
		if ( ! wp_verify_nonce( $nonce, 'camp_run_risky_nonce' ) ) {
			$this->send_error( esc_html__( 'Security check failed.', 'automation-web-platform' ), 403 );
		}
		$this->check_permissions( 'camp_run_risky_nonce' );
		$id  = intval( wp_unslash( $_POST['cid'] ?? 0 ) );
		$res = $this->handle_run_risky( $id );
		if ( $res['success'] ) {
			$this->send_success();
		} else {
			$this->send_error( $res['message'] );
		}
	}

	/**
	 * Handle running a risky campaign.
	 *
	 * @param int $id Campaign ID.
	 * @return array Result.
	 */
	public function handle_run_risky( $id ) {
		if ( ! $id ) {
			return array(
				'success' => false,
				'message' => 'Invalid ID',
			);
		}

		WAWP()->db->execute_update(
			$this->camp_table,
			array(
				'status'   => 'active',
				'next_run' => current_time( 'mysql' ),
			),
			array( 'id' => $id )
		);
		wp_cache_delete( 'wawp_campaign_data_' . $id, 'wawp_campaigns' );
		wp_cache_delete( 'wawp_campaigns_data_list', 'wawp_campaigns' );

		if ( ! wp_next_scheduled( $this->cron_hook ) ) {
			wp_schedule_single_event( time(), $this->cron_hook );
		}

		return array( 'success' => true );
	}

	/**
	 * AJAX handler to calculate recipients.
	 */
	public function ajax_wizard_calc_recipients() {
		$nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
		if ( ! wp_verify_nonce( $nonce, 'camp_calc_recip_nonce' ) ) {
			$this->send_error( esc_html__( 'Security check failed.', 'automation-web-platform' ), 403 );
		}
		$this->check_permissions( 'camp_calc_recip_nonce' );
		$res = $this->handle_wizard_calc_recipients( $_POST );
		$this->send_success( '', $res );
	}

	/**
	 * AJAX handler for message preview.
	 */
	public function ajax_preview() {
		$nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
		if ( ! wp_verify_nonce( $nonce, 'camp_preview_nonce' ) ) {
			$this->send_error( esc_html__( 'Security check failed.', 'automation-web-platform' ), 403 );
		}
		$this->check_permissions( 'camp_preview_nonce' );
		$msg = wp_kses_post( wp_unslash( $_POST['message'] ?? '' ) );
		$uid = intval( wp_unslash( $_POST['user_id'] ?? get_current_user_id() ) );
		$res = $this->handle_preview( $msg, $uid );
		$this->send_success( '', $res );
	}

	/**
	 * Handle calculating recipients.
	 *
	 * @param array $data Wizard data.
	 * @return array Count.
	 */
	public function handle_wizard_calc_recipients( $data ) {
		$role_ids             = isset( $data['role_ids'] ) ? array_map( 'sanitize_text_field', (array) $data['role_ids'] ) : array();
		$user_ids             = isset( $data['user_ids'] ) ? array_map( 'intval', (array) $data['user_ids'] ) : array();
		$billing_countries    = isset( $data['billing_countries'] ) ? array_map( 'sanitize_text_field', (array) $data['billing_countries'] ) : array();
		$wp_profile_languages = isset( $data['wp_profile_languages'] ) ? array_map( 'sanitize_text_field', (array) $data['wp_profile_languages'] ) : array();
		$woo_spent_over       = floatval( $data['total_spent_min'] ?? 0 );
		$woo_orders_over      = intval( $data['orders_count_min'] ?? 0 );
		$woo_ordered_products = isset( $data['woo_ordered_products'] ) ? array_map( 'intval', (array) $data['woo_ordered_products'] ) : array();
		$woo_order_statuses   = isset( $data['woo_order_statuses'] ) ? array_map( 'sanitize_text_field', (array) $data['woo_order_statuses'] ) : array();
		$only_verified        = (int) ( $data['only_verified_phone'] ?? 0 );

		$all_wp_user_ids = $this->merge_roles_users( $role_ids, $user_ids );
		$recipients      = $this->filter_by_billing_countries( $all_wp_user_ids, $billing_countries );
		$recipients      = $this->filter_by_profile_languages( $recipients, $wp_profile_languages );
		$recipients      = $this->segment_filter( $recipients, $woo_spent_over, $woo_orders_over, $only_verified );

		if ( ! empty( $woo_ordered_products ) || ! empty( $woo_order_statuses ) ) {
			$recipients = $this->filter_by_purchase_and_status( $recipients, $woo_ordered_products, $woo_order_statuses );
		}

		$external_numbers_raw = sanitize_textarea_field( wp_unslash( $data['external_numbers'] ?? '' ) );
		$ext_count            = 0;
		if ( $external_numbers_raw ) {
			$lines = preg_split( '/[\r\n]+/', $external_numbers_raw );
			foreach ( $lines as $ln ) {
				if ( trim( $ln ) ) {
					++$ext_count;
				}
			}
		}

		return array( 'count' => (int) count( $recipients ) + $ext_count );
	}

	/**
	 * Handle message preview.
	 *
	 * @param string $msg Message content.
	 * @param int    $uid User ID.
	 * @return array Preview data.
	 */
	public function handle_preview( $msg, $uid ) {
		$parsed = $this->parse_placeholders( $msg, $uid );

		return array( 'preview' => $parsed );
	}

	/**
	 * Apply audience filters.
	 *
	 * @param array $uids      User IDs.
	 * @param bool  $wa        Send WhatsApp.
	 * @param bool  $email     Send Email.
	 * @param bool  $meta      Send Meta.
	 * @param array $countries Countries.
	 * @param array $langs     Languages.
	 * @param float $spent     Amount spent.
	 * @param int   $orders    Order count.
	 * @param array $products  Product IDs.
	 * @param array $statuses  Order statuses.
	 * @param bool  $verified  Only verified.
	 * @return array Filtered user IDs.
	 */
	private function apply_audience_filters( $uids, $wa, $email, $meta, $countries, $langs, $spent, $orders, $products, $statuses, $verified ) {
		$uids = $this->filter_by_billing_countries( $uids, $countries );
		$uids = $this->filter_by_profile_languages( $uids, $langs );
		$uids = $this->segment_filter( $uids, $spent, $orders, $verified );
		if ( ! empty( $products ) || ! empty( $statuses ) ) {
			$uids = $this->filter_by_purchase_and_status( $uids, $products, $statuses );
		}

		return $uids;
	}

	/**
	 * Merge roles and user IDs.
	 *
	 * @param array $role_ids Role IDs.
	 * @param array $user_ids User IDs.
	 * @return array Merged IDs.
	 */
	private function merge_roles_users( $role_ids = array(), $user_ids = array() ) {
		global $wpdb;
		$uids = is_array( $user_ids ) ? array_map( 'intval', $user_ids ) : array();
		if ( ! empty( $role_ids ) && is_array( $role_ids ) ) {
			foreach ( $role_ids as $role ) {
				$role_key   = $wpdb->prefix . 'capabilities';
				$role_value = '%"' . esc_sql( $role ) . '"%';
				$offset     = 0;
				$batch_size = 5000;
				do {
					// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
					$batch = $wpdb->get_col(
						$wpdb->prepare(
							"SELECT user_id FROM {$wpdb->usermeta} WHERE meta_key = %s AND meta_value LIKE %s ORDER BY user_id ASC LIMIT %d OFFSET %d",
							$role_key,
							$role_value,
							$batch_size,
							$offset
						)
					);
					// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
					if ( ! empty( $batch ) ) {
						$uids = array_merge( $uids, array_map( 'intval', $batch ) );
					}
					$offset     += $batch_size;
					$batch_count = count( $batch );
				} while ( $batch_count === $batch_size );
			}
		}

		return array_unique( $uids );
	}

	/**
	 * Filter users by billing countries.
	 *
	 * @param array $uids User IDs.
	 * @param array $countries Countries.
	 * @return array Filtered IDs.
	 */
	private function filter_by_billing_countries( $uids, $countries ) {
		if ( empty( $countries ) || empty( $uids ) ) {
			return $uids;
		}

		global $wpdb;
		$uid_ints        = array_map( 'intval', $uids );
		$clean_countries = array_map( 'esc_sql', $countries );
		$country_list    = "'" . implode( "','", $clean_countries ) . "'";

		$matching_ids = array();
		foreach ( array_chunk( $uid_ints, 1000 ) as $chunk ) {
			$id_list   = implode( ',', $chunk );
			$sql       = "SELECT user_id FROM {$wpdb->usermeta} WHERE meta_key = 'billing_country' AND meta_value IN ({$country_list}) AND user_id IN ({$id_list})";
			$chunk_ids = WAWP()->db->execute_get_col( $sql );
			if ( ! empty( $chunk_ids ) ) {
				$matching_ids = array_merge( $matching_ids, array_map( 'intval', $chunk_ids ) );
			}
		}

		return array_values( array_intersect( $uid_ints, $matching_ids ) );
	}

	/**
	 * Filter users by profile languages.
	 *
	 * @param array $uids User IDs.
	 * @param array $langs Languages.
	 * @return array Filtered IDs.
	 */
	private function filter_by_profile_languages( $uids, $langs ) {
		if ( empty( $langs ) || empty( $uids ) ) {
			return $uids;
		}

		global $wpdb;
		$uid_ints    = array_map( 'intval', $uids );
		$clean_langs = array_map( 'esc_sql', $langs );
		$langs_list  = "'" . implode( "','", $clean_langs ) . "'";

		$matching_ids = array();
		foreach ( array_chunk( $uid_ints, 1000 ) as $chunk ) {
			$id_list   = implode( ',', $chunk );
			$sql       = "SELECT user_id FROM {$wpdb->usermeta} WHERE meta_key = 'wawp_profile_language' AND meta_value IN ({$langs_list}) AND user_id IN ({$id_list})";
			$chunk_ids = WAWP()->db->execute_get_col( $sql );
			if ( ! empty( $chunk_ids ) ) {
				$matching_ids = array_merge( $matching_ids, array_map( 'intval', $chunk_ids ) );
			}
		}

		return array_values( array_intersect( $uid_ints, $matching_ids ) );
	}

	/**
	 * Segment filter based on spent and orders.
	 *
	 * @param array $uids          User IDs.
	 * @param float $spent         Amount spent.
	 * @param int   $orders        Order count.
	 * @param bool  $only_verified Only verified.
	 * @return array Filtered IDs.
	 */
	private function segment_filter( $uids, $spent, $orders, $only_verified ) {
		if ( empty( $uids ) ) {
			return $uids;
		}

		if ( ! class_exists( 'WooCommerce' ) && ( $spent > 0 || $orders > 0 ) ) {
			return $uids;
		}

		global $wpdb;

		$uid_ints = array_map( 'intval', $uids );

		// Filter by verified status — one bulk usermeta query per chunk instead of N queries.
		if ( $only_verified ) {
			$verified_ids = array();
			foreach ( array_chunk( $uid_ints, 1000 ) as $chunk ) {
				$id_list      = implode( ',', $chunk );
				$verified_sql = "SELECT user_id FROM {$wpdb->usermeta}"
					. " WHERE meta_key = 'wawp_whatsapp_verified'"
					. " AND meta_value = 'Verified'"
					. " AND user_id IN ({$id_list})";
				$chunk_ids    = WAWP()->db->execute_get_col( $verified_sql );
				if ( ! empty( $chunk_ids ) ) {
					$verified_ids = array_merge( $verified_ids, array_map( 'intval', $chunk_ids ) );
				}
			}
			$uid_ints = array_values( array_intersect( $uid_ints, $verified_ids ) );
			if ( empty( $uid_ints ) ) {
				return array();
			}
		}

		// Filter by WooCommerce spent/orders — one aggregate query per chunk via wc_order_stats.
		if ( ( $spent > 0 || $orders > 0 ) && class_exists( 'WooCommerce' ) ) {
			$lookup_table = $wpdb->prefix . 'wc_customer_lookup';
			$stats_table  = $wpdb->prefix . 'wc_order_stats';
			$passing_ids  = array();
			foreach ( array_chunk( $uid_ints, 1000 ) as $chunk ) {
				$id_list   = implode( ',', $chunk );
				$stats_sql = 'SELECT cl.user_id,'
					. ' COALESCE(SUM(os.total_sales), 0) as total_spent,'
					. ' COUNT(os.order_id) as order_count'
					. " FROM {$lookup_table} cl"
					. " LEFT JOIN {$stats_table} os ON os.customer_id = cl.customer_id"
					. " AND os.status NOT IN ('wc-cancelled','wc-refunded','wc-failed')"
					. " WHERE cl.user_id IN ({$id_list})"
					. ' GROUP BY cl.user_id';
				$rows      = WAWP()->db->execute_get_results( $stats_sql, ARRAY_A );
				if ( ! empty( $rows ) ) {
					foreach ( $rows as $row ) {
						$passes_spent  = ( 0 === $spent || floatval( $row['total_spent'] ) >= $spent );
						$passes_orders = ( 0 === $orders || intval( $row['order_count'] ) >= $orders );
						if ( $passes_spent && $passes_orders ) {
							$passing_ids[] = (int) $row['user_id'];
						}
					}
				}
			}
			$uid_ints = array_values( array_intersect( $uid_ints, $passing_ids ) );
		}

		return $uid_ints;
	}

	/**
	 * Filter by purchase and status.
	 *
	 * @param array $uids     User IDs.
	 * @param array $products Product IDs.
	 * @param array $statuses Order statuses.
	 * @return array Filtered IDs.
	 */
	private function filter_by_purchase_and_status( $uids, $products, $statuses ) {
		if ( ! class_exists( 'WooCommerce' ) || empty( $uids ) ) {
			return $uids;
		}

		global $wpdb;

		$uid_ints = array_map( 'intval', $uids );

		// Build status clause — values sanitized via sanitize_text_field then esc_sql.
		$status_sql = '';
		if ( ! empty( $statuses ) ) {
			$clean_statuses = array_map(
				function ( $s ) {
					return esc_sql( 'wc-' . ltrim( sanitize_text_field( $s ), 'wc-' ) );
				},
				$statuses
			);
			$status_sql     = " AND p.post_status IN ('" . implode( "','", $clean_statuses ) . "')";
		}

		// Chunk uids into batches of 1000 to avoid oversized IN clauses with 100k+ users.
		$uid_chunks      = array_chunk( $uid_ints, 1000 );
		$order_cache_key = 'wawp_orders_' . md5( implode( ',', $uid_ints ) . $status_sql );
		$order_rows      = wp_cache_get( $order_cache_key, 'wawp_campaigns' );
		if ( false === $order_rows ) {
			$order_rows = array();
			foreach ( $uid_chunks as $chunk ) {
				$uid_list   = implode( ',', $chunk );
				$orders_sql = 'SELECT p.ID as order_id, pm.meta_value as customer_id'
					. " FROM {$wpdb->posts} p"
					. " INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = '_customer_user'"
					. " WHERE p.post_type = 'shop_order'"
					. " AND pm.meta_value IN ({$uid_list})"
					. $status_sql;
				$chunk_rows = WAWP()->db->execute_get_results( $orders_sql, ARRAY_A );
				if ( ! empty( $chunk_rows ) ) {
					$order_rows = array_merge( $order_rows, $chunk_rows );
				}
			}
			wp_cache_set( $order_cache_key, $order_rows, 'wawp_campaigns', 5 * MINUTE_IN_SECONDS );
		}

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

		// Build map: customer_id => [order_ids].
		$customer_orders = array();
		foreach ( $order_rows as $row ) {
			$customer_orders[ (int) $row['customer_id'] ][] = (int) $row['order_id'];
		}

		if ( empty( $products ) ) {
			return array_keys( $customer_orders );
		}

		// Filter to customers who ordered specific products via order_items bulk query.
		$all_order_ids = array();
		foreach ( $customer_orders as $oids ) {
			$all_order_ids = array_merge( $all_order_ids, $oids );
		}
		$order_id_ints   = array_map( 'intval', $all_order_ids );
		$product_id_list = implode( ',', array_map( 'intval', $products ) );

		// Chunk order IDs into batches of 1000 to avoid oversized IN clauses.
		$order_id_chunks    = array_chunk( $order_id_ints, 1000 );
		$items_cache_key    = 'wawp_items_' . md5( implode( ',', $order_id_ints ) . $product_id_list );
		$matching_order_ids = wp_cache_get( $items_cache_key, 'wawp_campaigns' );
		if ( false === $matching_order_ids ) {
			$matching_order_ids = array();
			foreach ( $order_id_chunks as $chunk ) {
				$order_id_list = implode( ',', $chunk );
				$items_sql     = 'SELECT DISTINCT oi.order_id'
					. " FROM {$wpdb->prefix}woocommerce_order_items oi"
					. " INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta oim"
					. " ON oim.order_item_id = oi.order_item_id AND oim.meta_key = '_product_id'"
					. " WHERE oi.order_item_type = 'line_item'"
					. " AND oi.order_id IN ({$order_id_list})"
					. " AND oim.meta_value IN ({$product_id_list})";
				$chunk_ids     = WAWP()->db->execute_get_col( $items_sql );
				if ( ! empty( $chunk_ids ) ) {
					$matching_order_ids = array_merge( $matching_order_ids, $chunk_ids );
				}
			}
			$matching_order_ids = array_unique( $matching_order_ids );
			wp_cache_set( $items_cache_key, $matching_order_ids, 'wawp_campaigns', 5 * MINUTE_IN_SECONDS );
		}

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

		$matching_set = array_flip( array_map( 'intval', $matching_order_ids ) );
		$result       = array();
		foreach ( $customer_orders as $cid => $oids ) {
			foreach ( $oids as $oid ) {
				if ( isset( $matching_set[ $oid ] ) ) {
					$result[] = $cid;
					break;
				}
			}
		}

		return array_unique( $result );
	}

	/**
	 * Find phone number for a user.
	 *
	 * @param int $uid User ID.
	 * @return string Phone number.
	 */
	private function find_phone( $uid ) {
		$ph = WAWP()->user_manager->get_user_phone( $uid );
		if ( ! $ph ) {
			$ph = get_user_meta( $uid, 'phone', true );
		}
		if ( ! $ph ) {
			$usr = get_userdata( $uid );
			$ph  = $usr->user_login ?? '';
		}
		list( $digits, ) = self::format_phone_number( $ph );

		return $digits;
	}

	/**
	 * Parse placeholders in a message.
	 *
	 * @param string $msg Message.
	 * @param int    $uid User ID.
	 * @return string Parsed message.
	 */
	private function parse_placeholders( $msg, $uid ) {
		if ( class_exists( 'WAWP_Message_Parser' ) ) {
			return WAWP_Message_Parser::parse_message_placeholders( $msg, array(), null, $uid );
		}

		return $msg;
	}

	/**
	 * Generate a random code.
	 *
	 * @param int $len Length.
	 * @return string Random code.
	 */
	private function rand_code( $len = 10 ) {
		return wp_generate_password( $len, false );
	}

	/**
	 * Cron job to send campaign messages.
	 *
	 * @param int|null $specific_cid Specific campaign ID.
	 */
	public function cron_send( $specific_cid = null ) {
		if ( ! $this->is_segmentation_enabled() ) {
			return;
		}

		global $wpdb;

		// Process queue generation for any queueing campaigns first.
		$queueing_camps = $wpdb->get_results( $wpdb->prepare( "SELECT id FROM %i WHERE status = 'queueing'", $this->camp_table ) );
		if ( ! empty( $queueing_camps ) ) {
			foreach ( $queueing_camps as $qc ) {
				$this->generate_queue_chunk( (int) $qc->id );
			}
		}

		if ( function_exists( 'wc_set_time_limit' ) ) {
			wc_set_time_limit( 300 );
		} elseif ( ! ini_get( 'safe_mode' ) ) {
			set_time_limit( 300 );
		}

		$base_limit = 50;

		// Bug fix: if the configured interval is large (>10s), process only 1 item per cron tick
		// to avoid blocking the PHP process for minutes. The cron will be re-triggered automatically.
		if ( ! $specific_cid ) {
			$sample_camp = WAWP()->db->get_results_cached( $wpdb->prepare( "SELECT min_whatsapp_interval FROM %i WHERE status IN ('active','risky') AND (paused=0 OR paused IS NULL) LIMIT 1", $this->camp_table ), 'camp_sample_interval', 'wawp_campaigns', 30 );
			if ( ! empty( $sample_camp ) ) {
				$sample_min = (int) ( $sample_camp[0]['min_whatsapp_interval'] ?? 2 );
				if ( $sample_min > 10 ) {
					$base_limit = 1;
				}
			}
		}

		if ( $specific_cid ) {

			$campaigns = WAWP()->db->get_results_cached( $wpdb->prepare( "SELECT * FROM %i WHERE status IN ('active', 'risky') AND (paused = 0 OR paused IS NULL) AND id = %d", $this->camp_table, $specific_cid ), 'camp_process_single_' . $specific_cid, 'wawp_campaigns', 10 );
		} else {

			$campaigns = WAWP()->db->get_results_cached( $wpdb->prepare( "SELECT * FROM %i WHERE status IN ('active', 'risky') AND (paused = 0 OR paused IS NULL)", $this->camp_table ), 'camp_process_all', 'wawp_campaigns', 10 );
		}

		foreach ( $campaigns as &$camp ) {
			$camp     = (object) $camp;
			$limit    = $base_limit;
			$lock_key = 'wawp_camp_lock_' . $camp->id;
			if ( get_transient( $lock_key ) ) {
				continue;
			}
			set_transient( $lock_key, 1, 240 );

			// Enforce daily send limits before fetching queue.
			$max_wa_day    = (int) ( $camp->max_wa_per_day ?? 0 );
			$max_email_day = (int) ( $camp->max_email_per_day ?? 0 );
			if ( $max_wa_day > 0 || $max_email_day > 0 ) {
				$today_start     = wp_date( 'Y-m-d 00:00:00' );
				$tomorrow_start  = wp_date( 'Y-m-d 00:00:00', strtotime( 'tomorrow' ) );
				$wa_limit_hit    = false;
				$email_limit_hit = false;
				if ( $max_wa_day > 0 ) {
					$sent_wa_today = (int) call_user_func_array( array( $wpdb, 'get_var' ), array( $wpdb->prepare( "SELECT COUNT(*) FROM %i WHERE campaign_id = %d AND channel IN ('whatsapp','meta') AND status = 'sent' AND sent_at >= %s", $this->queue_table, $camp->id, $today_start ) ) );
					if ( $sent_wa_today >= $max_wa_day ) {
						$wa_limit_hit = true;
					} else {
						$limit = min( $limit, $max_wa_day - $sent_wa_today );
					}
				}
				if ( $max_email_day > 0 ) {
					$sent_email_today = (int) call_user_func_array( array( $wpdb, 'get_var' ), array( $wpdb->prepare( "SELECT COUNT(*) FROM %i WHERE campaign_id = %d AND channel = 'email' AND status = 'sent' AND sent_at >= %s", $this->queue_table, $camp->id, $today_start ) ) );
					if ( $sent_email_today >= $max_email_day ) {
						$email_limit_hit = true;
					} else {
						$limit = min( $limit, $max_email_day - $sent_email_today );
					}
				}
				$wa_blocked           = ( $max_wa_day > 0 && $wa_limit_hit );
				$email_blocked        = ( $max_email_day > 0 && $email_limit_hit );
				$all_channels_limited = (
					( 0 === $max_wa_day || $wa_blocked ) &&
					( 0 === $max_email_day || $email_blocked )
				) && ( $wa_blocked || $email_blocked );
				if ( $all_channels_limited ) {
					$wpdb->update( $this->camp_table, array( 'next_run' => $tomorrow_start ), array( 'id' => $camp->id ) );
					wp_cache_delete( 'wawp_campaign_data_' . $camp->id, 'wawp_campaigns' );
					delete_transient( $lock_key );
					continue;
				}
			}

			$queue_items = WAWP()->db->get_results_cached( $wpdb->prepare( "SELECT * FROM %i WHERE campaign_id = %d AND status = 'pending' AND retry_count < 3 ORDER BY retry_count ASC, id ASC LIMIT %d", $this->queue_table, $camp->id, $limit ), 'camp_queue_' . $camp->id, 'wawp_campaigns', 5 );

			if ( empty( $queue_items ) ) {
				$this->check_campaign_completion( $camp->id );
				delete_transient( $lock_key );
				continue;
			}

			foreach ( $queue_items as $index => &$item ) {
				$item = (object) $item;
				if ( $index > 0 ) {
					if ( 'whatsapp' === $item->channel || 'meta' === $item->channel ) {
						$min   = (int) ( $camp->min_whatsapp_interval ? $camp->min_whatsapp_interval : 2 );
						$max   = (int) ( $camp->max_whatsapp_interval ? $camp->max_whatsapp_interval : 5 );
						$sleep = wp_rand( $min, $max );
						if ( $sleep > 0 ) {
							sleep( $sleep );
						}
					} elseif ( 'email' === $item->channel ) {
						$min   = (int) ( $camp->min_email_interval ? $camp->min_email_interval : 5 );
						$max   = (int) ( $camp->max_email_interval ? $camp->max_email_interval : 10 );
						$sleep = wp_rand( $min, $max );
						if ( $sleep > 0 ) {
							sleep( $sleep );
						}
					}
				}

				$current_status = call_user_func_array( array( $wpdb, 'get_var' ), array( $wpdb->prepare( 'SELECT status FROM %i WHERE id = %d AND (paused = 0 OR paused IS NULL)', $this->camp_table, $camp->id ) ) );
				if ( ! in_array( $current_status, array( 'active', 'risky' ), true ) ) {
					break;
				}

				$resolved = $this->resolve_campaign_content( $camp, $item->channel );

				$send_args = array(
					'channel'      => $item->channel,
					'message'      => $resolved['message'],
					'media_url'    => $resolved['media_url'],
					'subject'      => $camp->email_subject,
					'user_id'      => $item->user_id,
					/* translators: %s: Campaign name */
					'context'      => sprintf( esc_html__( 'Campaign: %s', 'automation-web-platform' ), $camp->name ),
					'replacements' => array(),
					'send_timing'  => 'instant',
					'skip_log'     => true,
				);

				if ( 'meta' === $item->channel ) {
					$send_args['meta_message_type'] = $camp->meta_message_type;
					$send_args['template_name']     = $camp->meta_template_name;
					$send_args['template_lang']     = $camp->meta_template_lang;
					$send_args['message']           = $resolved['message'];
				}

				if ( 'email' === $item->channel ) {
					$send_args['message'] = $resolved['message'];
				}

				$result = wawp_messenger()->send( $item->phone, $send_args );

				$is_email_item = ( 'email' === $item->channel );
				$log_phone     = $is_email_item ? '' : $item->phone;
				$log_email     = $is_email_item ? $item->phone : '';

				$last_response = wawp_messenger()->last_response;

				if ( $result ) {
					WAWP()->db->update_row(
						'campaigns_queue',
						array(
							'status'      => 'sent',
							'sent_at'     => current_time( 'mysql' ),
							'retry_count' => $item->retry_count + 1,
						),
						array( 'id' => $item->id )
					);
					WAWP()->db->increment_column( 'campaigns', 'processed_count', $camp->id );

					// Cache is automatically handled by the abstraction layer.

					if ( class_exists( 'WAWP_Unified_Log' ) ) {
						WAWP()->unified_log->add_activity_log(
							array(
								'user_id'    => $item->user_id ? $item->user_id : null,
								'phone'      => $log_phone,
								'email'      => $log_email,
								'event_type' => 'campaign',
								'gateway'    => $item->channel,
								'log_status' => 'success',
								'message'    => wp_trim_words( $this->parse_placeholders( $resolved['message'], $item->user_id ), 20, '...' ),
								'details'    => array(
									'campaign_id'   => $camp->id,
									'campaign_name' => $camp->name,
									'channel'       => $item->channel,
									'queue_id'      => $item->id,
									'response'      => $last_response,
								),
							)
						);
					}
				} else {
					$new_retry_count = $item->retry_count + 1;
					$error_msg       = 'Send failed via gateway';
					if ( is_array( $last_response ) ) {
						if ( ! empty( $last_response['message'] ) ) {
							$error_msg = $last_response['message'];
						} elseif ( ! empty( $last_response['error'] ) ) {
							$error_msg = $last_response['error'];
						}
					} elseif ( is_wp_error( $last_response ) ) {
						$error_msg = $last_response->get_error_message();
					} elseif ( is_string( $last_response ) && ! empty( $last_response ) ) {
						$error_msg = $last_response;
					}

					WAWP()->db->update_row(
						'campaigns_queue',
						array(
							'status'      => 'error',
							'retry_count' => $new_retry_count,
							'last_error'  => $error_msg,
						),
						array( 'id' => $item->id )
					);

					if ( class_exists( 'WAWP_Unified_Log' ) ) {
						WAWP()->unified_log->add_activity_log(
							array(
								'user_id'    => $item->user_id ? $item->user_id : null,
								'phone'      => $log_phone,
								'email'      => $log_email,
								'event_type' => 'campaign',
								'gateway'    => $item->channel,
								'log_status' => 'failed',
								'message'    => wp_trim_words( $this->parse_placeholders( $resolved['message'], $item->user_id ), 20, '...' ),
								'details'    => array(
									'campaign_id'   => $camp->id,
									'campaign_name' => $camp->name,
									'channel'       => $item->channel,
									'queue_id'      => $item->id,
									'retry'         => $new_retry_count,
									'error'         => $error_msg,
									'response'      => $last_response,
								),
							)
						);
					}
				}
			}

			delete_transient( $lock_key );
			wp_cache_delete( 'camp_queue_' . $camp->id, 'wawp_campaigns' );
			$this->check_campaign_completion( $camp->id );

			// Reschedule immediately for the next batch if there are still pending items for the current run.
			$check_camp = $wpdb->get_row( $wpdb->prepare( 'SELECT status, next_run FROM %i WHERE id = %d', $this->camp_table, $camp->id ) );
			if ( $check_camp && in_array( $check_camp->status, array( 'active', 'risky' ), true ) ) {
				$next_run_ts  = $check_camp->next_run ? strtotime( $check_camp->next_run ) : 0;
				$local_now_ts = strtotime( current_time( 'mysql' ) );
				if ( $next_run_ts <= $local_now_ts ) {
					$remaining_pending = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM %i WHERE campaign_id = %d AND status = 'pending' AND retry_count < 3", $this->queue_table, $camp->id ) );
					if ( $remaining_pending > 0 ) {
						$min_interval = 2;
						if ( 1 === (int) $limit ) {
							$min_interval = (int) ( $camp->min_whatsapp_interval ? $camp->min_whatsapp_interval : 2 );
						}
						$next_time = time() + $min_interval;
						if ( class_exists( 'WAWP_Task_Scheduler' ) ) {
							if ( ! WAWP_Task_Scheduler::next_scheduled( $this->cron_hook, array( (int) $camp->id ) ) ) {
								WAWP_Task_Scheduler::schedule_single( $next_time, $this->cron_hook, array( (int) $camp->id ) );
							}
						}
					}
				}
			}
		}
	}

	/**
	 * Check if a campaign is completed.
	 *
	 * @param int $cid Campaign ID.
	 */
	private function check_campaign_completion( $cid ) {
		global $wpdb;

		// Count pending items that can still be retried (retry_count < 3).
		// Items with retry_count >= 3 are exhausted and must not block completion.
		$remaining = call_user_func_array( array( $wpdb, 'get_var' ), array( $wpdb->prepare( "SELECT COUNT(*) FROM %i WHERE campaign_id = %d AND status = 'pending' AND retry_count < 3", $this->queue_table, $cid ) ) );

		// Also mark exhausted-pending items as 'failed' so they don't linger as pending.
		call_user_func_array( array( $wpdb, 'query' ), array( $wpdb->prepare( "UPDATE %i SET status = 'failed' WHERE campaign_id = %d AND status = 'pending' AND retry_count >= 3", $this->queue_table, $cid ) ) );

		if ( 0 === (int) $remaining ) {
			$camp_row    = WAWP()->db->execute_get_row( $wpdb->prepare( 'SELECT repeat_type, repeat_days FROM %i WHERE id = %d', $this->camp_table, $cid ) );
			$repeat_type = $camp_row ? $camp_row->repeat_type : 'no_repeat';
			$repeat_days = $camp_row ? max( 1, (int) $camp_row->repeat_days ) : 1;

			if ( ! empty( $repeat_type ) && 'no_repeat' !== $repeat_type ) {
				// Re-queue all sent items as pending for the next run.
				WAWP()->db->execute_query( $wpdb->prepare( "UPDATE %i SET status = 'pending', retry_count = 0, sent_at = NULL WHERE campaign_id = %d AND status = 'sent'", $this->queue_table, $cid ) );

				$interval_map = array(
					'daily'   => DAY_IN_SECONDS,
					'weekly'  => WEEK_IN_SECONDS,
					'monthly' => 30 * DAY_IN_SECONDS,
					'custom'  => $repeat_days * DAY_IN_SECONDS,
				);
				$interval     = isset( $interval_map[ $repeat_type ] ) ? $interval_map[ $repeat_type ] : DAY_IN_SECONDS;
				$next_run     = wp_date( 'Y-m-d H:i:s', strtotime( current_time( 'mysql' ) ) + $interval );

				WAWP()->db->execute_update(
					$this->camp_table,
					array(
						'status'          => 'active',
						'next_run'        => $next_run,
						'processed_count' => 0,
					),
					array( 'id' => $cid )
				);
			} else {
				WAWP()->db->execute_update(
					$this->camp_table,
					array(
						'status'   => 'completed',
						'next_run' => null,
					),
					array( 'id' => $cid )
				);
			}
		}
	}

	/**
	 * Handle email open tracking.
	 */
	public function handle_email_open_tracking() {
		$camp_id = filter_input( INPUT_GET, 'camp_id', FILTER_VALIDATE_INT ) ? filter_input( INPUT_GET, 'camp_id', FILTER_VALIDATE_INT ) : 0;
		$phone   = filter_input( INPUT_GET, 'to', FILTER_SANITIZE_SPECIAL_CHARS ) ? filter_input( INPUT_GET, 'to', FILTER_SANITIZE_SPECIAL_CHARS ) : '';

		if ( $camp_id && $phone ) {
			$this->log_action(
				'success',
				'Campaign Tracking - Email Open',
				array(
					'recipient' => $phone,
					'gateway'   => 'email',
					'order_id'  => $camp_id,
				),
				'Campaign Tracking'
			);
		}
		header( 'Content-Type: image/gif' );
		header( 'Content-Length: 43' );
		// 1x1 transparent GIF pixel — raw binary, escaping not applicable.
		$gif = pack( 'H*', '47494638396101000100800000000000ffffff21f90401000000002c000000000100010000020144003b' );

		$output_stream = call_user_func( 'fopen', 'php://output', 'w' );
		if ( $output_stream ) {
			call_user_func( 'fwrite', $output_stream, $gif );
			call_user_func( 'fclose', $output_stream );
		}

		exit;
	}

	/**
	 * Get React settings data.
	 *
	 * @return array Settings data.
	 */
	public function get_react_settings_data() {
		$data = $this->handle_get_campaigns_data();

		return array_merge(
			$data,
			array(
				'iconBaseUrl' => WAWP_PLUGIN_URL . 'assets/icons/',
				'nonces'      => array(
					'fetch'   => wp_create_nonce( 'wawp_get_campaigns_data_nonce' ),
					'calc'    => wp_create_nonce( 'camp_calc_recip_nonce' ),
					'preview' => wp_create_nonce( 'camp_preview_nonce' ),
					'rerun'   => wp_create_nonce( 'wawp_rerun_campaign_nonce' ),
					'delete'  => wp_create_nonce( 'wawp_bulk_delete_campaigns_nonce' ),
					'save'    => wp_create_nonce( 'wawp_save_campaign_nonce' ),
					'toggle'  => wp_create_nonce( 'wawp_toggle_campaign_nonce' ),
					'risky'   => wp_create_nonce( 'camp_run_risky_nonce' ),
				),
				'i18n'        => array(
					'title'                                => esc_html__( 'Bulk Campaigns', 'automation-web-platform' ),
					'subtitle'                             => esc_html__( 'Send high-impact bulk broadcasts and segmented campaigns.', 'automation-web-platform' ),
					'createCampaign'                       => esc_html__( 'Create New Campaign', 'automation-web-platform' ),
					'deleteConfirm'                        => esc_html__( 'Are you sure you want to delete these campaigns?', 'automation-web-platform' ),
					'campaigns_snapshot_title'             => esc_html__( 'Campaigns Performance Snapshot', 'automation-web-platform' ),
					'campaigns_snapshot_desc'              => esc_html__( 'Real-time breakdown of all active and historical campaign metrics.', 'automation-web-platform' ),
					'stat_total'                           => esc_html__( 'Total', 'automation-web-platform' ),
					'stat_total_desc'                      => esc_html__( 'Lifetime campaigns', 'automation-web-platform' ),
					'stat_active'                          => esc_html__( 'Active', 'automation-web-platform' ),
					'stat_active_desc'                     => esc_html__( 'Currently running', 'automation-web-platform' ),
					'stat_processed'                       => esc_html__( 'Processed', 'automation-web-platform' ),
					'stat_processed_desc'                  => esc_html__( 'Messages delivered', 'automation-web-platform' ),
					'stat_failed'                          => esc_html__( 'Failed', 'automation-web-platform' ),
					'stat_failed_desc'                     => esc_html__( 'Delivery errors', 'automation-web-platform' ),
					'workspace_title'                      => esc_html__( 'Campaign Workspace', 'automation-web-platform' ),
					'workspace_desc'                       => esc_html__( 'Manage, monitor and optimize your marketing campaigns.', 'automation-web-platform' ),
					'add_new_btn'                          => esc_html__( 'Add New', 'automation-web-platform' ),
					'search_placeholder'                   => esc_html__( 'Search campaigns...', 'automation-web-platform' ),
					'no_campaigns_found'                   => esc_html__( 'No campaigns found', 'automation-web-platform' ),
					'created_label'                        => esc_html__( 'Created', 'automation-web-platform' ),
					'media_type'                           => esc_html__( 'Media', 'automation-web-platform' ),
					'product_type'                         => esc_html__( 'Product', 'automation-web-platform' ),
					'post_type'                            => esc_html__( 'Post', 'automation-web-platform' ),
					'text_type'                            => esc_html__( 'Text', 'automation-web-platform' ),
					'progress_total'                       => esc_html__( 'Total', 'automation-web-platform' ),
					'channels_whatsapp_title'              => esc_html__( 'WhatsApp Web', 'automation-web-platform' ),
					'channels_meta_title'                  => esc_html__( 'Meta API', 'automation-web-platform' ),
					'channels_email_title'                 => esc_html__( 'Email', 'automation-web-platform' ),
					'frequency_onetime'                    => esc_html__( 'One-Time', 'automation-web-platform' ),
					'starts_label'                         => esc_html__( 'Starts', 'automation-web-platform' ),
					'next_run_label'                       => esc_html__( 'Next Run', 'automation-web-platform' ),
					'edit_action'                          => esc_html__( 'Edit', 'automation-web-platform' ),
					'approve_run_action'                   => esc_html__( 'Approve Run', 'automation-web-platform' ),
					'resume_action'                        => esc_html__( 'Resume', 'automation-web-platform' ),
					'pause_action'                         => esc_html__( 'Pause', 'automation-web-platform' ),
					'rerun_action'                         => esc_html__( 'Re-run', 'automation-web-platform' ),
					'delete_action'                        => esc_html__( 'Delete', 'automation-web-platform' ),
					'badge_paused'                         => esc_html__( 'Paused', 'automation-web-platform' ),
					'badge_running'                        => esc_html__( 'Running', 'automation-web-platform' ),
					'badge_completed'                      => esc_html__( 'Completed', 'automation-web-platform' ),
					'badge_risky'                          => esc_html__( 'Risky', 'automation-web-platform' ),
					'badge_setup'                          => esc_html__( 'Setup', 'automation-web-platform' ),
					'badge_all'                            => esc_html__( 'All', 'automation-web-platform' ),
					'badge_done'                           => esc_html__( 'Done', 'automation-web-platform' ),
					'badge_error'                          => esc_html__( 'Error', 'automation-web-platform' ),
					'confirm_approve_run'                  => esc_html__( 'Are you sure you want to approve this high-volume run?', 'automation-web-platform' ),
					'confirm_delete_camps'                 => esc_html__( 'Are you sure you want to delete selected campaign(s)?', 'automation-web-platform' ),
					'step_channels'                        => esc_html__( 'Channels', 'automation-web-platform' ),
					'step_audience'                        => esc_html__( 'Audience', 'automation-web-platform' ),
					'step_woocommerce'                     => esc_html__( 'WooCommerce', 'automation-web-platform' ),
					'step_content'                         => esc_html__( 'Content', 'automation-web-platform' ),
					'step_scheduling'                      => esc_html__( 'Scheduling', 'automation-web-platform' ),
					'step_review'                          => esc_html__( 'Review', 'automation-web-platform' ),
					'edit_campaign_title'                  => esc_html__( 'Edit Campaign', 'automation-web-platform' ),
					'create_campaign_title'                => esc_html__( 'Create New Bulk Campaign', 'automation-web-platform' ),
					'create_campaign_desc'                 => esc_html__( 'Target your audience with personalized messages', 'automation-web-platform' ),
					'calculating_recipients'               => esc_html__( 'Calculating...', 'automation-web-platform' ),
					'recipients_count'                     => esc_html__( 'Recipients', 'automation-web-platform' ),
					'campaign_name_label'                  => esc_html__( 'Campaign Name', 'automation-web-platform' ),
					'campaign_name_placeholder'            => esc_html__( 'E.g. Ramadan Sale 2024', 'automation-web-platform' ),
					'dispatch_channels_label'              => esc_html__( 'Dispatch Channels Selection', 'automation-web-platform' ),
					'whatsapp_web_label'                   => esc_html__( 'Whatsapp Web', 'automation-web-platform' ),
					'whatsapp_web_desc'                    => esc_html__( 'Direct Wawp engine', 'automation-web-platform' ),
					'meta_whatsapp_label'                  => esc_html__( 'Meta WhatsApp', 'automation-web-platform' ),
					'meta_whatsapp_desc'                   => esc_html__( 'Business API approved', 'automation-web-platform' ),
					'email_dispatch_label'                 => esc_html__( 'Email Dispatch', 'automation-web-platform' ),
					'email_dispatch_desc'                  => esc_html__( 'Secure SMTP delivery', 'automation-web-platform' ),
					'whatsapp_instances_label'             => esc_html__( 'WhatsApp Instances', 'automation-web-platform' ),
					'select_instances_placeholder'         => esc_html__( 'Select WhatsApp Instances...', 'automation-web-platform' ),
					'target_roles_label'                   => esc_html__( 'Target User Roles', 'automation-web-platform' ),
					'select_roles_placeholder'             => esc_html__( 'Select roles...', 'automation-web-platform' ),
					'target_specific_users_label'          => esc_html__( 'Target Specific Users', 'automation-web-platform' ),
					'search_users_placeholder'             => esc_html__( 'Search specific users...', 'automation-web-platform' ),
					'external_numbers_label'               => esc_html__( 'External Numbers (WhatsApp)', 'automation-web-platform' ),
					'one_number_per_line'                  => esc_html__( 'Enter one number per line.', 'automation-web-platform' ),
					'external_emails_label'                => esc_html__( 'External Emails', 'automation-web-platform' ),
					'one_email_per_line'                   => esc_html__( 'Enter one email per line.', 'automation-web-platform' ),
					'only_verified_phone_label'            => esc_html__( 'Only Verified Phone Numbers?', 'automation-web-platform' ),
					'only_verified_phone_desc'             => esc_html__( 'Only target users marked as verified in Wawp.', 'automation-web-platform' ),
					'billing_country_label'                => esc_html__( 'Billing Country', 'automation-web-platform' ),
					'billing_country_desc'                 => esc_html__( 'Filter by user billing country.', 'automation-web-platform' ),
					'select_countries_placeholder'         => esc_html__( 'Select countries...', 'automation-web-platform' ),
					'wp_profile_language_label'            => esc_html__( 'WordPress Profile Language', 'automation-web-platform' ),
					'wp_profile_language_desc'             => esc_html__( 'Filter by user\'s profile language.', 'automation-web-platform' ),
					'select_languages_placeholder'         => esc_html__( 'Select languages...', 'automation-web-platform' ),
					'min_total_spent_label'                => esc_html__( 'Min. Total Spent', 'automation-web-platform' ),
					'min_order_count_label'                => esc_html__( 'Min. Order Count', 'automation-web-platform' ),
					'purchased_products_label'             => esc_html__( 'Purchased Specific Products', 'automation-web-platform' ),
					'search_specific_products_placeholder' => esc_html__( 'Search specific products...', 'automation-web-platform' ),
					'order_statuses_filters_label'         => esc_html__( 'Order Statuses Filters', 'automation-web-platform' ),
					'message_type_strategy_title'          => __( 'Message Type Strategy', 'automation-web-platform' ),
					'message_type_strategy_desc'           => __( '"Text with Media" is optimized for WhatsApp. For Email, the media is beautifully embedded directly within the HTML message body. Sharing a post or product automatically formats and appends the content to all your active channels.', 'automation-web-platform' ),
					'wizard_tip_badge'                     => __( 'Smart Tip', 'automation-web-platform' ),
					'save_as_draft_btn'                    => __( 'Save as Draft', 'automation-web-platform' ),
					'untitled_campaign_draft'              => __( 'Untitled Campaign Draft', 'automation-web-platform' ),
					'message_type_label'                   => esc_html__( 'Message Type', 'automation-web-platform' ),
					'select_message_type_placeholder'      => esc_html__( 'Select Message Type...', 'automation-web-platform' ),
					'search_message_type_placeholder'      => esc_html__( 'Search message types...', 'automation-web-platform' ),
					'text_only_opt'                        => esc_html__( 'Text Only', 'automation-web-platform' ),
					'text_media_opt'                       => esc_html__( 'Text with Media (WhatsApp)', 'automation-web-platform' ),
					'share_post_opt'                       => esc_html__( 'Share WordPress Post/Page', 'automation-web-platform' ),
					'share_product_opt'                    => esc_html__( 'Share WooCommerce Product', 'automation-web-platform' ),
					'meta_templates_desc'                  => esc_html__( 'Templates are used only for business-initiated conversations on Meta API.', 'automation-web-platform' ),
					'personalize_btn'                      => esc_html__( 'Personalize', 'automation-web-platform' ),
					'email_campaign_marketing_label'       => esc_html__( 'Email Campaign Marketing', 'automation-web-platform' ),
					'smtp_api_badge'                       => esc_html__( 'SMTP / API', 'automation-web-platform' ),
					'email_subject_label'                  => esc_html__( 'Email Subject', 'automation-web-platform' ),
					'email_subject_placeholder'            => esc_html__( 'Enter campaign subject line...', 'automation-web-platform' ),
					'select_email_template_label'          => esc_html__( 'Select Email Template', 'automation-web-platform' ),
					'select_template_placeholder'          => esc_html__( 'Select a template...', 'automation-web-platform' ),
					'search_templates_placeholder'         => esc_html__( 'Search email templates...', 'automation-web-platform' ),
					'whatsapp_media_url_label'             => esc_html__( 'WhatsApp Media URL', 'automation-web-platform' ),
					'media_url_placeholder'                => esc_html__( 'https://your-site.com/image.jpg', 'automation-web-platform' ),
					'select_media_btn'                     => esc_html__( 'Select Media', 'automation-web-platform' ),
					'select_post_page_label'               => esc_html__( 'Select Post/Page', 'automation-web-platform' ),
					'choose_post_page_placeholder'         => esc_html__( 'Choose a post or page...', 'automation-web-platform' ),
					'search_posts_placeholder'             => esc_html__( 'Search posts/pages...', 'automation-web-platform' ),
					'append_post_info_label'               => esc_html__( 'Append post info to message text?', 'automation-web-platform' ),
					'select_product_label'                 => esc_html__( 'Select Product', 'automation-web-platform' ),
					'choose_product_placeholder'           => esc_html__( 'Choose a product...', 'automation-web-platform' ),
					'search_products_placeholder'          => esc_html__( 'Search products...', 'automation-web-platform' ),
					'append_product_info_label'            => esc_html__( 'Append product info to message text?', 'automation-web-platform' ),
					'info_title'                           => esc_html__( 'Title', 'automation-web-platform' ),
					'info_excerpt'                         => esc_html__( 'Excerpt', 'automation-web-platform' ),
					'info_price'                           => esc_html__( 'Price', 'automation-web-platform' ),
					'info_description'                     => esc_html__( 'Description', 'automation-web-platform' ),
					'info_link'                            => esc_html__( 'Link', 'automation-web-platform' ),
					'info_image'                           => esc_html__( 'Image', 'automation-web-platform' ),
					'max_interval_label'                   => esc_html__( 'Max Interval (s)', 'automation-web-platform' ),
					'min_interval_label'                   => esc_html__( 'Min Interval (s)', 'automation-web-platform' ),
					'daily_send_limit_label'               => esc_html__( 'Daily Send Limit', 'automation-web-platform' ),
					'whatsapp_scheduling_title'            => esc_html__( 'WhatsApp Scheduling', 'automation-web-platform' ),
					'email_scheduling_title'               => esc_html__( 'Email Scheduling', 'automation-web-platform' ),
					'common_scheduling_title'              => esc_html__( 'Common Scheduling', 'automation-web-platform' ),
					'schedule_start_label'                 => esc_html__( 'Schedule Start', 'automation-web-platform' ),
					'select_repeat_schedule_placeholder'   => esc_html__( 'Select Repeat Schedule...', 'automation-web-platform' ),
					'search_frequency_placeholder'         => esc_html__( 'Search frequency...', 'automation-web-platform' ),
					'no_repeat_opt'                        => esc_html__( 'No Repeat', 'automation-web-platform' ),
					'daily_opt'                            => esc_html__( 'Daily', 'automation-web-platform' ),
					'weekly_opt'                           => esc_html__( 'Weekly', 'automation-web-platform' ),
					'monthly_opt'                          => esc_html__( 'Monthly', 'automation-web-platform' ),
					'custom_interval_opt'                  => esc_html__( 'Custom Interval', 'automation-web-platform' ),
					'repeat_every_days_label'              => esc_html__( 'Repeat Every (Days)', 'automation-web-platform' ),
					'campaign_ready_title'                 => esc_html__( 'Campaign Ready!', 'automation-web-platform' ),
					'review_settings_desc'                 => esc_html__( 'Please review your settings before launching.', 'automation-web-platform' ),
					'review_audience_title'                => esc_html__( 'Audience', 'automation-web-platform' ),
					'review_roles_label'                   => esc_html__( 'Roles:', 'automation-web-platform' ),
					'review_users_label'                   => esc_html__( 'Users:', 'automation-web-platform' ),
					'review_recipients_label'              => esc_html__( 'Recipients:', 'automation-web-platform' ),
					'estimating_label'                     => esc_html__( 'Estimating...', 'automation-web-platform' ),
					'review_segmentation_title'            => esc_html__( 'Segmentation & Filters', 'automation-web-platform' ),
					'review_countries_label'               => esc_html__( 'Countries:', 'automation-web-platform' ),
					'review_languages_label'               => esc_html__( 'Languages:', 'automation-web-platform' ),
					'review_min_spent_label'               => esc_html__( 'Min. Total Spent:', 'automation-web-platform' ),
					'review_min_orders_label'              => esc_html__( 'Min. Order Count:', 'automation-web-platform' ),
					'review_products_label'                => esc_html__( 'Purchased Products:', 'automation-web-platform' ),
					'review_statuses_label'                => esc_html__( 'Filter Statuses:', 'automation-web-platform' ),
					'all_global_label'                     => esc_html__( 'ALL / Global', 'automation-web-platform' ),
					'any_amount_label'                     => esc_html__( 'Any Amount', 'automation-web-platform' ),
					'any_count_label'                      => esc_html__( 'Any Count', 'automation-web-platform' ),
					'review_content_title'                 => esc_html__( 'Content', 'automation-web-platform' ),
					'review_strategy_label'                => esc_html__( 'Strategy:', 'automation-web-platform' ),
					'standard_text_strategy'               => esc_html__( 'Standard Text', 'automation-web-platform' ),
					'share_post_strategy'                  => esc_html__( 'Share WP Post', 'automation-web-platform' ),
					'share_product_strategy'               => esc_html__( 'Share WooCommerce Product', 'automation-web-platform' ),
					'media_message_strategy'               => esc_html__( 'Media Message', 'automation-web-platform' ),
					'review_campaign_name_label'           => esc_html__( 'Campaign Name:', 'automation-web-platform' ),
					'review_shared_product_label'          => esc_html__( 'Shared Product:', 'automation-web-platform' ),
					'none_selected_label'                  => esc_html__( 'None selected', 'automation-web-platform' ),
					'review_shared_post_label'             => esc_html__( 'Shared Post:', 'automation-web-platform' ),
					'review_scheduling_title'              => esc_html__( 'Scheduling & Timing', 'automation-web-platform' ),
					'review_start_date_label'              => esc_html__( 'Start Date:', 'automation-web-platform' ),
					'asap_label'                           => esc_html__( 'ASAP', 'automation-web-platform' ),
					'review_interval_label'                => esc_html__( 'Interval:', 'automation-web-platform' ),
					'review_repeat_pattern_label'          => esc_html__( 'Repeat Pattern:', 'automation-web-platform' ),
					'back_btn'                             => esc_html__( 'Back', 'automation-web-platform' ),
					'cancel_btn'                           => esc_html__( 'Cancel', 'automation-web-platform' ),
					'launch_campaign_btn'                  => esc_html__( 'Launch Campaign', 'automation-web-platform' ),
					'next_step_btn'                        => esc_html__( 'Next Step', 'automation-web-platform' ),
					'id_label'                             => esc_html__( 'ID', 'automation-web-platform' ),
					'name_label'                           => esc_html__( 'Name', 'automation-web-platform' ),
					'type_label'                           => esc_html__( 'Type', 'automation-web-platform' ),
					'audience_label'                       => esc_html__( 'Audience', 'automation-web-platform' ),
					'progress_label'                       => esc_html__( 'Progress', 'automation-web-platform' ),
					'status_label'                         => esc_html__( 'Status', 'automation-web-platform' ),
					'channels_label'                       => esc_html__( 'Channels', 'automation-web-platform' ),
					'frequency_label'                      => esc_html__( 'Frequency', 'automation-web-platform' ),
				),
			)
		);
	}

	/**
	 * Dynamically resolve campaign content for post/product sharing.
	 *
	 * @param object $camp    Campaign database row.
	 * @param string $channel Queue item channel ('whatsapp', 'meta', 'email').
	 * @return array Array containing resolved 'message' and 'media_url'.
	 */
	public function resolve_campaign_content( $camp, $channel ) {
		$message   = $camp->message;
		$media_url = $camp->media_url;

		if ( 'email' === $channel ) {
			$message = $camp->email_message ? $camp->email_message : $camp->message;
		} elseif ( 'meta' === $channel ) {
			$message = $camp->meta_message_text ? $camp->meta_message_text : $camp->message;
		}

		$send_type = $camp->send_type;

		if ( 'wp_post' === $send_type && $camp->post_id ) {
			$post_id = (int) $camp->post_id;
			$post    = get_post( $post_id );
			if ( $post ) {
				$post_details = array();
				$is_email     = 'email' === $channel;

				if ( ! empty( $camp->post_include_title ) ) {
					$post_details[] = $is_email ? '<strong>' . esc_html( $post->post_title ) . '</strong>' : '*' . $post->post_title . '*';
				}
				if ( ! empty( $camp->post_include_excerpt ) ) {
					$excerpt = wp_strip_all_tags( $post->post_excerpt ? $post->post_excerpt : wp_trim_words( $post->post_content, 30 ) );
					if ( $excerpt ) {
						$post_details[] = $excerpt;
					}
				}
				if ( ! empty( $camp->post_include_link ) ) {
					$link           = get_permalink( $post_id );
					$post_details[] = $is_email ? '<a href="' . esc_url( $link ) . '">' . esc_html( $link ) . '</a>' : $link;
				}

				$post_text = implode( $is_email ? '<br/>' : "\n", $post_details );

				if ( ! empty( $camp->post_include_image ) ) {
					$image_id = get_post_thumbnail_id( $post_id );
					if ( $image_id ) {
						$resolved_img = wp_get_attachment_url( $image_id );
						if ( $resolved_img ) {
							$media_url = $resolved_img;
						}
					}
				}

				if ( ! empty( $camp->append_post ) ) {
					if ( $message ) {
						$message = $message . ( $is_email ? '<br/><br/>' : "\n\n" ) . $post_text;
					} else {
						$message = $post_text;
					}
				} else {
					$message = $post_text;
				}
			}
		} elseif ( 'woo_product' === $send_type && $camp->product_id && class_exists( 'WooCommerce' ) ) {
			$product_id = (int) $camp->product_id;
			$product    = wc_get_product( $product_id );
			if ( $product ) {
				$prod_details = array();
				$is_email     = 'email' === $channel;

				if ( ! empty( $camp->product_include_title ) ) {
					$prod_details[] = $is_email ? '<strong>' . esc_html( $product->get_name() ) . '</strong>' : '*' . $product->get_name() . '*';
				}
				if ( ! empty( $camp->product_include_price ) ) {
					$price = html_entity_decode( wp_strip_all_tags( wc_price( $product->get_price() ) ) );
					if ( $price ) {
						$prod_details[] = $price;
					}
				}
				if ( ! empty( $camp->product_include_excerpt ) ) {
					$desc = wp_strip_all_tags( $product->get_short_description() ? $product->get_short_description() : wp_trim_words( $product->get_description(), 30 ) );
					if ( $desc ) {
						$prod_details[] = $desc;
					}
				}
				if ( ! empty( $camp->product_include_link ) ) {
					$link           = get_permalink( $product_id );
					$prod_details[] = $is_email ? '<a href="' . esc_url( $link ) . '">' . esc_html( $link ) . '</a>' : $link;
				}

				$prod_text = implode( $is_email ? '<br/>' : "\n", $prod_details );

				if ( ! empty( $camp->product_include_image ) ) {
					$image_id = get_post_thumbnail_id( $product_id );
					if ( $image_id ) {
						$resolved_img = wp_get_attachment_url( $image_id );
						if ( $resolved_img ) {
							$media_url = $resolved_img;
						}
					}
				}

				if ( ! empty( $camp->append_product ) ) {
					if ( $message ) {
						$message = $message . ( $is_email ? '<br/><br/>' : "\n\n" ) . $prod_text;
					} else {
						$message = $prod_text;
					}
				} else {
					$message = $prod_text;
				}
			}
		}

		return array(
			'message'   => $message,
			'media_url' => $media_url,
		);
	}

	/**
	 * Generate a chunk of the queue in the background.
	 *
	 * @param int $cid Campaign ID.
	 */
	public function generate_queue_chunk( $cid ) {
		$state = get_option( 'wawp_camp_gen_queue_' . $cid );
		if ( ! $state ) {
			WAWP()->db->execute_update( $this->camp_table, array( 'status' => 'active' ), array( 'id' => $cid ) );
			return;
		}

		global $wpdb;

		$chunk_size = 500;
		$processed  = 0;

		$campaign = WAWP()->db->get_row_by_id( 'campaigns', $cid );
		if ( ! $campaign ) {
			delete_option( 'wawp_camp_gen_queue_' . $cid );
			return;
		}

		$send_whatsapp = (int) $campaign->send_whatsapp;
		$send_email    = (int) $campaign->send_email;
		$send_meta     = (int) $campaign->send_meta;
		$email_subject = $campaign->email_subject;
		$email_message = $campaign->email_message;

		$wa_total  = (int) ( $state['whatsapp_count'] ?? $this->get_audience_count( $cid, 'wa' ) );
		$ext_total = (int) ( $state['external_count'] ?? $this->get_audience_count( $cid, 'ext' ) );
		$em_total  = (int) ( $state['email_count'] ?? $this->get_audience_count( $cid, 'email' ) );

		// 1. WhatsApp uids — read from file, never from memory.
		if ( $send_whatsapp && $processed < $chunk_size && $state['whatsapp_offset'] < $wa_total ) {
			$uids = $this->read_audience_chunk( $cid, 'wa', $state['whatsapp_offset'], $chunk_size - $processed );
			if ( ! empty( $uids ) ) {
				$inserts = array();
				foreach ( $uids as $uid ) {
					$ph = $this->find_phone( (int) $uid );
					if ( $ph ) {
						$inserts[] = $wpdb->prepare(
							"(%d, 'whatsapp', %d, %s, %s, %s, 'pending')",
							$cid,
							(int) $uid,
							$ph,
							$this->rand_code( 12 ),
							$this->rand_code( 12 )
						);
					}
				}
				if ( ! empty( $inserts ) ) {
					// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
					$wpdb->query( "INSERT INTO {$this->queue_table} (campaign_id, channel, user_id, phone, unique_code, security_code, status) VALUES " . implode( ',', $inserts ) );
					// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
				}
				$processed                += count( $uids );
				$state['whatsapp_offset'] += count( $uids );
			}
		}

		// 2. External numbers — read from file.
		if ( $send_whatsapp && $processed < $chunk_size && $state['external_offset'] < $ext_total ) {
			$numbers = $this->read_audience_chunk( $cid, 'ext', $state['external_offset'], $chunk_size - $processed );
			if ( ! empty( $numbers ) ) {
				$inserts = array();
				foreach ( $numbers as $en ) {
					$inserts[] = $wpdb->prepare(
						"(%d, 'whatsapp', 0, %s, %s, %s, 'pending')",
						$cid,
						$en,
						$this->rand_code( 12 ),
						$this->rand_code( 12 )
					);
				}
				if ( ! empty( $inserts ) ) {
					// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
					$wpdb->query( "INSERT INTO {$this->queue_table} (campaign_id, channel, user_id, phone, unique_code, security_code, status) VALUES " . implode( ',', $inserts ) );
					// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
				}
				$processed                += count( $numbers );
				$state['external_offset'] += count( $numbers );
			}
		}

		// 3. Meta uids — same audience file as WhatsApp, different channel.
		if ( $send_meta && $processed < $chunk_size && $state['meta_offset'] < $wa_total ) {
			$uids = $this->read_audience_chunk( $cid, 'wa', $state['meta_offset'], $chunk_size - $processed );
			if ( ! empty( $uids ) ) {
				$inserts = array();
				foreach ( $uids as $uid ) {
					$ph = $this->find_phone( (int) $uid );
					if ( $ph ) {
						$inserts[] = $wpdb->prepare(
							"(%d, 'meta', %d, %s, %s, %s, 'pending')",
							$cid,
							(int) $uid,
							$ph,
							$this->rand_code( 12 ),
							$this->rand_code( 12 )
						);
					}
				}
				if ( ! empty( $inserts ) ) {
					// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
					$wpdb->query( "INSERT INTO {$this->queue_table} (campaign_id, channel, user_id, phone, unique_code, security_code, status) VALUES " . implode( ',', $inserts ) );
					// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
				}
				$processed            += count( $uids );
				$state['meta_offset'] += count( $uids );
			}
		}

		// 4. Email uids — read from file.
		if ( $send_email && $processed < $chunk_size && $state['email_offset'] < $em_total ) {
			$uids = $this->read_audience_chunk( $cid, 'email', $state['email_offset'], $chunk_size - $processed );
			if ( ! empty( $uids ) ) {
				$inserts = array();
				foreach ( $uids as $uid ) {
					$usr = get_userdata( (int) $uid );
					if ( $usr && $usr->user_email ) {
						$inserts[] = $wpdb->prepare(
							"(%d, 'email', %d, %s, %s, %s, 'pending', %s)",
							$cid,
							(int) $uid,
							$usr->user_email,
							$this->rand_code( 12 ),
							$this->rand_code( 12 ),
							wp_json_encode(
								array(
									'subject' => $email_subject,
									'body'    => $email_message,
								)
							)
						);
					}
				}
				if ( ! empty( $inserts ) ) {
					// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
					$wpdb->query( "INSERT INTO {$this->queue_table} (campaign_id, channel, user_id, phone, unique_code, security_code, status, details) VALUES " . implode( ',', $inserts ) );
					// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
				}
				$processed             += count( $uids );
				$state['email_offset'] += count( $uids );
			}
		}

		$whatsapp_done = ( ! $send_whatsapp || ( $state['whatsapp_offset'] >= $wa_total && $state['external_offset'] >= $ext_total ) );
		$meta_done     = ( ! $send_meta || $state['meta_offset'] >= $wa_total );
		$email_done    = ( ! $send_email || $state['email_offset'] >= $em_total );

		if ( $whatsapp_done && $meta_done && $email_done ) {
			WAWP()->db->execute_update(
				$this->camp_table,
				array(
					'status'   => $state['target_status'],
					'next_run' => $campaign->start_datetime ? $campaign->start_datetime : current_time( 'mysql' ),
				),
				array( 'id' => $cid )
			);
			delete_option( 'wawp_camp_gen_queue_' . $cid );
			$this->delete_audience_files( $cid );
			wp_cache_delete( 'wawp_campaign_data_' . $cid, 'wawp_campaigns' );
			wp_cache_delete( 'wawp_campaigns_data_list', 'wawp_campaigns' );
		} else {
			update_option( 'wawp_camp_gen_queue_' . $cid, $state, false );
		}
	}

	/**
	 * Search users dynamically.
	 *
	 * @param array $params Request parameters.
	 * @return array Searched users.
	 */
	public function handle_search_users( $params ) {
		$term = isset( $params['term'] ) ? sanitize_text_field( wp_unslash( $params['term'] ) ) : '';
		if ( empty( $term ) ) {
			return array();
		}

		$users_raw = get_users(
			array(
				'search'         => '*' . $term . '*',
				'search_columns' => array( 'ID', 'user_login', 'user_nicename', 'user_email', 'display_name' ),
				'number'         => 50,
				'fields'         => array( 'ID', 'display_name', 'user_email' ),
			)
		);

		return array_map(
			function ( $u ) {
				return array(
					'id'    => (string) $u->ID,
					'label' => "{$u->display_name} ({$u->user_email})",
				);
			},
			$users_raw
		);
	}

	/**
	 * Search products dynamically.
	 *
	 * @param array $params Request parameters.
	 * @return array Searched products.
	 */
	public function handle_search_products( $params ) {
		$term = isset( $params['term'] ) ? sanitize_text_field( wp_unslash( $params['term'] ) ) : '';
		if ( empty( $term ) ) {
			return array();
		}

		$prods_raw = get_posts(
			array(
				'post_type'   => 'product',
				's'           => $term,
				'numberposts' => 50,
				'post_status' => 'publish',
				'fields'      => 'ids',
			)
		);

		return array_map(
			function ( $pid ) {
				return array(
					'id'    => (string) $pid,
					'label' => get_the_title( $pid ),
				);
			},
			$prods_raw
		);
	}

	/**
	 * Search posts/pages dynamically.
	 *
	 * @param array $params Request parameters.
	 * @return array Searched posts.
	 */
	public function handle_search_posts( $params ) {
		$term = isset( $params['term'] ) ? sanitize_text_field( wp_unslash( $params['term'] ) ) : '';
		if ( empty( $term ) ) {
			return array();
		}

		$posts_raw = get_posts(
			array(
				'post_type'   => array( 'post', 'page' ),
				's'           => $term,
				'numberposts' => 50,
				'post_status' => 'publish',
				'fields'      => 'ids',
			)
		);

		return array_map(
			function ( $pid ) {
				return array(
					'id'    => (string) $pid,
					'label' => get_the_title( $pid ),
				);
			},
			$posts_raw
		);
	}
}
