<?php
/**
 * Core Class
 *
 * Main core class handling initialization and coordination.
 *
 * @package opti-behavior
 * @copyright 2025 Opti-User
 * @version 1.0.3
 */

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

/**
 * Core Class
 *
 * Handles plugin initialization and component coordination.
 *
 * @since 1.0.0
 */
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
// phpcs:disable WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
// phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching
class Opti_Behavior_Heatmap_Core {

	const SLUG = 'opti-behavior';
	const PLAN = 'basic';

	const FROM_PC     = 0;
	const FROM_MOBILE = 1;

	const ACCESS_NAMES = array(
		self::FROM_PC     => 'pc',
		self::FROM_MOBILE => 'mobile',
	);

	const VIEW_WIDTH = array(
		self::FROM_PC     => 1250,
		self::FROM_MOBILE => 375,
	);

	const CLICK_PC         = 0x10; // 16
	const CLICK_MOBILE     = 0x11; // 17
	const BREAKAWAY_PC     = 0x20; // 32
	const BREAKAWAY_MOBILE = 0x21; // 33
	const ATTENTION_PC     = 0x30; // 48
	const ATTENTION_MOBILE = 0x31; // 49

	const EVENT_NAMES = array(
		'click_pc'         => self::CLICK_PC,
		'click_mobile'     => self::CLICK_MOBILE,
		'breakaway_pc'     => self::BREAKAWAY_PC,
		'breakaway_mobile' => self::BREAKAWAY_MOBILE,
		'attention_pc'     => self::ATTENTION_PC,
		'attention_mobile' => self::ATTENTION_MOBILE,
	);

	const LIST_PER_PAGE = 25;

	/**
	 * Singleton instance
	 *
	 * @var Opti_Behavior_Heatmap_Core
	 */
	private static $instance = null;

	/**
	 * Options handler
	 *
	 * @var Opti_Behavior_Heatmap_Options
	 */
	protected $options;

	/**
	 * Database handler
	 *
	 * @var Opti_Behavior_Heatmap_Database
	 */
	protected $database;

	/**
	 * Data protection handler
	 *
	 * @var Opti_Behavior_Heatmap_Data_Protection
	 */
	protected $data_protection;

	/**
	 * Debug manager
	 *
	 * @var Opti_Behavior_Heatmap_Debug_Manager
	 */
	protected $debug_manager;

	/**
	 * Session handler
	 *
	 * @var Opti_Behavior_Heatmap_Session
	 */
	protected $session;

	/**
	 * Analytics handler
	 *
	 * @var Opti_Behavior_Heatmap_Analytics
	 */
	protected $analytics;

	/**
	 * Frontend handler
	 *
	 * @var Opti_Behavior_Heatmap_Frontend
	 */
	protected $frontend;

	/**
	 * AJAX handler
	 *
	 * @var Opti_Behavior_Heatmap_Ajax_Handler
	 */
	protected $ajax;

	/**
	 * Bot tracker
	 *
	 * @var Opti_Behavior_Heatmap_Bot_Tracker
	 */
	protected $bot_tracker;

	/**
	 * Dashboard handler
	 *
	 * @var Opti_Behavior_Heatmap_Dashboard
	 */
	protected $dashboard;

	/**
	 * Post meta box handler
	 *
	 * @var Opti_Behavior_Heatmap_Post_Metabox
	 */
	protected $post_metabox;

	/**
	 * Funnel page handler
	 *
	 * @var Opti_Behavior_Funnel_Page
	 */
	protected $funnel_page;

	/**
	 * File storage handler
	 *
	 * @var Opti_Behavior_Heatmap_File_Storage
	 */
	protected $file_storage;

	/**
	 * Heatmap storage handler (optimized file-based storage)
	 *
	 * @var Opti_Behavior_Heatmap_Storage
	 */
	protected $heatmap_storage;

	/**
	 * Data archiver
	 *
	 * @var Opti_Behavior_Heatmap_Data_Archiver
	 */
	protected $data_archiver;

	/**
	 * Is debug mode
	 *
	 * @var bool
	 */
	public $is_debug = false;

	/**
	 * Accuracy ranges
	 *
	 * @var array
	 */
	protected $ar = array();

	/**
	 * Get singleton instance
	 *
	 * @return Opti_Behavior_Heatmap_Core
	 */
	public static function get_instance() {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/**
	 * Constructor.
	 *
	 * @since 1.0.0
	 */
	private function __construct() {
		// Make the singleton visible immediately during construction.
		//
		// WordPress can load text domains while this constructor is still running;
		// our text-domain override calls get_instance() for logging. Without this
		// early assignment, that re-entrant call creates a second core instance,
		// registering admin_menu callbacks twice and rendering pages such as
		// Funnels twice.
		if ( null === self::$instance ) {
			self::$instance = $this;
		}

		$this->init_debug();
		$this->init_options();
		$this->init_components();
		$this->init_hooks();
		$this->set_accuracy_ranges();
	}

	/**
	 * Initialize debug mode.
	 *
	 * @since 1.0.0
	 */
	private function init_debug() {
		$this->is_debug = (bool) apply_filters( 'opti_behavior_heatmap_debug', false );
	}

	/**
	 * Initialize options.
	 *
	 * @since 1.0.0
	 */
	private function init_options() {
		$this->options = new Opti_Behavior_Heatmap_Options( null );
	}

	/**
	 * Check whether a Pro runtime is already loaded in this request.
	 *
	 * Free and Pro intentionally share a small set of heatmap class names. Older
	 * Pro builds include some of those files unconditionally, so Free must not
	 * pre-load its copies while an old Pro runtime is active.
	 *
	 * @return bool
	 */
	private function is_pro_runtime_loaded() {
		return defined( 'OptiBehavior_PRO_VERSION' )
			|| defined( 'OPTI_BEHAVIOR_PRO_VERSION' )
			|| class_exists( 'OptiBehavior_Pro_Core', false );
	}

	/**
	 * Get the loaded Pro version constant, if available.
	 *
	 * @return string
	 */
	private function get_loaded_pro_version() {
		if ( defined( 'OPTI_BEHAVIOR_PRO_VERSION' ) ) {
			return OPTI_BEHAVIOR_PRO_VERSION;
		}

		if ( defined( 'OptiBehavior_PRO_VERSION' ) ) {
			return OptiBehavior_PRO_VERSION;
		}

		return '';
	}

	/**
	 * Check whether the loaded Pro version has shared-class include guards.
	 *
	 * @return bool
	 */
	private function pro_runtime_has_shared_class_guards() {
		$pro_version = $this->get_loaded_pro_version();

		return '' !== $pro_version && version_compare( $pro_version, '1.5.30', '>=' );
	}

	/**
	 * Initialize components.
	 *
	 * @since 1.0.0
	 */
	private function init_components() {
		// Debug manager (must be first for logging)
		$this->debug_manager = new Opti_Behavior_Heatmap_Debug_Manager( $this );
		$this->debug_manager->log( 'Opti-Behavior plugin initializing...', 'info', 'core' );

		// Core components
		$this->debug_manager->log( 'Initializing database component', 'debug', 'core' );
		$this->database  = new Opti_Behavior_Heatmap_Database( $this );

		$this->debug_manager->log( 'Initializing session component', 'debug', 'core' );
		$this->session   = new Opti_Behavior_Heatmap_Session( $this );

		$this->debug_manager->log( 'Initializing analytics component', 'debug', 'core' );
		$this->analytics = new Opti_Behavior_Heatmap_Analytics( $this );

		// Data protection system (must be initialized early)
		$this->debug_manager->log( 'Initializing data protection system', 'debug', 'core' );
		$this->data_protection = new Opti_Behavior_Heatmap_Data_Protection( $this );

		// File storage system (now available in free version too).
		// Do not autoload Free's copy while Pro is active: old Pro builds can
		// later load their own same-named class and fatal on redeclaration.
		if ( ! $this->is_pro_runtime_loaded()
			&& ! class_exists( 'Opti_Behavior_Heatmap_File_Storage', false )
			&& file_exists( OPTI_BEHAVIOR_HEATMAP_INCLUDES_DIR . 'class-opti-behavior-heatmap-file-storage.php' )
		) {
			require_once OPTI_BEHAVIOR_HEATMAP_INCLUDES_DIR . 'class-opti-behavior-heatmap-file-storage.php';
		}

		if ( class_exists( 'Opti_Behavior_Heatmap_File_Storage', false ) ) {
			$this->debug_manager->log( 'Initializing file storage', 'info', 'core' );
			$this->file_storage = new Opti_Behavior_Heatmap_File_Storage( $this->debug_manager );

			// Data archiver (requires file storage, Pro feature only)
			if ( opti_behavior_pro_active() && class_exists( 'Opti_Behavior_Heatmap_Data_Archiver' ) ) {
				$this->debug_manager->log( 'Pro version detected - initializing data archiver', 'debug', 'core' );
				$this->data_archiver = new Opti_Behavior_Heatmap_Data_Archiver( $this->file_storage );
				$this->data_archiver->init();
			}
		} elseif ( $this->is_pro_runtime_loaded() ) {
			$this->debug_manager->log( 'Skipping Free file storage preload because Pro runtime is loaded', 'debug', 'core' );
		} else {
			$this->debug_manager->log( 'File storage class not found', 'warning', 'core' );
		}

		// Optimized heatmap storage (file-based with metadata in filename)
		if ( class_exists( 'Opti_Behavior_Heatmap_Storage' ) ) {
			$this->debug_manager->log( 'Initializing optimized heatmap storage', 'info', 'core' );
			$this->heatmap_storage = Opti_Behavior_Heatmap_Storage::get_instance();
			$this->heatmap_storage->set_debug_manager( $this->debug_manager );
		}

		// Frontend components
		$this->debug_manager->log( 'Initializing frontend component', 'debug', 'core' );
		$this->frontend = new Opti_Behavior_Heatmap_Frontend( $this );

		// AJAX handler (needed for both frontend and admin)
		$this->debug_manager->log( 'Initializing AJAX handler', 'debug', 'core' );
		$this->ajax = new Opti_Behavior_Heatmap_Ajax_Handler( $this );

		// Bot tracker (server-side bot detection)
		$this->debug_manager->log( 'Initializing bot tracker', 'debug', 'core' );
		$this->bot_tracker = new Opti_Behavior_Heatmap_Bot_Tracker( $this );

		// A/B Testing components (loaded on both frontend and admin).
		$this->debug_manager->log( 'Initializing A/B testing components', 'debug', 'core' );
		$this->init_ab_testing();

		// Funnel tracking (needs to be initialized on both frontend and backend)
		$this->debug_manager->log( 'Initializing funnel tracking', 'debug', 'core' );
		require_once OPTI_BEHAVIOR_HEATMAP_ADMIN_DIR . 'class-opti-behavior-funnel-page.php';
		$this->funnel_page = new Opti_Behavior_Funnel_Page( $this );

		// Admin components
		if ( is_admin() ) {
			$this->debug_manager->log( 'Admin context detected - initializing admin components', 'debug', 'core' );
			$this->dashboard = new Opti_Behavior_Heatmap_Dashboard( $this );
			$this->post_metabox = new Opti_Behavior_Heatmap_Post_Metabox( $this );

			// Initialize heatmap utilities (from Pro, now in Free)
			// Only load if Pro plugin is not active to avoid duplicate class declarations.
			//
			// FREE-feature recovery: when Pro plugin file is loaded but the license is
			// invalid (suspended / disabled / expired / chargebacked / blacklisted /
			// missing manifest), Pro's init_modules() early-returns and does NOT load
			// the heatmap detail page / parser / cache / AJAX classes. In that case
			// $pro_active is still true (the helper is hardcoded), but the FREE plugin
			// must take over so FREE-tier features (click heatmap detail page) keep
			// working. We detect this by also checking opti_behavior_pro_validate_env(),
			// which returns false when the manifest does not grant Pro access.
			$pro_runtime_loaded = $this->is_pro_runtime_loaded();
			$pro_active         = function_exists( 'opti_behavior_pro_active' ) && opti_behavior_pro_active();
			$pro_modules_loaded = $pro_active
				&& function_exists( 'opti_behavior_pro_validate_env' )
				&& opti_behavior_pro_validate_env();
			$can_load_free_heatmap_components = ! $pro_modules_loaded
				&& ( ! $pro_runtime_loaded || $this->pro_runtime_has_shared_class_guards() );
			$this->debug_manager->log( 'Pro plugin active status: ' . ( $pro_active ? 'YES' : 'NO' ) . '; Pro runtime loaded: ' . ( $pro_runtime_loaded ? 'YES' : 'NO' ) . '; Pro modules loaded: ' . ( $pro_modules_loaded ? 'YES' : 'NO' ), 'debug', 'core' );

			if ( $can_load_free_heatmap_components ) {
				$this->debug_manager->log( 'Loading FREE version heatmap components (Pro modules unavailable)', 'info', 'core' );

				if ( ! class_exists( 'Opti_Behavior_Heatmap_Parser', false ) && file_exists( OPTI_BEHAVIOR_HEATMAP_INCLUDES_DIR . 'class-opti-behavior-heatmap-parser.php' ) ) {
					require_once OPTI_BEHAVIOR_HEATMAP_INCLUDES_DIR . 'class-opti-behavior-heatmap-parser.php';
					$this->debug_manager->log( 'Loaded Opti_Behavior_Heatmap_Parser class', 'debug', 'core' );
				}
				if ( ! class_exists( 'Opti_Behavior_Heatmap_Cache', false ) && file_exists( OPTI_BEHAVIOR_HEATMAP_INCLUDES_DIR . 'class-opti-behavior-heatmap-cache.php' ) ) {
					require_once OPTI_BEHAVIOR_HEATMAP_INCLUDES_DIR . 'class-opti-behavior-heatmap-cache.php';
					$this->debug_manager->log( 'Loaded Opti_Behavior_Heatmap_Cache class', 'debug', 'core' );
				}

				// Initialize heatmap detail page (from Pro, now in Free)
				// Already checked $pro_active above - this code only runs if Pro is NOT active
				if ( ! class_exists( 'Opti_Behavior_Heatmap_Detail_Page', false ) && file_exists( OPTI_BEHAVIOR_HEATMAP_ADMIN_DIR . 'class-opti-behavior-heatmap-detail-page.php' ) ) {
					require_once OPTI_BEHAVIOR_HEATMAP_ADMIN_DIR . 'class-opti-behavior-heatmap-detail-page.php';
					new Opti_Behavior_Heatmap_Detail_Page();
					$this->debug_manager->log( 'Initialized Opti_Behavior_Heatmap_Detail_Page', 'info', 'core' );
				}

				// Initialize heatmap AJAX handlers (from Pro, now in Free)
				$ajax_file_path = OPTI_BEHAVIOR_HEATMAP_INCLUDES_DIR . 'class-opti-behavior-heatmap-ajax.php';
				$ajax_class_exists = class_exists( 'Opti_Behavior_Heatmap_Ajax', false );
				$ajax_file_exists = file_exists( $ajax_file_path );

				$this->debug_manager->log( 'AJAX class check: class_exists=' . ( $ajax_class_exists ? 'YES' : 'NO' ) . ', file_exists=' . ( $ajax_file_exists ? 'YES' : 'NO' ) . ', path=' . $ajax_file_path, 'debug', 'core' );

			// Already checked $pro_active above - this code only runs if Pro is NOT active
				if ( file_exists( $ajax_file_path ) ) {
					if ( ! class_exists( 'Opti_Behavior_Heatmap_Ajax', false ) ) {
						require_once $ajax_file_path;
					}
					// Always instantiate when Pro is inactive
					new Opti_Behavior_Heatmap_Ajax();
					$this->debug_manager->log( 'Initialized Opti_Behavior_Heatmap_Ajax - AJAX handlers registered for FREE version', 'info', 'core' );
				} else {
					$this->debug_manager->log( 'ERROR: Cannot load Opti_Behavior_Heatmap_Ajax - file does not exist', 'error', 'core' );
				}
			} else {
				$this->debug_manager->log( 'Pro runtime owns heatmap components - skipping FREE version heatmap components', 'info', 'core' );
			}
		}

		$this->debug_manager->log( 'All components initialized successfully', 'info', 'core' );
	}

	/**
	 * Initialize A/B testing components.
	 *
	 * Loads the database layer, bucketer, statistical engine, manager,
	 * renderer (frontend only), and registers the AJAX trait actions.
	 *
	 * @since 1.3.0
	 */
	private function init_ab_testing() {
		// All A/B classes are autoloaded via class-autoloader.php class map.
		// Renderer (frontend only — hooks into template_redirect, the_title, etc.).
		if ( ! is_admin() ) {
			if ( class_exists( 'Opti_Behavior_AB_Test_Renderer' ) ) {
				$renderer = Opti_Behavior_AB_Test_Renderer::get_instance();
				$renderer->init();
				$this->debug_manager->log( 'A/B test renderer initialized', 'debug', 'core' );
			}
		}

		// Admin page (registers menu, enqueues assets).
		if ( is_admin() && class_exists( 'Opti_Behavior_AB_Test_Page' ) ) {
			new Opti_Behavior_AB_Test_Page( $this );
			$this->debug_manager->log( 'A/B test admin page initialized', 'debug', 'core' );
		}

		// Visual Editor — hooks admin_menu, admin_enqueue_scripts, wp_ajax_*, and
		// wp_enqueue_scripts (frontend helper injection), so instantiated unconditionally.
		if ( class_exists( 'Opti_Behavior_AB_Test_Visual_Editor' ) ) {
			new Opti_Behavior_AB_Test_Visual_Editor();
			$this->debug_manager->log( 'A/B test visual editor initialized', 'debug', 'core' );
		}

		// Register AJAX actions via the trait on the existing AJAX handler.
		// The trait Opti_Behavior_AB_Tests_Ajax is `use`d by Opti_Behavior_Heatmap_Ajax_Handler.
		if ( isset( $this->ajax ) && method_exists( $this->ajax, 'register_ab_test_ajax_actions' ) ) {
			$this->ajax->register_ab_test_ajax_actions();
			$this->debug_manager->log( 'A/B test AJAX actions registered', 'debug', 'core' );
		}

		$this->debug_manager->log( 'A/B testing components loaded', 'info', 'core' );
	}

	/**
	 * Initialize WordPress hooks
	 */
	private function init_hooks() {
		add_action( 'admin_init', array( $this, 'admin_init' ) );
		add_action( 'opti_behavior_heatmap_cron_daily', array( $this, 'opti_behavior_heatmap_cron_daily' ) );
		add_action( 'opti_behavior_aggregate_daily_stats', array( $this, 'aggregate_daily_stats' ) );
		add_action( 'opti_behavior_send_scheduled_reports', array( $this, 'process_scheduled_reports' ) );
		add_action( 'opti_behavior_scheduled_smart_cleanup', array( $this, 'run_scheduled_smart_cleanup' ) );
		add_filter( 'wp_is_mobile', array( $this, 'wp_is_mobile' ) );

		// A/B testing cron hooks.
		add_action( 'opti_behavior_ab_aggregate_daily', array( $this, 'ab_aggregate_daily_stats' ) );
		add_action( 'opti_behavior_ab_auto_winner_check', array( $this, 'ab_auto_winner_check' ) );
		add_action( 'opti_behavior_ab_cleanup', array( $this, 'ab_cleanup_old_data' ) );

		// DEF-AB-003 fix: wire up the Pro scheduler so tests with a
		// start_date/end_date actually auto-start and auto-stop. The Pro
		// class `Opti_Behavior_AB_Test_Scheduler` defines the static methods
		// but nothing was invoking them, leaving the whole schedule feature
		// dormant. Hooking onto the existing hourly auto-winner cron keeps
		// the cron-table footprint small.
		add_action( 'opti_behavior_ab_auto_winner_check', array( __CLASS__, 'ab_run_scheduler_pro' ), 20 );
	}

	/**
	 * DEF-AB-003 fix helper — invoked from the hourly `opti_behavior_ab_auto_winner_check`
	 * cron tick. No-ops safely when the Pro plugin is inactive or the class is missing.
	 *
	 * @since 1.2.8
	 */
	public static function ab_run_scheduler_pro() {
		if ( ! class_exists( 'Opti_Behavior_AB_Test_Scheduler' ) ) {
			return;
		}
		if ( method_exists( 'Opti_Behavior_AB_Test_Scheduler', 'auto_start_scheduled_tests' ) ) {
			Opti_Behavior_AB_Test_Scheduler::auto_start_scheduled_tests();
		}
		if ( method_exists( 'Opti_Behavior_AB_Test_Scheduler', 'auto_stop_expired_tests' ) ) {
			Opti_Behavior_AB_Test_Scheduler::auto_stop_expired_tests();
		}
	}

	/**
	 * Set accuracy ranges
	 */
	private function set_accuracy_ranges() {
		$this->ar = $this->get_ar();
	}

	/**
	 * Get accuracy ranges
	 *
	 * @return array
	 */
	public function get_ar() {
		return array(
			// PC accuracy ranges (index 0)
			array(
				1 => array( 600, 2560 ),  // Standard accuracy: very wide range
				2 => array( 700, 1920 ),  // High accuracy: common desktop range
			),
			// Mobile accuracy ranges (index 1)
			array(
				1 => array( 280, 768 ),   // Standard accuracy: all mobile devices
				2 => array( 320, 600 ),   // High accuracy: common mobile range
			),
		);
	}

	/**
	 * Admin init callback
	 */
	public function admin_init() {
		// Handle activation
		if ( is_admin() && in_array( get_option( 'Activated_Plugin' ), array( 'opti-behavior', 'opti_behavior_heatmap' ) ) ) {
			delete_option( 'Activated_Plugin' );
			$this->database->activation();
		}

		// Check for version updates
		$activated_ver = $this->options['activated_ver'] ? $this->options['activated_ver'] : '';
		if ( version_compare( $activated_ver, OPTI_BEHAVIOR_HEATMAP_VERSION, '<' ) || self::PLAN !== $this->options['activated_plan'] ) {
			$this->database->setup();
		}

			// Safety check: ensure analytics tables exist (self-healing on admin).
			// This intentionally covers every table created by database setup, not just
			// the newest tables, so interrupted upgrades and partial uploads can recover
			// by re-running dbDelta without deleting existing data.
			global $wpdb;
			$need_setup = false;
			$tables = array(
				"{$wpdb->prefix}optibehavior_events",
				"{$wpdb->prefix}optibehavior_pages",
				"{$wpdb->prefix}optibehavior_sessions",
				"{$wpdb->prefix}optibehavior_visitors",
				"{$wpdb->prefix}optibehavior_pageviews",
				"{$wpdb->prefix}optibehavior_recordings",
				"{$wpdb->prefix}optibehavior_session_pages",
				"{$wpdb->prefix}optibehavior_referrers",
				"{$wpdb->prefix}optibehavior_outbound_clicks",
				"{$wpdb->prefix}optibehavior_bot_visits",
				"{$wpdb->prefix}optibehavior_heatmap_pages",
				"{$wpdb->prefix}optibehavior_daily_stats",
				"{$wpdb->prefix}optibehavior_errors",
				"{$wpdb->prefix}optibehavior_error_types",
				"{$wpdb->prefix}optibehavior_friction",
				"{$wpdb->prefix}optibehavior_performance",
				"{$wpdb->prefix}optibehavior_broken_links",
				"{$wpdb->prefix}optibehavior_journey_groups",
				"{$wpdb->prefix}optibehavior_report_schedules",
				"{$wpdb->prefix}optibehavior_report_logs",
				"{$wpdb->prefix}optibehavior_ab_tests",
				"{$wpdb->prefix}optibehavior_ab_variants",
				"{$wpdb->prefix}optibehavior_ab_goals",
				"{$wpdb->prefix}optibehavior_ab_impressions",
				"{$wpdb->prefix}optibehavior_ab_conversions",
				"{$wpdb->prefix}optibehavior_ab_daily_stats",
				"{$wpdb->prefix}optibehavior_ab_decision_log",
			);
			foreach ( $tables as $t ) {
				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Checking table existence.
				if ( ! $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $t ) ) ) {
					$need_setup = true;
					break;
				}
			}
			if ( $need_setup ) {
				$this->database->setup();
			}

			// One-time data migration: fix sessions that have ip = NULL due to the
			// ajax_save_recording() race-condition bug.  The migration is idempotent
			// (guarded by an option flag) so it is safe to call on every admin_init.
			$this->database->migrate_null_ip_to_anonymous();

		// Register scripts and styles
		$this->register_assets();
	}

	/**
	 * Register assets
	 */
	private function register_assets() {
		// Register debug script (must be first)
		wp_register_script(
			'opti-behavior-debug',
			OPTI_BEHAVIOR_HEATMAP_ASSETS_URL . 'js/opti-behavior-debug.js',
			array(),
			OPTI_BEHAVIOR_HEATMAP_VERSION,
			false
		);

		// Localize debug configuration
		wp_localize_script(
			'opti-behavior-debug',
			'opti_behaviorDebugConfig',
			$this->debug_manager->get_js_debug_config()
		);

		wp_register_script(
			'opti-behavior',
			OPTI_BEHAVIOR_HEATMAP_ASSETS_URL . 'js/opti-behavior-heatmap-simple.js',
			array( 'jquery', 'opti-behavior-debug' ),
			OPTI_BEHAVIOR_HEATMAP_VERSION,
			false
		);
		wp_register_style(
			'opti-behavior',
			OPTI_BEHAVIOR_HEATMAP_ASSETS_URL . 'css/style.css',
			array(),
			OPTI_BEHAVIOR_HEATMAP_VERSION
		);
	}

	/**
	 * Daily cron callback
	 */
	public function opti_behavior_heatmap_cron_daily() {
		$this->database->delete_old_data( $this->options['period'] );
		$this->database->cleanup_pages();
		if ( $this->debug_manager ) {
			$this->debug_manager->cleanup_old_logs();
		}
	}

	/**
	 * Aggregate daily statistics from raw data into pre-calculated daily_stats table.
	 * Runs via cron at 3 AM daily (before cleanup at 4 AM).
	 * - Finalizes yesterday's data
	 * - Updates today's running totals
	 * - Backfills any missing days (last 7 days)
	 *
	 * @since 1.0.4
	 */
	public function aggregate_daily_stats() {
		global $wpdb;

		$debug_manager = $this->get_debug_manager();
		$debug_manager->log( 'Starting daily stats aggregation', 'info', 'aggregation' );

		$daily_stats_table = $wpdb->prefix . 'optibehavior_daily_stats';
		$sessions_table    = $wpdb->prefix . 'optibehavior_sessions';
		$visitors_table    = $wpdb->prefix . 'optibehavior_visitors';
		$pageviews_table   = $wpdb->prefix . 'optibehavior_pageviews';

		// Check if daily_stats table exists
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table names from $wpdb->prefix; analytics queries.
		$table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $daily_stats_table ) );
		if ( ! $table_exists ) {
			$debug_manager->log( 'Daily stats table does not exist, skipping aggregation', 'warning', 'aggregation' );
			return;
		}

		// Get dates to process: today, yesterday, and backfill last 7 days
		$today     = gmdate( 'Y-m-d' );
		$yesterday = gmdate( 'Y-m-d', strtotime( '-1 day' ) );

		// Build list of dates to process (last 7 days for backfill + today)
		$dates_to_process = array();
		for ( $i = 7; $i >= 0; $i-- ) {
			$dates_to_process[] = gmdate( 'Y-m-d', strtotime( "-{$i} days" ) );
		}

		$processed_count = 0;

		foreach ( $dates_to_process as $stat_date ) {
			$is_today     = ( $stat_date === $today );
			$is_yesterday = ( $stat_date === $yesterday );

			// Check if this date already has finalized data (skip unless it's today/yesterday)
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table names from $wpdb->prefix; analytics queries.
			$existing = $wpdb->get_row(
				$wpdb->prepare(
					"SELECT id, is_finalized FROM {$daily_stats_table} WHERE stat_date = %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
					$stat_date
				)
			);

			// Skip finalized historical data (but always process today and yesterday)
			if ( $existing && $existing->is_finalized && ! $is_today && ! $is_yesterday ) {
				continue;
			}

			$date_start = $stat_date . ' 00:00:00';
			$date_end   = $stat_date . ' 23:59:59';

			// Aggregate sessions data for this date
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table names from $wpdb->prefix; analytics queries.
			$session_stats = $wpdb->get_row(
				$wpdb->prepare(
					"SELECT
						COUNT(*) as sessions,
						COUNT(DISTINCT visitor_id) as visitors,
						SUM(page_views) as pageviews,
						SUM(CASE WHEN is_bounce = 1 THEN 1 ELSE 0 END) as bounce_sessions,
						SUM(duration) as total_duration,
						SUM(CASE WHEN traffic_type = 'human' THEN 1 ELSE 0 END) as human_sessions,
						SUM(CASE WHEN traffic_type = 'spam' THEN 1 ELSE 0 END) as spam_sessions,
						SUM(CASE WHEN traffic_type = 'automated' OR traffic_type = 'bot' THEN 1 ELSE 0 END) as automated_sessions
					FROM {$sessions_table}
					WHERE start_time BETWEEN %s AND %s",
					$date_start,
					$date_end
				)
			);

			// Get scroll depth data from pageviews
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table names from $wpdb->prefix; analytics queries.
			$scroll_stats = $wpdb->get_row(
				$wpdb->prepare(
					"SELECT
						SUM(scroll_depth) as total_scroll_depth,
						COUNT(CASE WHEN scroll_depth > 0 THEN 1 END) as scroll_depth_count
					FROM {$pageviews_table}
					WHERE view_time BETWEEN %s AND %s",
					$date_start,
					$date_end
				)
			);

			// Count new vs returning visitors for this date
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table names from $wpdb->prefix; analytics queries.
			$visitor_stats = $wpdb->get_row(
				$wpdb->prepare(
					"SELECT
						COUNT(DISTINCT CASE WHEN v.first_visit >= %s AND v.first_visit <= %s THEN s.visitor_id END) as new_visitors,
						COUNT(DISTINCT CASE WHEN v.first_visit < %s THEN s.visitor_id END) as returning_visitors
					FROM {$sessions_table} s
					LEFT JOIN {$visitors_table} v ON s.visitor_id = v.id
					WHERE s.start_time BETWEEN %s AND %s",
					$date_start,
					$date_end,
					$date_start,
					$date_start,
					$date_end
				)
			);

			// Prepare data for insert/update
			$stats_data = array(
				'stat_date'          => $stat_date,
				'sessions'           => (int) ( $session_stats->sessions ?? 0 ),
				'visitors'           => (int) ( $session_stats->visitors ?? 0 ),
				'pageviews'          => (int) ( $session_stats->pageviews ?? 0 ),
				'bounce_sessions'    => (int) ( $session_stats->bounce_sessions ?? 0 ),
				'total_duration'     => (int) ( $session_stats->total_duration ?? 0 ),
				'total_scroll_depth' => (int) ( $scroll_stats->total_scroll_depth ?? 0 ),
				'scroll_depth_count' => (int) ( $scroll_stats->scroll_depth_count ?? 0 ),
				'human_sessions'     => (int) ( $session_stats->human_sessions ?? 0 ),
				'spam_sessions'      => (int) ( $session_stats->spam_sessions ?? 0 ),
				'automated_sessions' => (int) ( $session_stats->automated_sessions ?? 0 ),
				'new_visitors'       => (int) ( $visitor_stats->new_visitors ?? 0 ),
				'returning_visitors' => (int) ( $visitor_stats->returning_visitors ?? 0 ),
				'last_aggregated'    => current_time( 'mysql' ),
				'is_finalized'       => $is_today ? 0 : 1, // Today is never finalized, past days are.
			);

			if ( $existing ) {
				// Update existing record
				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table names from $wpdb->prefix; analytics queries.
				$wpdb->update(
					$daily_stats_table,
					$stats_data,
					array( 'id' => $existing->id ),
					array( '%s', '%d', '%d', '%d', '%d', '%d', '%d', '%d', '%d', '%d', '%d', '%d', '%d', '%s', '%d' ),
					array( '%d' )
				);
			} else {
				// Insert new record
				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table names from $wpdb->prefix; analytics queries.
				$wpdb->insert(
					$daily_stats_table,
					$stats_data,
					array( '%s', '%d', '%d', '%d', '%d', '%d', '%d', '%d', '%d', '%d', '%d', '%d', '%d', '%s', '%d' )
				);
			}

			++$processed_count;
		}

		$debug_manager->log( "Daily stats aggregation complete. Processed {$processed_count} days.", 'info', 'aggregation' );
	}

	/**
	 * Process scheduled reports via cron.
	 *
	 * Checks for due schedules and sends reports.
	 * Runs every 15 minutes via WordPress cron.
	 *
	 * @since 1.1.0
	 */
	/**
	 * Cron callback: aggregate A/B testing daily stats.
	 *
	 * Rolls up raw impression and conversion rows from yesterday into the
	 * daily_stats table for faster reporting.
	 *
	 * @since 1.3.0
	 */
	public function ab_aggregate_daily_stats() {
		if ( ! class_exists( 'Opti_Behavior_AB_Test_Database' ) ) {
			return;
		}
		$this->debug_manager->log( 'Running A/B daily stats aggregation', 'info', 'ab-cron' );
		Opti_Behavior_AB_Test_Database::aggregate_daily_stats(); // Defaults to yesterday.
	}

	/**
	 * Cron callback: check running A/B tests for auto-winner declaration.
	 *
	 * For each running test, evaluates statistical significance and, if
	 * the result is conclusive and the minimum duration has elapsed,
	 * automatically stops the test and declares a winner.
	 *
	 * @since 1.3.0
	 */
	public function ab_auto_winner_check() {
		if ( ! class_exists( 'Opti_Behavior_AB_Test_Database' ) || ! class_exists( 'Opti_Behavior_AB_Test_Engine' ) || ! class_exists( 'Opti_Behavior_AB_Test_Manager' ) ) {
			return;
		}

		$this->debug_manager->log( 'Running A/B auto-winner check', 'info', 'ab-cron' );

		// Pro: Auto-start/stop scheduled tests.
		if ( class_exists( 'Opti_Behavior_AB_Test_Scheduler' ) ) {
			$started = Opti_Behavior_AB_Test_Scheduler::auto_start_scheduled_tests();
			$stopped = Opti_Behavior_AB_Test_Scheduler::auto_stop_expired_tests();
			if ( $started || $stopped ) {
				$this->debug_manager->log(
					sprintf( 'Scheduler: auto-started %d, auto-stopped %d tests', $started, $stopped ),
					'info',
					'ab-cron'
				);
			}
		}

		$running_tests = Opti_Behavior_AB_Test_Database::get_tests( array( 'status' => 'running', 'per_page' => 100 ) );
		$manager       = Opti_Behavior_AB_Test_Manager::get_instance();

		foreach ( $running_tests as $opti_ab_test ) {
			$opti_ab_stats = Opti_Behavior_AB_Test_Database::get_test_stats( (int) $opti_ab_test->id );
			if ( empty( $opti_ab_stats ) ) {
				continue;
			}

			$opti_ab_sig = Opti_Behavior_AB_Test_Engine::check_significance(
				$opti_ab_stats,
				floatval( $opti_ab_test->confidence_level ),
				(int) $opti_ab_test->min_sample_size,
				(int) $opti_ab_test->min_duration_hours,
				$opti_ab_test->started_at
			);

			if ( ! empty( $opti_ab_sig['is_significant'] ) && ! empty( $opti_ab_sig['winner_variant_id'] ) ) {
				$this->debug_manager->log(
					sprintf( 'Auto-declaring winner for test #%d: variant #%d', $opti_ab_test->id, $opti_ab_sig['winner_variant_id'] ),
					'info',
					'ab-cron'
				);
				$manager->declare_winner( (int) $opti_ab_test->id, (int) $opti_ab_sig['winner_variant_id'] );
			}
		}
	}

	/**
	 * Cron callback: clean up old A/B testing raw data.
	 *
	 * Deletes raw impression and conversion rows older than the configured
	 * retention period (default 90 days). Aggregated daily stats are kept.
	 *
	 * @since 1.3.0
	 */
	public function ab_cleanup_old_data() {
		if ( ! class_exists( 'Opti_Behavior_AB_Test_Database' ) ) {
			return;
		}

		$settings       = get_option( 'opti_behavior_ab_settings', array() );
		$retention_days = isset( $settings['data_retention_days'] ) ? (int) $settings['data_retention_days'] : 90;

		$this->debug_manager->log( sprintf( 'Running A/B data cleanup (retention: %d days)', $retention_days ), 'info', 'ab-cron' );
		$deleted = Opti_Behavior_AB_Test_Database::cleanup_old_data( $retention_days );
		$this->debug_manager->log( sprintf( 'A/B cleanup deleted %d rows', $deleted ), 'info', 'ab-cron' );
	}

	public function process_scheduled_reports() {
		$debug_manager = $this->get_debug_manager();
		$debug_manager->log( 'Starting scheduled reports processing', 'info', 'reports' );

		// Load required classes
		if ( ! class_exists( 'Opti_Behavior_Report_Scheduler' ) ) {
			require_once OPTI_BEHAVIOR_HEATMAP_INCLUDES_DIR . 'class-opti-behavior-report-scheduler.php';
		}
		if ( ! class_exists( 'Opti_Behavior_Report_Generator' ) ) {
			require_once OPTI_BEHAVIOR_HEATMAP_INCLUDES_DIR . 'class-opti-behavior-report-generator.php';
		}
		if ( ! class_exists( 'Opti_Behavior_Report_Mailer' ) ) {
			require_once OPTI_BEHAVIOR_HEATMAP_INCLUDES_DIR . 'class-opti-behavior-report-mailer.php';
		}

		$scheduler = new Opti_Behavior_Report_Scheduler( $this );
		$generator = new Opti_Behavior_Report_Generator( $this );
		$mailer    = new Opti_Behavior_Report_Mailer( $this );

		// Get due schedules
		$due_schedules = $scheduler->get_due_schedules();

		if ( empty( $due_schedules ) ) {
			$debug_manager->log( 'No due schedules found', 'debug', 'reports' );
			return;
		}

		$debug_manager->log( sprintf( 'Found %d due schedule(s)', count( $due_schedules ) ), 'info', 'reports' );

		foreach ( $due_schedules as $schedule ) {
			$start_time = microtime( true );

			try {
				$debug_manager->log( sprintf( 'Processing schedule #%d: %s', $schedule['id'], $schedule['name'] ), 'info', 'reports' );

				// Generate report data
				$report = $generator->generate( $schedule );

				if ( ! $report || empty( $report['sections'] ) ) {
					$debug_manager->log( sprintf( 'Schedule #%d: Failed to generate report data', $schedule['id'] ), 'error', 'reports' );

					$scheduler->log_send(
						$schedule['id'],
						'failed',
						array(
							'error'           => 'Failed to generate report data',
							'execution_time'  => (int) ( ( microtime( true ) - $start_time ) * 1000 ),
							'recipients_count'=> count( $schedule['recipients'] ),
						)
					);

					continue;
				}

				// Send report
				$result = $mailer->send( $schedule, $report );

				// Calculate execution time
				$execution_time = (int) ( ( microtime( true ) - $start_time ) * 1000 );

				// Log result
				$status = $result['success'] ? 'success' : ( $result['partial'] ? 'partial' : 'failed' );

				$scheduler->log_send(
					$schedule['id'],
					$status,
					array(
						'recipients_count'   => count( $schedule['recipients'] ),
						'recipients_success' => $result['sent_count'],
						'period_start'       => $report['period_start'],
						'period_end'         => $report['period_end'],
						'error'              => $result['error'] ?? null,
						'execution_time'     => $execution_time,
					)
				);

				// Mark as sent and calculate next send time
				$scheduler->mark_as_sent( $schedule['id'] );

				$debug_manager->log(
					sprintf(
						'Schedule #%d: Completed. Status: %s, Sent: %d/%d, Time: %dms',
						$schedule['id'],
						$status,
						$result['sent_count'],
						count( $schedule['recipients'] ),
						$execution_time
					),
					$status === 'success' ? 'info' : 'warning',
					'reports'
				);

			} catch ( Exception $e ) {
				$debug_manager->log(
					sprintf( 'Schedule #%d: Exception - %s', $schedule['id'], $e->getMessage() ),
					'error',
					'reports'
				);

				$scheduler->log_send(
					$schedule['id'],
					'failed',
					array(
						'error'            => $e->getMessage(),
						'execution_time'   => (int) ( ( microtime( true ) - $start_time ) * 1000 ),
						'recipients_count' => count( $schedule['recipients'] ),
					)
				);
			}
		}

		// Clean up old logs (keep last 90 days)
		$scheduler->clear_old_logs( 90 );

		$debug_manager->log( 'Scheduled reports processing complete', 'info', 'reports' );
	}

	/**
	 * Filter wp_is_mobile
	 *
	 * @param bool $is_mobile Whether the request is from a mobile device.
	 * @return bool
	 */
	public function wp_is_mobile( $is_mobile ) {
		// Nonce verification not required - this filter only reads GET parameters for display purposes
		// The actual heatmap view is protected by capability checks in can_view()
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only GET parameter for display mode
		$optibehavior = filter_input( INPUT_GET, 'opti-behavior', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only GET parameter for display mode
		$mobile_view = isset( $_GET['mobile_view'] ) && sanitize_text_field( wp_unslash( $_GET['mobile_view'] ) ) == '1';
		return $is_mobile;
	}

	/**
	 * Option validation callback
	 *
	 * @param array $new_options New options.
	 * @param array $old_options Old options.
	 * @return array
	 */
	public function option_checker( $new_options, $old_options = null ) {
		// Simple validation - just return the options as-is for now
		// More complex validation can be added later
		return $new_options;
	}

	/**
	 * Get options
	 *
	 * @return Opti_Behavior_Heatmap_Options
	 */
	public function get_options() {
		return $this->options;
	}

	/**
	 * Get database handler
	 *
	 * @return Opti_Behavior_Heatmap_Database
	 */
	public function get_database() {
		return $this->database;
	}

	/**
	 * Get session handler
	 *
	 * @return Opti_Behavior_Heatmap_Session
	 */
	public function get_session() {
		return $this->session;
	}

	/**
	 * Get analytics handler
	 *
	 * @return Opti_Behavior_Heatmap_Analytics
	 */
	public function get_analytics() {
		return $this->analytics;
	}

	/**
	 * Get frontend handler
	 *
	 * @return Opti_Behavior_Heatmap_Frontend
	 */
	public function get_frontend() {
		return $this->frontend;
	}

	/**
	 * Get AJAX handler
	 *
	 * @return Opti_Behavior_Heatmap_Ajax_Handler
	 */
	public function get_ajax() {
		return $this->ajax;
	}

	/**
	 * Get dashboard handler
	 *
	 * @return Opti_Behavior_Heatmap_Dashboard
	 */
	public function get_dashboard() {
		return $this->dashboard;
	}

	/**
	 * Get data protection handler
	 *
	 * @return Opti_Behavior_Heatmap_Data_Protection
	 */
	public function get_data_protection() {
		return $this->data_protection;
	}

	/**
	 * Get debug manager
	 *
	 * @return Opti_Behavior_Heatmap_Debug_Manager
	 */
	public function get_debug_manager() {
		return $this->debug_manager;
	}

	/**
	 * Get file storage handler
	 *
	 * @return Opti_Behavior_Heatmap_File_Storage
	 */
	public function get_file_storage() {
		return $this->file_storage;
	}

	/**
	 * Get heatmap storage handler (optimized file-based storage)
	 *
	 * @return Opti_Behavior_Heatmap_Storage
	 */
	public function get_heatmap_storage() {
		return $this->heatmap_storage;
	}

	/**
	 * Get data archiver
	 *
	 * @return Opti_Behavior_Heatmap_Data_Archiver
	 */
	public function get_data_archiver() {
		return $this->data_archiver;
	}

	/**
	 * Get funnel page handler
	 *
	 * @return Opti_Behavior_Funnel_Page
	 */
	public function get_funnel_page() {
		return $this->funnel_page;
	}

	/**
	 * Check if user can view heatmaps
	 *
	 * @return bool
	 */
	public function can_view() {
		// Protect heatmap view: only administrators can view overlay via URL parameter
		// phpcs:disable WordPress.Security.NonceVerification.Recommended -- GET parameter check for heatmap overlay (permission check follows)
		if ( isset( $_GET['opti-behavior'] ) ) {
			// phpcs:enable WordPress.Security.NonceVerification.Recommended
			return current_user_can( 'manage_options' );
		}
		// phpcs:enable WordPress.Security.NonceVerification.Recommended

		// Fallback: restrict general heatmap viewing to administrators as well
		return current_user_can( 'manage_options' );
	}

	/**
	 * Static activation hook
	 */
	public static function activation() {
		$instance = self::get_instance();
		$instance->database->activation();
	}

	/**
	 * Static deactivation hook
	 */
	public static function deactivation() {
		wp_clear_scheduled_hook( 'opti_behavior_heatmap_cron_daily' );
		wp_clear_scheduled_hook( 'opti_behavior_aggregate_daily_stats' );
		wp_clear_scheduled_hook( 'opti_behavior_send_scheduled_reports' );
		wp_clear_scheduled_hook( 'opti_behavior_scheduled_smart_cleanup' );
	}

	/**
	 * Cron callback for scheduled smart cleanup.
	 *
	 * @since 1.0.9
	 */
	public function run_scheduled_smart_cleanup() {
		$settings = get_option( 'opti_behavior_auto_cleanup_settings', array() );

		if ( empty( $settings['enabled'] ) ) {
			return;
		}

		// For monthly frequency, check if it's been 30+ days since last run
		if ( isset( $settings['frequency'] ) && $settings['frequency'] === 'monthly' ) {
			$last_run = isset( $settings['last_run'] ) ? intval( $settings['last_run'] ) : 0;
			if ( $last_run > 0 && ( time() - $last_run ) < ( 30 * DAY_IN_SECONDS ) ) {
				return; // Not yet time for monthly cleanup
			}
		}

		// Delegate to the maintenance trait method via dashboard
		if ( $this->dashboard ) {
			$this->dashboard->run_scheduled_cleanup();
		}
	}
}
