<?php
/**
 * Astra Sites Analytics
 *
 * @since  4.4.27
 * @package Astra Sites
 */

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

if ( ! class_exists( 'Astra_Sites_Analytics' ) ) {

	/**
	 * Astra_Sites_Analytics
	 */
	class Astra_Sites_Analytics {

		/**
		 * Instance of Astra_Sites_Analytics
		 *
		 * @since  4.4.27
		 * @var self Astra_Sites_Analytics
		 */
		private static $instance = null;

		/**
		 * Analytics Events instance for tracking one-time milestone events.
		 *
		 * @since 4.5.2
		 * @var BSF_Analytics_Events
		 */
		private static $events;

		/**
		 * Instance of Astra_Sites_Analytics.
		 *
		 * @since  4.4.27
		 *
		 * @return self Class object.
		 */
		public static function get_instance() {
			if ( ! isset( self::$instance ) ) {
				self::$instance = new self();
			}

			return self::$instance;
		}

		/**
		 * Constructor.
		 *
		 * @since  4.4.27
		 */
		private function __construct() {
			if ( ! class_exists( 'BSF_Analytics_Events' ) ) {
				require_once ASTRA_SITES_DIR . 'admin/bsf-analytics/class-bsf-analytics-events.php';
			}
			self::$events = new BSF_Analytics_Events( 'astra_sites' );

			add_action( 'astra_sites_after_plugin_activation', array( $this, 'update_settings_after_plugin_activation' ), 10, 2 );
			add_action( 'admin_init', array( $this, 'maybe_update_finish_setup_banner_clicked' ) );
			add_action( 'wp_ajax_astra_sites_set_woopayments_analytics', array( $this, 'set_woopayments_analytics' ) );
			add_filter( 'bsf_core_stats', array( $this, 'add_astra_sites_analytics_data' ), 10, 1 );
			add_action( 'customize_save_after', array( $this, 'track_astra_customizer_update' ) );

			// Track import events for events_record.
			add_action( 'astra_sites_import_complete', array( $this, 'track_template_imported_event' ) );
			add_action( 'ast_block_templates_after_block_import', array( $this, 'track_block_pattern_imported_event' ), 10, 2 );
			add_action( 'ast_block_templates_after_kit_import', array( $this, 'track_kit_imported_event' ) );

			// Track onboarding step visits.
			add_action( 'astra_sites_onboarding_step_visited', array( $this, 'track_onboarding_step_visited_event' ) );

			// Track content edits for KPI records.
			add_action( 'save_post', array( $this, 'track_content_edited' ), 10, 2 );

			// Track setup/onboarding milestone events.
			add_action( 'getting_started_action_items_served', array( $this, 'track_finish_setup_initiated_event' ) );
			add_action( 'update_option_getting_started_action_items', array( $this, 'track_finish_setup_course_progress_event' ), 10, 2 );
			add_action( 'getting_started_setup_wizard_dismissed', array( $this, 'track_finish_setup_dismissed_event' ) );

			// Track WooPayments banner dismissal.
			add_action( 'astra_sites_woopayments_banner_dismissed', array( $this, 'track_woopayments_banner_dismissed_event' ) );

			// Track license activation/deactivation events.
			add_action( 'bsf_activate_license_astra-pro-sites_after_success', array( $this, 'track_license_activated_event' ) );
			add_action( 'bsf_deactivate_license_astra-pro-sites_after_success', array( $this, 'track_license_deactivated_event' ) );

			// Track plugin version updates (hooked before update class overwrites the version option).
			add_action( 'astra_sites_update_before', array( $this, 'track_plugin_updated_event' ) );
		}

		/**
		 * Update settings after plugin activation.
		 *
		 * @param string $plugin_init The plugin initialization path.
		 * @param array  $data        Additional data (optional).
		 *
		 * @since 4.4.27
		 * @return void
		 */
		public function update_settings_after_plugin_activation( $plugin_init, $data = array() ) {
			// Bail if the plugin slug is not set or empty.
			if ( ! isset( $data['plugin_slug'] ) || '' === $data['plugin_slug'] ) {
				return;
			}

			// Set WooPayments related settings.
			$this->maybe_woopayments_included( $plugin_init, $data );

			$plugin_slug      = $data['plugin_slug'];
			$required_plugins = Astra_Sites_Page::get_instance()->get_setting( 'required_plugins', array() );

			// If the plugin is already activated by starter templates, return early.
			if ( ( isset( $required_plugins[ $plugin_slug ] ) && 'activated' === $required_plugins[ $plugin_slug ] ) ) {
				return;
			}

			// If required plugins is not an array, initialize it.
			if ( ! is_array( $required_plugins ) ) {
				$required_plugins = array();
			}

			// Set the plugin activation status and update in settings.
			$required_plugins[ $plugin_slug ] = isset( $data['was_plugin_active'] ) && $data['was_plugin_active'] ? 'was_active' : 'activated';
			Astra_Sites_Page::get_instance()->update_settings(
				array(
					'required_plugins' => $required_plugins,
				)
			);
		}

		/**
		 * Maybe update finish setup banner clicked.
		 *
		 * @since 4.4.37
		 * @return void
		 */
		public function maybe_update_finish_setup_banner_clicked() {
			// Only allow administrators to update this setting.
			if ( ! current_user_can( 'manage_options' ) ) {
				return;
			}

			// Bail early if the source is not dashboard-banner.
			// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verification not needed here.
			if ( ! isset( $_GET['source'] ) || 'dashboard-banner' !== sanitize_text_field( $_GET['source'] ) ) {
				return;
			}

			// Update the finish setup banner clicked setting.
			Astra_Sites_Page::get_instance()->update_settings(
				array(
					'fs_banner_clicked' => 'yes',
				)
			);

			// Track as a one-time event.
			self::$events->track( 'finish_setup_banner_clicked', ASTRA_SITES_VER );
		}

		/**
		 * Check if WooCommerce Payments plugin is included and update settings accordingly.
		 * 
		 * @param string $plugin_init The plugin initialization path.
		 * @param array  $data Additional data (optional).
		 *
		 * @since 4.4.23
		 * @return void
		 */
		public function maybe_woopayments_included( $plugin_init, $data = array() ) {
			if ( 'woocommerce-payments/woocommerce-payments.php' === $plugin_init ) {
				// Prevent showing the banner if plugin was already active.
				if ( ! isset( $data['was_plugin_active'] ) || ! $data['was_plugin_active'] ) {
					Astra_Sites_Page::get_instance()->update_settings(
						array(
							'woopayments_ref'     => true,
							'wcpay_referred_time' => time(),
						)
					);
				}

				Astra_Sites_Page::get_instance()->update_settings(
					array(
						'woopayments_included' => true,
					)
				);
			}
		}

		/**
		 * Set WooPayments analytics.
		 *
		 * @since 4.4.23
		 * @return void
		 */
		public function set_woopayments_analytics() {
			// Verify user has permission to modify settings.
			if ( ! current_user_can( 'manage_options' ) ) {
				wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'astra-sites' ) ) );
				exit;
			}

			// Verify nonce.
			if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'woopayments_nonce' ) ) {
				wp_send_json_error( array( 'message' => __( 'Invalid nonce', 'astra-sites' ) ) );
				exit;
			}

			$source = isset( $_POST['source'] ) ? sanitize_text_field( wp_unslash( $_POST['source'] ) ) : '';
			if ( ! in_array( $source, array( 'banner', 'onboarding' ), true ) ) {
				wp_send_json_error( array( 'message' => __( 'Invalid source', 'astra-sites' ) ) );
				exit;
			}

			$key = "woopayments_{$source}_clicked";
			Astra_Sites_Page::get_instance()->update_settings( array( $key => true ) );

			// Track as a one-time event.
			self::$events->track(
				$key,
				ASTRA_SITES_VER,
				array( 'source' => $source )
			);

			wp_send_json_success( array( 'message' => 'WooPayments analytics updated!' ) );
			exit;
		}

		/**
		 * Track when a full template import completes.
		 *
		 * @param array $demo_data Demo data from the import.
		 *
		 * @since 4.4.51
		 * @return void
		 */
		public function track_template_imported_event( $demo_data = array() ) {
			$required_plugins = Astra_Sites_Page::get_instance()->get_setting( 'required_plugins', array() );

			$properties = array(
				'template_id'  => ! empty( $demo_data['id'] ) ? $demo_data['id'] : '',
				'template_url' => ! empty( $demo_data['astra-site-url'] ) ? $demo_data['astra-site-url'] : '',
				'title'        => ! empty( $demo_data['title']['rendered'] ) ? sanitize_text_field( $demo_data['title']['rendered'] ) : '',
				'site_type'    => ! empty( $demo_data['astra-site-type'] ) ? $demo_data['astra-site-type'] : '',
				'page_builder' => Astra_Sites_Page::get_instance()->get_setting( 'page_builder' ),
				'plugins'      => is_array( $required_plugins ) && ! empty( $required_plugins ) ? $required_plugins : array(),
			);

			self::$events->track(
				'template_imported',
				ASTRA_SITES_VER,
				$properties
			);

			// Store timestamp for KPI records.
			$this->store_kpi_timestamp( 'template' );
		}

		/**
		 * Track when a block pattern is imported.
		 *
		 * @param int    $block_id The ID of the imported block.
		 * @param string $type     The type of pattern (e.g., 'block', 'page').
		 *
		 * @since 4.4.51
		 * @return void
		 */
		public function track_block_pattern_imported_event( $block_id, $type ) {
			$properties = array(
				'block_id' => $block_id,
				'type'     => $type,
			);

			self::$events->track(
				'block_pattern_imported',
				ASTRA_SITES_VER,
				$properties
			);

			// Store timestamp for KPI records — $type is 'block' or 'page'.
			$this->store_kpi_timestamp( $type );
		}

		/**
		 * Track when a template kit is imported.
		 *
		 * @since 4.4.51
		 * @return void
		 */
		public function track_kit_imported_event() {
			self::$events->track(
				'kit_imported',
				ASTRA_SITES_VER
			);

			// Store timestamp for KPI records.
			$this->store_kpi_timestamp( 'kit' );
		}

		/**
		 * Track cumulative onboarding steps visited state.
		 *
		 * Reads all visited steps from settings and retracks with full state.
		 * Uses track() with $force=true so the server always has the latest cumulative snapshot.
		 *
		 * @param string $step The step identifier (e.g. 'welcome', 'template-listing').
		 *
		 * @since 4.5.2
		 * @return void
		 */
		public function track_onboarding_step_visited_event( $step ) {
			// Get all visited steps from settings (cumulative state).
			$steps_visited = Astra_Sites_Page::get_instance()->get_setting( 'steps_visited' );
			$steps_visited = is_array( $steps_visited ) ? $steps_visited : array();

			// Build properties map: each step as key with its status (yes/skipped).
			$properties = array();
			foreach ( $steps_visited as $visited_step => $status ) {
				$properties[ sanitize_key( $visited_step ) ] = $status;
			}

			// Replace any existing pending/pushed entry with updated state.
			self::$events->track(
				'onboarding_step_visited',
				ASTRA_SITES_VER,
				$properties,
				true
			);
		}

		/**
		 * Store a timestamp for KPI tracking.
		 *
		 * @param string $type The type of import ('template', 'block', 'page', 'kit', 'content_edited').
		 *
		 * @since 4.4.51
		 * @return void
		 */
		private function store_kpi_timestamp( $type ) {
			$timestamps = Astra_Sites_Page::get_instance()->get_setting( '_import_timestamps', array() );
			if ( ! is_array( $timestamps ) ) {
				$timestamps = array();
			}

			$bucket_map = array(
				'template'       => 'templates',
				'kit'            => 'kits',
				'content_edited' => 'content_edited',
			);
			$bucket     = isset( $bucket_map[ $type ] ) ? $bucket_map[ $type ] : 'block_patterns';

			if ( ! isset( $timestamps[ $bucket ] ) || ! is_array( $timestamps[ $bucket ] ) ) {
				$timestamps[ $bucket ] = array();
			}

			$timestamps[ $bucket ][] = time();
			Astra_Sites_Page::get_instance()->update_settings( array( '_import_timestamps' => $timestamps ) );
		}

		/**
		 * Track when a post, page, or CPT is added or edited.
		 *
		 * Stores a timestamp in the 'content_edited' bucket for KPI tracking.
		 * Skips auto-saves, revisions, and non-public post types.
		 *
		 * @param int      $post_id The post ID.
		 * @param \WP_Post $post    The post object.
		 *
		 * @since 4.4.51
		 * @return void
		 */
		public function track_content_edited( $post_id, $post ) {
			if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
				return;
			}

			if ( wp_is_post_revision( $post_id ) ) {
				return;
			}

			$post_type_obj = get_post_type_object( $post->post_type );
			if ( ! $post_type_obj || ! $post_type_obj->public ) {
				return;
			}

			$this->store_kpi_timestamp( 'content_edited' );
		}

		/**
		 * Track Astra Customizer updates after template import.
		 *
		 * This method tracks when users modify Astra Customizer settings after
		 * a successful template import. It only tracks once per user to avoid
		 * repeated logging on every save.
		 *
		 * @since 4.4.47
		 * @return void
		 */
		public function track_astra_customizer_update() {
			// Only track if import has been completed.
			$import_complete = get_option( 'astra_sites_import_complete', 'no' );
			if ( 'yes' !== $import_complete ) {
				return;
			}

			// Check if we've already tracked this (one-time tracking).
			$already_tracked = Astra_Sites_Page::get_instance()->get_setting( 'astra_customizer_updated', false );
			if ( $already_tracked ) {
				return;
			}

			// Get the current theme to ensure this is Astra-related.
			$current_theme = get_stylesheet();
			if ( 0 !== strpos( $current_theme, 'astra' ) ) {
				return;
			}

			// Mark as tracked to ensure one-time logging.
			Astra_Sites_Page::get_instance()->update_settings(
				array(
					'astra_customizer_updated' => true,
				)
			);
		}

		/**
		 * Track cumulative course progress state (done / not_done per course).
		 *
		 * Fires on `update_option_getting_started_action_items`. Compares old vs new
		 * to detect status changes, then retracks with full state of all courses.
		 * Uses track() with $force=true so the server always has the latest cumulative snapshot.
		 *
		 * @param mixed $old_value The old option value.
		 * @param mixed $new_value The new option value.
		 *
		 * @since 4.5.2
		 * @return void
		 */
		public function track_finish_setup_course_progress_event( $old_value, $new_value ) {
			if ( ! is_array( $new_value ) || empty( $new_value ) ) {
				return;
			}

			$old_value = is_array( $old_value ) ? $old_value : array();

			// Check if any course status actually changed.
			$has_change = false;
			foreach ( $new_value as $key => $action_item ) {
				$is_done  = ! empty( $action_item['status'] );
				$was_done = isset( $old_value[ $key ] ) && ! empty( $old_value[ $key ]['status'] );

				if ( $is_done !== $was_done ) {
					$has_change = true;
					break;
				}
			}

			if ( ! $has_change ) {
				return;
			}

			// Get full course list from default action items.
			if ( ! class_exists( '\GS\Classes\GS_Helper' ) ) {
				return;
			}

			$all_courses = \GS\Classes\GS_Helper::get_default_action_items();
			if ( ! is_array( $all_courses ) || empty( $all_courses ) ) {
				return;
			}

			// Build properties map from full course list, checking status in DB state.
			$properties   = array();
			$all_complete = true;
			foreach ( $all_courses as $course ) {
				$course_id = isset( $course['id'] ) ? $course['id'] : '';
				if ( empty( $course_id ) ) {
					continue;
				}
				$is_done                              = isset( $new_value[ $course_id ] ) && ! empty( $new_value[ $course_id ]['status'] );
				$properties[ sanitize_key( $course_id ) ] = $is_done ? 'completed' : 'pending';
				if ( ! $is_done ) {
					$all_complete = false;
				}
			}

			$event_value = $all_complete ? 'completed' : 'in_progress';

			// Replace any existing pending/pushed entry with updated state.
			self::$events->track(
				'finish_setup_course_progress',
				$event_value,
				$properties,
				true
			);
		}

		/**
		 * Track when the finish setup wizard is dismissed.
		 *
		 * @since 4.5.2
		 * @return void
		 */
		public function track_finish_setup_dismissed_event() {
			self::$events->track(
				'finish_setup_dismissed',
				ASTRA_SITES_VER
			);
		}

		/**
		 * Track when the WooPayments banner is dismissed.
		 * Non-unique so each dismissal is counted separately.
		 *
		 * @since 4.5.2
		 * @return void
		 */
		public function track_woopayments_banner_dismissed_event() {
			self::$events->track(
				'woopayments_banner_dismissed',
				ASTRA_SITES_VER,
				array(),
				false
			);
		}

		/**
		 * Track finish setup initiated when courses are first served to the user.
		 * Dedup ensures this fires only once even if the endpoint is called multiple times.
		 *
		 * @param array $action_items The action items being served.
		 *
		 * @since 4.5.2
		 * @return void
		 */
		public function track_finish_setup_initiated_event( $action_items ) {
			if ( empty( $action_items ) || ! is_array( $action_items ) ) {
				return;
			}

			$course_names = array();
			foreach ( $action_items as $action_item ) {
				if ( ! empty( $action_item['id'] ) ) {
					$course_names[] = sanitize_key( $action_item['id'] );
				}
			}

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

			self::$events->track(
				'finish_setup_initiated',
				ASTRA_SITES_VER,
				array(
					'courses' => $course_names,
				)
			);
		}

		/**
		 * Track when the product license is activated.
		 *
		 * @since 4.5.2
		 * @return void
		 */
		public function track_license_activated_event() {
			self::$events->track(
				'license_activated',
				ASTRA_SITES_VER
			);
		}

		/**
		 * Track when the product license is deactivated.
		 *
		 * @since 4.5.2
		 * @return void
		 */
		public function track_license_deactivated_event() {
			self::$events->track(
				'license_deactivated',
				ASTRA_SITES_VER
			);
		}

		/**
		 * Track plugin version updates.
		 *
		 * Fires on `astra_sites_update_before` — before the update class
		 * overwrites `astra-sites-auto-version` with the new version.
		 * Uses track() with $force=true so each version transition is tracked.
		 *
		 * @since 4.5.2
		 * @return void
		 */
		public function track_plugin_updated_event() {
			$old_version = get_option( 'astra-sites-auto-version', '' );
			if ( empty( $old_version ) || version_compare( $old_version, ASTRA_SITES_VER, '>=' ) ) {
				return;
			}

			self::$events->track(
				'plugin_updated',
				ASTRA_SITES_VER,
				array(
					'from_version' => $old_version,
				),
				true
			);
		}

		/**
		 * Detect state-based milestone events from already-computed stats.
		 * Dedup in BSF_Analytics_Events::track() ensures each fires only once.
		 *
		 * @since 4.5.2
		 * @return void
		 */
		private function detect_state_events() {
			// plugin_activated: track once with install source and time-to-value.
			$referer_key   = defined( 'BSF_UTM_ANALYTICS_REFERER' ) ? BSF_UTM_ANALYTICS_REFERER : 'bsf_product_referers';
			$bsf_referrers = get_option( $referer_key, array() );
			$source        = ! empty( $bsf_referrers['astra-sites'] ) ? $bsf_referrers['astra-sites'] : 'self';
			$is_pro        = defined( 'ASTRA_PRO_SITES_NAME' );

			$install_time      = get_option( 'astra_sites_usage_installed_time', 0 );
			$days_since_install = 0;
			if ( $install_time > 0 ) {
				$days_since_install = (int) floor( ( time() - $install_time ) / DAY_IN_SECONDS );
			}

			self::$events->track(
				'plugin_activated',
				ASTRA_SITES_VER,
				array(
					'source'             => $source,
					'is_pro'             => $is_pro ? 'yes' : 'no',
					'days_since_install' => (string) $days_since_install,
				)
			);

			// woopayments_referred: track once if WooPayments was newly activated via template.
			if ( get_option( 'astra_sites_import_complete', 'no' ) === 'yes'
				&& Astra_Sites_Page::get_instance()->get_setting( 'woopayments_ref' ) ) {
				self::$events->track( 'woopayments_referred', ASTRA_SITES_VER );
			}

		}

		/**
		 * Check if WooPayments is configured and connected to Stripe.
		 *
		 * @since 4.4.24
		 * @return bool True if WooPayments is active and connected to Stripe, false otherwise.
		 */
		public static function is_woo_payments_configured() {
			// Check if WCPay account is connected to Stripe.
			if ( class_exists( 'WC_Payments' ) && method_exists( 'WC_Payments', 'get_account_service' ) ) {
				$account_service = WC_Payments::get_account_service();
				if ( method_exists( $account_service, 'is_stripe_connected' ) && $account_service->is_stripe_connected() ) {
					self::$events->track(
						'woopayments_configured',
						ASTRA_SITES_VER
					);
					return true;
				}
			}

			return false;
		}

		/**
		 * Checks if WooPayments has processed at least one transaction.
		 *
		 * @since 4.4.34
		 * @return bool True if WooPayments has transactions, false otherwise.
		 */
		private static function has_woopayments_successful_transaction() {
			// Check if WooPayments plugin is active.
			if ( ! is_plugin_active( 'woocommerce-payments/woocommerce-payments.php' ) ) {
				return false;
			}

			// Check if WooCommerce is active (required for WooPayments).
			if ( ! is_plugin_active( 'woocommerce/woocommerce.php' ) || ! function_exists( 'wc_get_orders' ) ) {
				return false;
			}

			// Get the timestamp after which to check for transactions.
			$after_timestamp = Astra_Sites_Page::get_instance()->get_setting( 'wcpay_referred_time', 0 );

			// Use WooCommerce's built-in order query system to support both legacy and HPOS storage.
			$orders = wc_get_orders(
				array(
					'payment_method' => 'woocommerce_payments',
					'status'         => array( 'completed', 'processing', 'on-hold' ),
					'limit'          => 1,
					'date_after'     => intval( $after_timestamp ),
					'meta_query'     => array( //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Required for analytics and run in background.
						array(
							'key'     => '_wcpay_payment_transaction_id',
							'value'   => '',
							'compare' => '!=',
						),
					),
				)
			);

			if ( ! empty( $orders ) ) {
				self::$events->track(
					'woopayments_transaction_completed',
					ASTRA_SITES_VER
				);
				return true;
			}

			return false;
		}

		/**
		 * Get all posts that have been imported and not modified since import.
		 *
		 * These are posts that were imported via Starter Templates and haven't been
		 * edited by the user since import (within a 3-minute buffer for background processing).
		 *
		 * @param string $post_type The post type to check. Default is 'any'.
		 * 
		 * @since 4.4.33
		 * @return array Post IDs that have not been modified since their import.
		 */
		private static function get_unmodified_imported_post_ids( $post_type = 'any' ) {
			// Get all posts that have been imported.
			$args  = array(
				'post_type'      => $post_type,
				'post_status'    => 'publish',
				'posts_per_page' => -1,
				'meta_query'     => array( //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Required for analytics and run in background.
					array(
						'key'     => '_astra_sites_imported_post',
						'value'   => '1',
						'compare' => '=',
					),
				),
			);
			$posts = get_posts( $args );

			// Get all post IDs that have not been modified since their import.
			return array_reduce(
				$posts,
				function ( $carry, $post ) {
					$post_date     = strtotime( $post->post_date );
					$modified_date = strtotime( $post->post_modified );

					// Check if modified time is within 3 minutes (180 seconds) of post date. 3 mins buffer for background post processing.
					if ( abs( $modified_date - $post_date ) <= 180 ) {
						$carry[] = $post->ID;
					}
					return $carry;
				},
				array()
			);
		}

		/**
		 * Check if any user-engaged posts exist for a given post type.
		 *
		 * "User-engaged" means either:
		 * - Posts created by the user (not imported).
		 * - Imported posts that have been modified by the user since import.
		 *
		 * @param string $post_type The post type to check.
		 * @param array  $args      Optional arguments for WP_Query.
		 *
		 * @since 4.4.33
		 * @return bool True if any qualifying posts exist, false otherwise.
		 */
		private static function has_user_engaged_posts_of_type( $post_type = 'any', $args = array() ) {
			// If post type is not 'any', ensure it exists.
			if ( 'any' !== $post_type && ! post_type_exists( $post_type ) ) {
				return false;
			}

			// Merge default arguments with provided ones.
			$args = array_merge(
				array(
					'post_type'   => $post_type,
					'post_status' => 'publish',
					'fields'      => 'ids',
					'numberposts' => 1,
				),
				$args
			);

			// Exclude post__not_in if already set.
			if ( ! isset( $args['post__not_in'] ) ) {
				// Exclude posts that have been imported and not modified since import.
				$excluded_post_ids = self::get_unmodified_imported_post_ids( $post_type );
				if ( ! empty( $excluded_post_ids ) ) {
					$args['post__not_in'] = $excluded_post_ids;
				}
			}

			$query = new \WP_Query( $args );
			$found = $query->have_posts();
			wp_reset_postdata();

			return $found;
		}

		/**
		 * Checks if Astra Customizer has been updated after template import.
		 *
		 * ACTIVE CONDITION:
		 * - Astra theme is active.
		 * - User modified Astra Customizer settings after template import.
		 *
		 * @since 4.4.47
		 * @return bool True if Astra Customizer was updated post-import, false otherwise.
		 */
		public static function is_astra_customizer_updated() {
			// Verify Astra theme is still active.
			$current_theme = get_stylesheet();
			if ( 0 !== strpos( $current_theme, 'astra' ) ) {
				return false;
			}

			// Check if the setting has been tracked.
			$customizer_updated = (bool) Astra_Sites_Page::get_instance()->get_setting( 'astra_customizer_updated', false );
			if ( ! $customizer_updated ) {
				return false;
			}

			return true;
		}

		/**
		 * Checks if any Spectra block is used on the site.
		 *
		 * ACTIVE CONDITION:
		 * - Imported post/page is updated, published and has at least one Spectra block.
		 * - New post/page is published and has at least one Spectra block.
		 *
		 * @since 4.4.27
		 *
		 * @return bool
		 */
		public static function is_spectra_blocks_used() {
			if ( ! is_plugin_active( 'ultimate-addons-for-gutenberg/ultimate-addons-for-gutenberg.php' ) ) {
				return false;
			}

			// Check for Spectra v2 blocks (wp:uagb/).
			$has_spectra_v2_blocks = self::has_user_engaged_posts_of_type( 'any', array( 's' => '<!-- wp:uagb/' ) );
			if ( $has_spectra_v2_blocks ) {
				return true;
			}

			// Check for Spectra v3 blocks (wp:spectra/).
			return self::has_user_engaged_posts_of_type( 'any', array( 's' => '<!-- wp:spectra/' ) );
		}

		/**
		 * Checks if any UAE Header Footer Layout is published or any UAE widget is used on the site.
		 *
		 * ACTIVE CONDITION:
		 * - Imported header footer post is updated, published.
		 * - New header footer post is published.
		 *
		 * @since 4.4.27
		 *
		 * @return bool
		 */
		public static function is_uae_widgets_used() {
			if ( ! is_plugin_active( 'header-footer-elementor/header-footer-elementor.php' ) ) {
				return false;
			}

			// Check for user-engaged elementor-hf posts.
			if ( self::has_user_engaged_posts_of_type( 'elementor-hf' ) ) {
				return true;
			}

			// Check UAE widget usage data (fallback for older detection method).
			$uae_used_widgets = get_option( 'uae_widgets_usage_data_option', array() );
			if ( is_array( $uae_used_widgets ) && ! empty( $uae_used_widgets ) ) {
				return true;
			}

			return false;
		}

		/**
		 * Checks if any SureForms form is published.
		 *
		 * ACTIVE CONDITION:
		 * - Imported form is updated and published.
		 * - New form is published.
		 *
		 * @since 4.4.27
		 *
		 * @return bool
		 */
		public static function is_sureforms_form_published() {
			return self::has_user_engaged_posts_of_type( 'sureforms_form' );
		}

		/**
		 * Checks if SureMail has at least one connection configured.
		 *
		 * ACTIVE CONDITION: SureMails is connected.
		 *
		 * @since 4.4.27
		 *
		 * @return bool
		 */
		public static function is_suremails_connected() {
			if ( ! is_plugin_active( 'suremails/suremails.php' ) ) {
				return false;
			}

			// Get SureMails connections from options.
			$suremails_connections_option = defined( 'SUREMAILS_CONNECTIONS' ) ? SUREMAILS_CONNECTIONS : 'suremails_connections';
			$suremails_connections        = get_option( $suremails_connections_option, array() );
			if ( is_array( $suremails_connections ) && isset( $suremails_connections['connections'] ) && ! empty( $suremails_connections['connections'] ) ) {
				return true;
			}

			return false;
		}

		/**
		 * Checks if SureCart has any published product.
		 *
		 * ACTIVE CONDITION:
		 * - Imported SureCart product is updated and published.
		 * - New SureCart product is published.
		 *
		 * @since 4.4.27
		 *
		 * @return bool
		 */
		public static function is_surecart_product_published() {
			return self::has_user_engaged_posts_of_type( 'sc_product' );
		}

		/**
		 * Checks if CartFlows has any published funnel.
		 *
		 * ACTIVE CONDITION:
		 * - Imported funnel is updated and published.
		 * - New funnel is published.
		 *
		 * @since 4.4.27
		 *
		 * @return bool
		 */
		public static function is_cartflows_funnel_published() {
			return self::has_user_engaged_posts_of_type( 'cartflows_flow' );
		}

		/**
		 * Checks if LatePoint activities are made by admin or appointment created by users.
		 *
		 * ACTIVE CONDITION: Any activity is made by admin or appointment created by users.
		 *
		 * @since 4.4.27
		 *
		 * @return bool
		 */
		public static function is_latepoint_booking_managed() {
			if ( ! is_plugin_active( 'latepoint/latepoint.php' ) ) {
				return false;
			}

			global $wpdb;
			$latepoint_activities_table = defined( 'LATEPOINT_TABLE_ACTIVITIES' ) ? LATEPOINT_TABLE_ACTIVITIES : $wpdb->prefix . 'latepoint_activities';

			// Table names can't be parameterized in wpdb::prepare(), safe to ignore PHPCS here.
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$count = $wpdb->get_var( "SELECT COUNT(*) FROM {$latepoint_activities_table}" );

			return $count > 0;
		}

		/**
		 * Checks if a Presto Player video is embedded on the site.
		 *
		 * ACTIVE CONDITION: Presto Player block/shortcode used on any post/page
		 *
		 * @since 4.4.27
		 *
		 * @return bool
		 */
		public static function is_presto_player_used() {
			if ( ! is_plugin_active( 'presto-player/presto-player.php' ) ) {
				return false;
			}

			// Exclude 'pp_video_block' post type to avoid false positives.
			$args = array(
				'post_type'     => array_diff(
					get_post_types(
						array( 'public' => true ),
						'names'
					),
					array( 'pp_video_block' )
				),
				'posts__not_in' => array(), // To search in all posts including imported ones.
				's'             => '<!-- /wp:presto-player',
			);

			// Check for Presto Player block in posts.
			$is_presto_player_block_used = self::has_user_engaged_posts_of_type( 'any', $args );
			if ( $is_presto_player_block_used ) {
				return true;
			}

			// Check for Presto Player shortcode in posts.
			$args['s'] = '[presto_player id=';

			return self::has_user_engaged_posts_of_type( 'any', $args );
		}

		/**
		 * Checks if SureRank optimization is done on any post or 3+ site level SEO settings.
		 *
		 * ACTIVE CONDITION:
		 * - Optimization of any pages/posts (update SureRank meta, etc. of any posts/pages)
		 * - Optimize 3+ things in "site level" SEO settings (change configuration of 3 things from default values)
		 *
		 * @since 4.4.39
		 *
		 * @return bool True if optimization is done on any post or 3+ site level SEO settings, false otherwise.
		 */
		public static function is_surerank_optimization_done() {
			if ( ! defined( 'SURERANK_VERSION' ) || ! is_plugin_active( 'surerank/surerank.php' ) ) {
				return false;
			}

			// Check if 3 or more site-level settings are changed from default.
			if ( is_callable( '\SureRank\Inc\Functions\Defaults::get_instance' ) ) {
				$key               = defined( 'SURERANK_SETTINGS' ) ? SURERANK_SETTINGS : 'surerank_settings';
				$surerank_defaults = \SureRank\Inc\Functions\Defaults::get_instance()->get_global_defaults();
				$surerank_settings = get_option( $key, array() );

				if ( is_array( $surerank_settings ) && is_array( $surerank_defaults ) ) {
					$changed_settings = self::shallow_two_level_diff( $surerank_settings, $surerank_defaults );
					if ( count( $changed_settings ) >= 3 ) {
						return true;
					}
				}
			}

			global $wpdb;
			// Use direct SQL with IN() for multiple meta keys instead of WP_Query to avoid heavy JOINs and improve performance.
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$posts = $wpdb->get_col(
				$wpdb->prepare(
					"
						SELECT DISTINCT pm.post_id
						FROM {$wpdb->postmeta} pm
						INNER JOIN {$wpdb->posts} p ON pm.post_id = p.ID
						WHERE pm.meta_key IN (%s, %s, %s, %s, %s, %s)
						AND p.post_status = 'publish'
						LIMIT 1
					",
					'surerank_settings_general',
					'surerank_settings_schemas',
					'surerank_settings_social',
					'surerank_settings_post_no_index',
					'surerank_settings_post_no_follow',
					'surerank_settings_post_no_archive'
				)
			);

			return count( $posts ) >= 1;
		}

		/**
		 * Checks if ModernCart is enabled.
		 *
		 * ACTIVE CONDITION: Modern Cart setting is enabled.
		 *
		 * @since 4.4.41
		 *
		 * @return bool True if ModernCart is enabled, false otherwise.
		 */
		public static function is_modern_cart_enabled() {
			if ( ! defined( 'MODERNCART_VER' ) || ! is_plugin_active( 'modern-cart/modern-cart.php' ) ) {
				return false;
			}

			$option_name = defined( 'MODERNCART_MAIN_SETTINGS' ) ? MODERNCART_MAIN_SETTINGS : 'moderncart_settings';
			$settings    = get_option( $option_name, array() );

			if ( ! is_array( $settings ) || ! isset( $settings['enable_moderncart'] ) ) {
				return true; // Default is set to `all` which indicates that Modern Cart is enabled everywhere.
			}

			// Check if Modern Cart is enabled on WooCommerce pages or everywhere.
			return 'wc_pages' === $settings['enable_moderncart'] || 'all' === $settings['enable_moderncart'];
		}

		/**
		 * Add required plugins analytics data.
		 *
		 * @param array $stats Stats array.
		 *
		 * @since 4.4.27
		 * @return void
		 */
		private static function add_required_plugins_analytics( &$stats ) {
			$required_plugins = Astra_Sites_Page::get_instance()->get_setting( 'required_plugins', array() );
			if ( ! is_array( $required_plugins ) ) {
				return;
			}

			$stats['plugins_data'] = ! empty( $required_plugins ) ? wp_json_encode( $required_plugins ) : '';
		}

		/**
		 * Add plugin active analytics data.
		 *
		 * @param array $stats Stats array.
		 *
		 * @since 4.4.27
		 * @return void
		 */
		private static function add_plugin_active_analytics( &$stats ) {
			$stats = array_merge(
				$stats,
				array(
					'astra_customizer_updated'    => self::is_astra_customizer_updated(),
					'spectra_blocks_used'         => self::is_spectra_blocks_used(),
					'uae_widgets_used'            => self::is_uae_widgets_used(),
					'sureforms_form_published'    => self::is_sureforms_form_published(),
					'suremails_connected'         => self::is_suremails_connected(),
					'surecart_product_published'  => self::is_surecart_product_published(),
					'cartflows_funnel_published'  => self::is_cartflows_funnel_published(),
					'latepoint_booking_managed'   => self::is_latepoint_booking_managed(),
					'presto_player_used'          => self::is_presto_player_used(),
					'surerank_optimization_done'  => self::is_surerank_optimization_done(),
					'modern_cart_enabled'         => self::is_modern_cart_enabled(),
				)
			);
		}

		/**
		 * Add finish setup analytics data.
		 *
		 * @param array $stats Stats array.
		 *
		 * @since 4.4.28
		 * @return void
		 */
		private static function add_finish_setup_analytics( &$stats ) {
			// Get setup wizard showing option name.
			$option_name = class_exists( '\GS\Classes\GS_Helper' )
				? \GS\Classes\GS_Helper::get_setup_wizard_showing_option_name()
				: 'getting_started_is_setup_wizard_showing';
			$is_setup_wizard_showing = get_option( $option_name, false );
			$action_items_status     = get_option( 'getting_started_action_items', array() );
			$fs_banner_clicked       = (string) Astra_Sites_Page::get_instance()->get_setting( 'fs_banner_clicked', '' );
 
			$courses_status          = array();
			$no_of_completed_courses = 0;

			// Get the courses status from action items.
			if ( is_array( $action_items_status ) ) {
				foreach ( $action_items_status as $key => $action_item ) {
					$status                 = isset( $action_item['status'] ) ? $action_item['status'] : false;
					$courses_status[ $key ] = $status ? 'done' : 'not_done';
					if ( $status ) {
						$no_of_completed_courses++;
					}
				}
			}

			// Total number of courses.
			$total_courses = count( $courses_status );

			// Boolean and numeric values.
			$stats['boolean_values']['is_finish_setup_showing'] = $is_setup_wizard_showing;
			$stats['numeric_values']['total_courses']           = $total_courses;
			$stats['numeric_values']['no_of_completed_courses'] = $no_of_completed_courses;
			$stats['boolean_values']['course_completed']        = 0 !== $total_courses && $no_of_completed_courses >= $total_courses;

			// Dashboard banner clicked.
			if ( $fs_banner_clicked ) {
				$stats['boolean_values']['finish_setup_banner_clicked'] = 'yes' === $fs_banner_clicked;
			}

			// Plain Json data.
			$stats['courses_status'] = ! empty( $courses_status ) ? wp_json_encode( $courses_status ) : '';
		}

		/**
		 * Add KPI tracking data for import events.
		 *
		 * Groups stored timestamps by date, counts per metric per day,
		 * skips today's (incomplete) data, and cleans up sent timestamps.
		 *
		 * @param array $stats Reference to the astra sites stats data.
		 * @since 4.4.51
		 * @return void
		 */
		public static function add_kpi_tracking_data( &$stats ) {
			$timestamps = Astra_Sites_Page::get_instance()->get_setting( '_import_timestamps', array() );
			if ( empty( $timestamps ) || ! is_array( $timestamps ) ) {
				return;
			}

			$kpi_data = array();

			// Mapping: option key => KPI metric name.
			$metric_map = array(
				'templates'      => 'templates_imported',
				'block_patterns' => 'block_patterns_imported',
				'kits'           => 'kits_imported',
				'content_edited' => 'post_content_edited',
			);

			$remaining_timestamps = array();

			foreach ( $metric_map as $bucket => $metric_name ) {
				if ( empty( $timestamps[ $bucket ] ) || ! is_array( $timestamps[ $bucket ] ) ) {
					continue;
				}

				$result                          = self::process_kpi_bucket( $timestamps[ $bucket ], $metric_name, $kpi_data );
				$kpi_data                        = $result['kpi_data'];
				$remaining_timestamps[ $bucket ] = $result['remaining'];
			}

			// Only add to stats if we have data to report.
			if ( ! empty( $kpi_data ) ) {
				$stats['kpi_records'] = $kpi_data;
			}

			// Cleanup: keep only today's timestamps.
			$remaining_timestamps = array_filter( $remaining_timestamps );
			Astra_Sites_Page::get_instance()->update_settings( array( '_import_timestamps' => $remaining_timestamps ) );
		}

		/**
		 * Process a single bucket of timestamps for KPI reporting.
		 *
		 * @param array  $bucket_timestamps Array of Unix timestamps.
		 * @param string $metric_name       The KPI metric name.
		 * @param array  $kpi_data          Existing KPI data to merge into.
		 *
		 * @since 4.4.51
		 * @return array { kpi_data: array, remaining: array }
		 */
		private static function process_kpi_bucket( $bucket_timestamps, $metric_name, $kpi_data ) {
			$today     = gmdate( 'Y-m-d' );
			$remaining = array();

			foreach ( $bucket_timestamps as $timestamp ) {
				if ( ! is_numeric( $timestamp ) ) {
					continue;
				}

				$date = gmdate( 'Y-m-d', (int) $timestamp );

				// Keep today's timestamps — incomplete day, skip from reporting.
				if ( $date === $today ) {
					$remaining[] = $timestamp;
					continue;
				}

				if ( ! isset( $kpi_data[ $date ] ) ) {
					$kpi_data[ $date ] = array( 'numeric_values' => array() );
				}

				if ( ! isset( $kpi_data[ $date ]['numeric_values'][ $metric_name ] ) ) {
					$kpi_data[ $date ]['numeric_values'][ $metric_name ] = 0;
				}

				$kpi_data[ $date ]['numeric_values'][ $metric_name ]++;
			}

			return array(
				'kpi_data'  => $kpi_data,
				'remaining' => $remaining,
			);
		}

		/**
		 * Add astra sites analytics data.
		 *
		 * @param array $stats stats array.
		 *
		 * @since 4.4.27
		 * @return array
		 */
		public function add_astra_sites_analytics_data( $stats ) {
			// Load the plugin.php file to use is_plugin_active function.
			if ( ! function_exists( 'is_plugin_active' ) ) {
				require_once ABSPATH . 'wp-admin/includes/plugin.php';
			}

			$import_complete = get_option( 'astra_sites_import_complete', 'no' ) === 'yes';

			$stats['plugin_data']['astra_sites'] = array(
				'version'        => defined( 'ASTRA_PRO_SITES_NAME' ) ? 'premium' : 'free',
				'site_language'  => get_locale(),
				'plugin_version' => ASTRA_SITES_VER,
				'page_builder'   => Astra_Sites_Page::get_instance()->get_setting( 'page_builder' ),
				'boolean_values' => array(
					'import_complete'                => $import_complete,
					'woopayments_included'           => Astra_Sites_Page::get_instance()->get_setting( 'woopayments_included' ),
					'was_woopayments_referred'       => Astra_Sites_Page::get_instance()->get_setting( 'woopayments_ref' ),
					'woopayments_banner_clicked'     => Astra_Sites_Page::get_instance()->get_setting( 'woopayments_banner_clicked' ),
					'woopayments_onboarding_clicked' => Astra_Sites_Page::get_instance()->get_setting( 'woopayments_onboarding_clicked' ),
					'woopayments_configured'         => self::is_woo_payments_configured(),
					'has_woopayments_transaction'    => self::has_woopayments_successful_transaction(),
				),
				'numeric_values' => array(
					'woopayments_banner_dismissed_count' => Astra_Sites_Page::get_instance()->get_setting( 'woopayments_banner_dismissed_count' ),
				),
				'steps_visited' => Astra_Sites_Page::get_instance()->get_setting( 'steps_visited' ),
			);

			if ( $import_complete ) {
				self::add_required_plugins_analytics( $stats['plugin_data']['astra_sites'] );
				self::add_plugin_active_analytics( $stats['plugin_data']['astra_sites']['boolean_values'] );
				self::add_finish_setup_analytics( $stats['plugin_data']['astra_sites'] );
			}

			// Add KPI tracking data.
			self::add_kpi_tracking_data( $stats['plugin_data']['astra_sites'] );

			// Detect state-based milestone events (dedup in track() ensures each fires only once).
			$this->detect_state_events();

			// Flush pending events into payload (only if any exist).
			$pending_events = self::$events->flush_pending();
			if ( ! empty( $pending_events ) ) {
				$stats['plugin_data']['astra_sites']['events_record'] = $pending_events;
			}

			return $stats;
		}

		/**
		 * Compare top-level and one-level nested settings with defaults.
		 *
		 * @param array $settings Current settings.
		 * @param array $defaults Default settings.
		 *
		 * @since 4.4.39
		 * @return array Changed settings (top-level + one-level deep).
		 */
		public static function shallow_two_level_diff( array $settings, array $defaults ) {
			$difference = array();

			foreach ( $settings as $key => $value ) {
				// Key missing in defaults = changed.
				if ( ! array_key_exists( $key, $defaults ) ) {
					$difference[ $key ] = $value;
					continue;
				}

				// If value is an array, only check one level deep.
				if ( is_array( $value ) && is_array( $defaults[ $key ] ) ) {
					$nested_diff = array();
					foreach ( $value as $sub_key => $sub_value ) {
						if ( ! array_key_exists( $sub_key, $defaults[ $key ] ) || $sub_value !== $defaults[ $key ][ $sub_key ] ) {
							$nested_diff[ $sub_key ] = $sub_value;
						}
					}
					if ( ! empty( $nested_diff ) ) {
						$difference[ $key ] = $nested_diff;
					}
				} elseif ( $value !== $defaults[ $key ] ) {
					// Compare scalar values directly.
					$difference[ $key ] = $value;
				}
			}

			return $difference;
		}
	}

	/**
	 * Kicking this off by calling 'get_instance()' method
	 */
	Astra_Sites_Analytics::get_instance();
}
