<?php
namespace Burst\Admin;

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

use Burst\Admin\App\App;
use Burst\Admin\Abilities_Api\Abilities_Api;
use Burst\Admin\Archive\Archive;
use Burst\Admin\Burst_Wp_Cli\Burst_Wp_Cli;
use Burst\Admin\Cron\Cron;
use Burst\Admin\Dashboard_Widget\Dashboard_Widget;
use Burst\Admin\Data_Sharing\Data_Sharing;
use Burst\Admin\DB_Upgrade\DB_Upgrade;
use Burst\Admin\Debug\Debug;
use Burst\Admin\Plugins\Plugin_Updates;
use Burst\Admin\Posts\Posts;
use Burst\Admin\Reports\Report_Logs;
use Burst\Admin\Reports\Reports;
use Burst\Admin\Search\Search;
use Burst\Admin\Errors\Errors;
use Burst\Admin\Integrations\Integrations_Settings;
use Burst\Admin\Search_Console\Search_Console;
use Burst\Admin\Engagement\Reading_Engagement;
use Burst\Admin\Share\Share;
use Burst\Admin\Geo_Ip\Geo_Ip;
use Burst\Admin\Statistics\Geo_Statistics;
use Burst\Admin\Statistics\Goal_Statistics;
use Burst\Admin\Statistics\Statistics;
use Burst\Frontend\Goals\Goal;
use Burst\Frontend\Goals\Goals;
use Burst\Traits\Admin_Helper;
use Burst\Traits\Database_Helper;
use Burst\Traits\Helper;
use Burst\Traits\Save;

class Admin {
	use Database_Helper;
	use Helper;
	use Admin_Helper;
	use Save;

	public Tasks $tasks;
	public Statistics $statistics;
	public App $app;
	public Share $share;

	/**
	 * Initialize the admin class
	 */
	public function init(): void {
		$recalculate_cron_interval = apply_filters( 'burst_recalculate_cron_interval', 'burst_every_ten_minutes' );
		add_action( $recalculate_cron_interval, [ $this, 'update_last_statistic_data' ] );
		add_action( 'burst_recalculate_known_uids_cron', [ $this, 'update_known_uids_table' ] );
		add_action( 'burst_recalculate_bounces_cron', [ $this, 'recalculate_bounces' ] );
		add_action( 'burst_recalculate_first_time_visits_cron', [ $this, 'recalculate_first_time_visits' ] );

		if ( ! BURST_TRACK_ONLY ) {
			/**
			 * Tell the consent API we're following the api
			 */
			$plugin = BURST_PLUGIN;
			add_filter( "wp_consent_api_registered_$plugin", '__return_true' );
			add_filter( "plugin_action_links_$plugin", [ $this, 'plugin_settings_link' ] );
			add_filter( "network_admin_plugin_action_links_$plugin", [ $this, 'plugin_settings_link' ] );

			// deactivating.
			add_action( 'admin_footer', [ $this, 'deactivate_popup' ], 40 );
			add_action( 'admin_init', [ $this, 'listen_for_deactivation' ], 40 );
			add_action( 'admin_init', [ $this, 'add_privacy_info' ], 10 );

			// remove tables on multisite uninstall.
			add_filter( 'wpmu_drop_tables', [ $this, 'ms_remove_tables' ], 10, 2 );
			add_filter( 'burst_do_action', [ $this, 'maybe_delete_all_data' ], 10, 3 );
			add_action( 'burst_after_updated_goals', [ $this, 'create_js_file' ], 10, 1 );
			add_action( 'burst_after_saved_fields', [ $this, 'create_js_file' ], 10, 1 );
			add_action( 'burst_daily', [ $this, 'create_js_file' ] );
			// The baked should_load_ecommerce tracking option depends on which ecommerce
			// plugin is active, so the combined js file must be refreshed on (de)activation.
			add_action( 'activated_plugin', [ $this, 'schedule_js_file_refresh_on_plugin_change' ] );
			add_action( 'deactivated_plugin', [ $this, 'schedule_js_file_refresh_on_plugin_change' ] );
			add_action( 'burst_create_js_file', [ $this, 'create_js_file' ] );
			add_action( 'burst_daily', [ $this, 'schedule_detect_malicious_data_cron' ] );
			add_action( 'burst_detect_malicious_data', [ $this, 'detect_malicious_data' ] );
			add_action( 'burst_dismiss_task', [ $this, 'dismiss_malicious_data_notice' ], 10, 1 );
			add_action( 'burst_dismiss_task', [ $this, 'dismiss_php_error_notice' ], 10, 1 );
			// After run_table_init_hook (priority 10), so the goals table exists when the file is baked.
			add_action( 'wp_initialize_site', [ $this, 'create_js_file_for_new_site' ], 20, 1 );
			add_action( 'admin_init', [ $this, 'activation' ], 3, 1 );
			add_action( 'burst_activation', [ $this, 'setup_defaults' ], 20, 1 );
			add_action( 'burst_activation', [ $this, 'run_table_init_hook' ], 10, 1 );
			add_action( 'after_reset_stats', [ $this, 'run_table_init_hook' ], 10, 1 );
			add_action( 'wp_initialize_site', [ $this, 'run_table_init_hook' ], 10, 1 );
			add_action( 'burst_upgrade_before', [ $this, 'run_table_init_hook' ], 10, 1 );
			add_action( 'burst_cron_table_upgrade', [ $this, 'run_table_init_hook' ], 10, 1 );
			add_action( 'burst_daily', [ $this, 'validate_tasks' ] );
			add_action( 'burst_validate_tasks', [ $this, 'validate_tasks' ] );
			add_action( 'plugins_loaded', [ $this, 'init_wpcli' ] );
			add_action( 'burst_scheduled_task_fix_malicious_data_removal', [ $this, 'clean_malicious_data' ] );
			add_action( 'burst_daily', [ $this, 'test_database_tables' ] );
			add_action( 'burst_attempt_database_fix', [ $this, 'test_database_tables' ] );
			add_action( 'burst_weekly', [ $this, 'cleanup_bf_dismissed_tasks' ] );
			add_action( 'burst_daily', [ $this, 'cleanup_php_error_notices' ] );
			add_action( 'burst_daily', [ $this, 'cleanup_orphaned_block_goals' ] );
			add_filter( 'burst_menu', [ $this, 'add_ecommerce_menu_item' ] );

			$this->maybe_update_site_scheme();
			$upgrade = new Upgrade();
			$upgrade->init();
			$db_upgrade = new DB_Upgrade();
			$db_upgrade->init();
			$cron = new Cron();
			$cron->init();

			$archive = new Archive();
			$archive->init();

			$goal_statistics = new Goal_Statistics();
			$goal_statistics->init();
			$this->statistics = new Statistics();
			$this->statistics->init();
			$geo_statistics = new Geo_Statistics();
			$geo_statistics->init();
			// The Country GeoIP database manager ships in free. Pro replaces it with
			// its City variant (Geo_Ip_Pro), so only load the core manager when Pro
			// is not active.
			if ( ! defined( 'BURST_PRO_FILE' ) ) {
				$geo_ip = new Geo_Ip();
				$geo_ip->init();
			}
			$search = new Search();
			$search->init();
			$errors = new Errors();
			$errors->init();
			$integrations_settings = new Integrations_Settings();
			$integrations_settings->init();
			$search_console = new Search_Console();
			$search_console->init();
			$reading_engagement = new Reading_Engagement();
			$reading_engagement->init();
			$reports = new Reports();
			$reports->init();
			$reports_logs = Report_Logs::instance();
			$reports_logs->init();
			$this->app = new App();
			$this->app->init();
			$abilities_api = new Abilities_Api();
			$abilities_api->init();

			$posts = new Posts();
			$posts->init();

			$review = new Review();
			$review->init();

			// Smart update timing (Integrations > Smart update timing). Hooks
			// register unconditionally and gate themselves on the settings
			// toggles and stored opt-ins, so cron events keep a handler, stored
			// per-plugin opt-ins keep updating inside the window and enabling a
			// toggle takes effect in the request that saves it.
			$plugin_updates = new Plugin_Updates();
			$plugin_updates->init();

			$this->tasks = new Tasks();
			$widget      = new Dashboard_Widget();
			$widget->init();

			$debug = new Debug();
			$debug->init();

			$milestones = new Milestones();
			$milestones->init();

			if ( $this->get_option_bool( 'anonymous_usage_data' ) ) {
				$data_sharing = new Data_Sharing();
				$data_sharing->init();
			}

			if ( defined( 'BURST_BLUEPRINT' ) && ! get_option( 'burst_demo_data_installed' ) ) {
				add_action( 'init', [ $this, 'install_demo_data' ] );
				update_option( 'burst_demo_data_installed', true, false );
			}

			$this->share = new Share();
			$this->share->init();
		}
	}

	/**
	 * Persist the detected SSL scheme so cron requests can use it as a fallback.
	 * Only runs outside of cron, where is_ssl() is reliable.
	 */
	private function maybe_update_site_scheme(): void {
		if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
			return;
		}
		$detected_scheme = is_ssl() ? 'https' : 'http';
		if ( get_option( 'burst_site_scheme' ) !== $detected_scheme ) {
			update_option( 'burst_site_scheme', $detected_scheme, false );
		}
	}


	/**
	 * Cron to update the last 10 minutes of user data for bounces and first_time_visits.
	 */
	public function update_last_statistic_data(): void {
		if ( ! wp_next_scheduled( 'burst_recalculate_known_uids_cron' ) ) {
			wp_schedule_single_event( time() + 10, 'burst_recalculate_known_uids_cron' );
		}

		if ( ! wp_next_scheduled( 'burst_recalculate_bounces_cron' ) ) {
			wp_schedule_single_event( time() + 60, 'burst_recalculate_bounces_cron' );
		}

		if ( ! wp_next_scheduled( 'burst_recalculate_first_time_visits_cron' ) ) {
			wp_schedule_single_event( time() + 120, 'burst_recalculate_first_time_visits_cron' );
		}
	}

	/**
	 * Recalculate first_time_visit flags
	 *
	 * Marks the earliest statistic for each UID as first_time_visit = 1
	 * Runs daily to keep data accurate without impacting real-time performance
	 *
	 * @hooked burst_recalculate_first_time_visits_cron
	 */
	public function recalculate_first_time_visits(): void {
		global $wpdb;

		$last_update    = get_option( 'burst_last_first_time_visit_update', time() - 15 * MINUTE_IN_SECONDS );
		$default_cutoff = time() - apply_filters( 'burst_cron_update_range_seconds', 15 * MINUTE_IN_SECONDS );
		$time_cutoff    = ( $last_update < $default_cutoff ) ? $last_update : $default_cutoff;

		// Mark session as first_time_visit = 1 only if the session's earliest statistic
		// matches the UID's first_seen in known_uids — meaning this is genuinely their first visit.
		$wpdb->query(
			$wpdb->prepare(
				"UPDATE {$wpdb->prefix}burst_sessions sess
				INNER JOIN (
					SELECT session_id, MIN(time) AS first_time, uid
					FROM {$wpdb->prefix}burst_statistics
					WHERE time >= %d
					GROUP BY session_id, uid
				) st ON st.session_id = sess.ID
				INNER JOIN {$wpdb->prefix}burst_known_uids known ON st.uid = known.uid
				SET sess.first_time_visit = 1
				WHERE sess.first_time_visit = 0
				AND known.first_seen >= st.first_time - 1
				AND known.first_seen <= st.first_time + 1",
				$time_cutoff
			)
		);

		update_option( 'burst_last_first_time_visit_update', time(), false );
	}

	/**
	 * Update incrementail UIDs table.
	 */
	public function update_known_uids_table(): void {
		global $wpdb;
		// Get sync cutoff (default: last 10 minutes of new data to process).
		$last_sync      = get_option( 'burst_last_known_uids_sync', time() - 15 * MINUTE_IN_SECONDS );
		$default_cutoff = time() - apply_filters( 'burst_cron_update_range_seconds', 15 * MINUTE_IN_SECONDS );
		$sync_cutoff    = ( $last_sync < $default_cutoff ) ? $last_sync : $default_cutoff;

		// Cleanup threshold: remove UIDs not seen in 31+ days.
		$cleanup_cutoff = time() - ( 31 * DAY_IN_SECONDS );

		// Step 1: Add/update UIDs from recent statistics (last 10-15 minutes).
		$wpdb->query(
			$wpdb->prepare(
				"
        INSERT INTO {$wpdb->prefix}burst_known_uids (uid, first_seen, last_seen)
        SELECT uid, MIN(time) as first_seen, MAX(time) as last_seen
        FROM {$wpdb->prefix}burst_statistics
        WHERE time >= %d
        GROUP BY uid
        ON DUPLICATE KEY UPDATE
            first_seen = LEAST(first_seen, VALUES(first_seen)),
            last_seen = GREATEST(last_seen, VALUES(last_seen))
    ",
				$sync_cutoff
			)
		);

		// Step 2: Remove UIDs not seen in 31+ days (cleanup old visitors).
		$wpdb->query(
			$wpdb->prepare(
				"
        DELETE FROM {$wpdb->prefix}burst_known_uids
        WHERE last_seen < %d
    ",
				$cleanup_cutoff
			)
		);

		update_option( 'burst_last_known_uids_sync', time(), false );
	}

	/**
	 * Recalculate bounces using until last non-bounce
	 */
	public function recalculate_bounces(): void {
		$last_update    = get_option( 'burst_last_bounces_update', time() - 15 * MINUTE_IN_SECONDS );
		$default_cutoff = time() - apply_filters( 'burst_cron_update_range_seconds', 15 * MINUTE_IN_SECONDS );
		$time_cutoff    = ( $last_update < $default_cutoff ) ? $last_update : $default_cutoff;

		global $wpdb;

		$bounce_time_milliseconds = apply_filters( 'burst_bounce_time', 5000 );
		// Find sessions with >1 pageview or long time_on_page (engaged), then mark them as not bounced.
		$wpdb->query(
			$wpdb->prepare(
				"UPDATE {$wpdb->prefix}burst_sessions sess
            INNER JOIN (
                SELECT session_id
                FROM {$wpdb->prefix}burst_statistics
                WHERE time > %d
                GROUP BY session_id
                HAVING COUNT(*) > 1
                    OR MAX(time_on_page) > %d
            ) nb ON sess.ID = nb.session_id
            SET sess.bounce = 0
            WHERE sess.bounce = 1",
				$time_cutoff,
				$bounce_time_milliseconds
			)
		);

		update_option( 'burst_last_bounces_update', time(), false );
	}

	/**
	 * Remove CM and BF tasks from the permanently dismissed array if it is february.
	 */
	public function cleanup_bf_dismissed_tasks(): void {
		if ( (int) gmdate( 'n' ) === 2 ) {
			\Burst\burst_loader()->admin->tasks->undismiss_task( 'bf_notice' );
			\Burst\burst_loader()->admin->tasks->undismiss_task( 'cm_notice' );
		}
	}

	/**
	 * Run a daily check if all database tables exist.
	 */
	public function test_database_tables(): void {
		// do not run if the plugin is just activated.
		$plugin_activated_time = get_option( 'burst_activation_time', 0 );
		$one_hour_ago          = time() - HOUR_IN_SECONDS;
		if ( $plugin_activated_time === 0 || $one_hour_ago < $plugin_activated_time ) {
			return;
		}

		$table_names    = $this->get_table_list();
		$missing_tables = [];
		foreach ( $table_names as $table_name ) {
			if ( ! $this->table_exists( $table_name ) ) {
				$missing_tables[] = $table_name;
			}
		}

		if ( ! empty( $missing_tables ) ) {
			$first_attempt = ! get_option( 'burst_attempt_database_fix' );
			if ( $first_attempt ) {
				// first, try installing them.
				$this->run_table_init_hook();
				update_option( 'burst_attempt_database_fix', true, false );
				wp_schedule_single_event( time() + 10, 'burst_attempt_database_fix' );
			} else {
				update_option( 'burst_missing_tables', implode( ',', $missing_tables ), false );
			}
		} else {
			delete_option( 'burst_missing_tables' );
			delete_option( 'burst_attempt_database_fix' );
		}
	}

	/**
	 * Initialize WP CLI
	 *
	 * @throws \Exception //exception.
	 */
	public function init_wpcli(): void {
		if ( ! defined( 'WP_CLI' ) || ! WP_CLI ) {
			return;
		}

		// Register the command.
		\WP_CLI::add_command( 'burst', Burst_Wp_Cli::class );
	}

	/**
	 * Offload the malicious data detection two minutes later in the future.
	 * This also resolves a missing table error right after deactivating, and activating the plugin again with data reset.
	 */
	public function schedule_detect_malicious_data_cron(): void {
		if ( ! wp_next_scheduled( 'burst_detect_malicious_data' ) ) {
			wp_schedule_single_event( time() + 2 * MINUTE_IN_SECONDS, 'burst_detect_malicious_data' );
		}
	}
	/**
	 * Check if there is anomalous data in the past 24 hours, over 1000 requests from one visitor.
	 */
	public function detect_malicious_data(): void {
		if ( ! $this->user_can_manage() ) {
			return;
		}
		$interval_days = (int) apply_filters( 'burst_data_cleanup_interval_days', 1 );
		$data_treshold = (int) apply_filters( 'burst_data_cleanup_treshold', 1000 );
		global $wpdb;
		$uids = $wpdb->get_results(
			$wpdb->prepare(
				"
            SELECT uid, COUNT(*) as record_count
            FROM {$wpdb->prefix}burst_statistics
            WHERE time > UNIX_TIMESTAMP(NOW() - INTERVAL %d DAY)
            GROUP BY uid
            HAVING COUNT(*) > %d LIMIT 1;
        ",
				$interval_days,
				$data_treshold
			),
			ARRAY_A
		);

		if ( ! empty( $uids ) ) {

			$uids = array_map(
				function ( $item ) {
					return $item['uid'];
				},
				$uids
			);
			// get first $uid.
			$uid = reset( $uids );
			update_option( 'burst_cleanup_uid', $uid, false );
			$total_hits   = array_sum( wp_list_pluck( $uids, 'record_count' ) );
			$average_hits = round( $total_hits / count( $uids ) );
			update_option( 'burst_cleanup_uid_visits', $average_hits, false );
			update_option( 'burst_cleanup_data_detected_time', time(), false );
		} else {
			// if this notice is over two weeks old, clear it.
			$detected_time     = (int) get_option( 'burst_cleanup_data_detected_time', time() );
			$max_age_threshold = time() - ( 14 * DAY_IN_SECONDS );
			if ( $detected_time < $max_age_threshold ) {
				delete_option( 'burst_cleanup_data_detected_time' );
				delete_option( 'burst_clean_data' );
				delete_option( 'burst_cleanup_uid' );
				delete_option( 'burst_cleanup_uid_visits' );
			}
		}
	}

	/**
	 * Clean up the notification data when the task has been dismissed instead of fixed.
	 */
	public function dismiss_malicious_data_notice( string $task_id ): void {
		if ( $task_id === 'malicious_data_removal' ) {
			delete_option( 'burst_cleanup_data_detected_time' );
			delete_option( 'burst_clean_data' );
			delete_option( 'burst_cleanup_uid' );
			delete_option( 'burst_cleanup_uid_visits' );
		}
	}

	/**
	 * On a daily basis, cleanup suspiciously high amounts of data.
	 *
	 * @hooked burst_daily
	 */
	public function clean_malicious_data(): void {
		if ( ! $this->user_can_manage() ) {
			return;
		}

		// only when explicitly triggerred.
		if ( ! get_option( 'burst_clean_data' ) ) {
			return;
		}

		$uid = get_option( 'burst_cleanup_uid' );
		if ( strlen( $uid ) < 10 ) {
			self::error_log( 'Suspicious UID format, cleanup aborted: ' . $uid );
			return;
		}

		$detected_time     = (int) get_option( 'burst_cleanup_data_detected_time', time() );
		$max_age_threshold = time() - ( 14 * DAY_IN_SECONDS );

		delete_option( 'burst_cleanup_data_detected_time' );
		delete_option( 'burst_clean_data' );
		delete_option( 'burst_cleanup_uid' );
		delete_option( 'burst_cleanup_uid_visits' );

		if ( $detected_time < $max_age_threshold ) {
			return;
		}

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

		$interval_days     = (int) apply_filters( 'burst_data_cleanup_interval_days', 1 );
		$cleanup_threshold = $detected_time - ( $interval_days * DAY_IN_SECONDS );

		global $wpdb;
		$wpdb->query( 'START TRANSACTION' );

		try {
			// 1. Get statistic IDs first.
			$statistic_ids = $wpdb->get_col(
				$wpdb->prepare(
					"
            SELECT ID
            FROM {$wpdb->prefix}burst_statistics
            WHERE uid = %s
            AND time >= %d
        ",
					$uid,
					$cleanup_threshold
				)
			);

			// 2. Delete goal statistics.
			if ( ! empty( $statistic_ids ) ) {
				$placeholders = implode( ',', array_fill( 0, count( $statistic_ids ), '%d' ) );
				$sql          = "DELETE FROM {$wpdb->prefix}burst_goal_statistics WHERE statistic_id IN ($placeholders)";
                // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- values are prepared above.
				$wpdb->query( $wpdb->prepare( $sql, ...$statistic_ids ) );
			}

			// 3. Get session IDs first.
			$session_ids = $wpdb->get_col(
				$wpdb->prepare(
					"
            SELECT DISTINCT session_id
            FROM {$wpdb->prefix}burst_statistics
            WHERE uid = %s
            AND time >= %d
            AND session_id IS NOT NULL
        ",
					$uid,
					$cleanup_threshold
				)
			);

			// 4. Delete sessions.
			if ( ! empty( $session_ids ) ) {
				$placeholders = implode( ',', array_fill( 0, count( $session_ids ), '%d' ) );
				$sql          = "DELETE FROM {$wpdb->prefix}burst_sessions WHERE ID IN ($placeholders)";
                // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- values are prepared above.
				$wpdb->query( $wpdb->prepare( $sql, ...$session_ids ) );
			}

			// 5. Delete statistics.
			$wpdb->query(
				$wpdb->prepare(
					"
            DELETE FROM {$wpdb->prefix}burst_statistics
            WHERE uid = %s
            AND time >= %d
        ",
					$uid,
					$cleanup_threshold
				)
			);

			$wpdb->query( 'COMMIT' );

		} catch ( \Exception $e ) {
			$wpdb->query( 'ROLLBACK' );
			self::error_log( 'Rolled back data cleanup: ' . $e->getMessage() );
		}
	}

	/**
	 * Once a day, check if any tasks need to be added again
	 */
	public function validate_tasks(): void {
		$this->tasks->validate_tasks();
	}

	/**
	 * Insert row in a table
	 */
	private function insert_row( string $table, array $rows ): void {
		if ( ! $this->user_can_manage() ) {
			return;
		}
		global $wpdb;
		$table = "{$wpdb->prefix}burst_$table";
		foreach ( $rows as $row ) {
			$wpdb->insert(
				$table,
				$row
			);
		}
	}

	/**
	 * Clean up errors after some time, to prevent them hanging around indefinitely.
	 */
	public function cleanup_php_error_notices(): void {
		$last_detected = get_option( 'burst_php_error_time', time() );
		if ( ! $last_detected ) {
			return;
		}

		$x_days_ago = time() - 7 * DAY_IN_SECONDS;
		if ( $last_detected < $x_days_ago ) {
			delete_option( 'burst_php_error_time' );
			delete_option( 'burst_php_error_detected' );
			delete_option( 'burst_php_error_count' );
		}
	}

	/**
	 * Clean up the notification data when the task has been dismissed instead of fixed.
	 */
	public function dismiss_php_error_notice( string $task_id ): void {
		if ( $task_id === 'php_error_detected' ) {
			delete_option( 'burst_php_error_time' );
			delete_option( 'burst_php_error_detected' );
			delete_option( 'burst_php_error_count' );
		}
	}

	/**
	 * Get a random referrer for the demo data setup
	 */
	private function get_random_referrer(): string {
		$referrers = [
			'https://www.google.com',
			'https://duckduckgo.com',
			'https://bing.com',
			'https://burst-statistics.com',
		];
		return $referrers[ array_rand( $referrers ) ];
	}

	/**
	 * Install demo data in Burst if blueprint.json is active
	 */
	public function install_demo_data(): void {
		// check if database installed.
		if ( ! $this->table_exists( 'burst_statistics' ) ) {
			return;
		}

		global $wpdb;

		$data = [
			[
				'name' => 'Chrome',
			],
			[
				'name' => 'Safari',
			],
			[
				'name' => 'Firefox',
			],
		];
		$this->insert_row( 'browsers', $data );

		$data = [
			[
				'name' => 'desktop',
			],
			[
				'name' => 'mobile',
			],
			[
				'name' => 'tablet',
			],
		];
		$this->insert_row( 'devices', $data );

		$data = [
			[
				'name' => 'Windows',
			],
			[
				'name' => 'MacOS',
			],
			[
				'name' => 'Linux',
			],
		];
		$this->insert_row( 'platforms', $data );
		// get all demo pages.
		$posts           = get_posts(
			[
				'post_type'      => [ 'page', 'post' ],
				'post_status'    => 'publish',
				'posts_per_page' => -1,
			]
		);
		$start_date_unix = time();
		$total_days      = 30;
		// we're walking back in time, so to get an increasing nr of pageviews, we decrease the max views each day.
		$max_views = 500;
		for ( $i = 0; $i < $total_days; $i++ ) {
			$stats_date_unix = $start_date_unix - ( $i * DAY_IN_SECONDS );
			$max_views      -= $i * 2;
			$min_views       = 10;
			if ( $max_views <= $min_views ) {
				$max_views = $min_views + 5;
			}
			foreach ( $posts as $post ) {
				$post_id = $post->ID;

				$page_url     = str_replace( home_url(), '', get_permalink( $post_id ) );
				$visitors     = random_int( $min_views, $max_views );
				$values       = [];
				$placeholders = [];

				for ( $j = 0; $j < $visitors; $j++ ) {
					$uid          = random_int( 1, 1000 );
					$bounce       = random_int( 0, 1 );
					$browser_id   = random_int( 1, 3 );
					$device_id    = random_int( 1, 3 );
					$platform_id  = random_int( 1, 3 );
					$time_on_page = wp_rand( 20, 3 * MINUTE_IN_SECONDS );
					$referrer     = $this->get_random_referrer();

					$wpdb->insert(
						"{$wpdb->prefix}burst_sessions",
						[
							'referrer'          => $referrer,
							'first_visited_url' => '/',
							'last_visited_url'  => '/',
							'browser_id'        => $browser_id,
							'device_id'         => $device_id,
							'platform_id'       => $platform_id,
							'bounce'            => $bounce,
							'first_time_visit'  => 1,
						],
						[ '%s', '%s', '%s', '%d', '%d', '%d', '%d', '%d' ]
					);

					$session_id = $wpdb->insert_id;

					$placeholders[] = '(%d, %s, %d, %d, %d)';
					$values         = array_merge(
						$values,
						[
							$stats_date_unix,
							$page_url,
							$uid,
							$time_on_page,
							$session_id,
						]
					);
				}

				$query = "
					INSERT INTO {$wpdb->prefix}burst_statistics
					(time, page_url, uid, time_on_page, session_id)
					VALUES " . implode( ', ', $placeholders );
                // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- values are prepared.
				$wpdb->query( $wpdb->prepare( $query, ...$values ) );
			}
		}
	}

	/**
	 * Activation processing
	 */
	public function activation(): void {
		if ( ! $this->has_admin_access() ) {
			return;
		}

		if ( get_option( 'burst_run_activation' ) ) {
			do_action( 'burst_activation' );
			update_option( 'burst_run_activation', false );
		}
	}

	/**
	 * Compile js file from settings and javascript so we can prevent inline variables
	 */
	public function create_js_file(): void {
		if ( ! $this->user_can_manage() ) {
			return;
		}

		$privacy_level   = $this->get_option( 'privacy_level', 'cookie' );
		$cookieless      = ( $privacy_level === 'fingerprint' );
		$cookieless_text = $cookieless ? '-cookieless' : '';
		$localize_args   = \Burst\burst_loader()->frontend->tracking->get_options();

		$js = 'let burst = ' . wp_json_encode( $localize_args ) . ';';

		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
		$js                .= file_get_contents( BURST_PATH . "assets/js/build/burst$cookieless_text.min.js" );
		$ghost_mode_enabled = apply_filters( 'burst_obfuscate_filename', $this->get_option_bool( 'ghost_mode' ) );
		$filename           = $this->get_frontend_js_filename( $ghost_mode_enabled );
		$upload_dir         = $this->upload_dir( 'js', $ghost_mode_enabled );
		$file               = $upload_dir . $filename;

		// copy timeme script to uploads dir if ghost mode is enabled.
		$timeme_copy_failed = false;
		if ( $ghost_mode_enabled ) {
			$js                      = $this->strip_window_exports( $js );
			$timeme_original_file    = BURST_PATH . 'assets/js/timeme/timeme.min.js';
			$timeme_obfustcated_file = $upload_dir . 'timeme.min.js';
			// also refresh an existing copy when the bundled file is newer, e.g. after a plugin update.
			if ( ! file_exists( $timeme_obfustcated_file ) || filemtime( $timeme_original_file ) > filemtime( $timeme_obfustcated_file ) ) {
				$timeme_copy_failed = ! copy( $timeme_original_file, $timeme_obfustcated_file );
			}
		}

		require_once ABSPATH . 'wp-admin/includes/file.php';
		global $wp_filesystem;
		$written = false;
		if ( WP_Filesystem() && $wp_filesystem->is_dir( $upload_dir ) && $wp_filesystem->is_writable( $upload_dir ) ) {
			// Write to a temp file and rename it into place, so a request served
			// mid-write never loads a truncated script. The temp name is unique per
			// call: concurrent runs (e.g. parallel requests during an upgrade) would
			// otherwise rename each other's temp file away mid-write.
			$temp_file = $file . '.' . uniqid( '', true ) . '.tmp';
			$written   = (bool) $wp_filesystem->put_contents( $temp_file, $js, FS_CHMOD_FILE );
			if ( $written ) {
				$written = $wp_filesystem->move( $temp_file, $file, true );
			}
			if ( ! $written && $wp_filesystem->exists( $temp_file ) ) {
				$wp_filesystem->delete( $temp_file );
			}
		}

		// Only clear the error flag once everything is actually on disk: put_contents()
		// can fail with a writable directory (e.g. an unwritable existing file), and
		// WP_Filesystem() itself can fail (e.g. FS_METHOD ftpext without credentials).
		if ( $written && ! $timeme_copy_failed ) {
			delete_option( 'burst_js_write_error' );
			$this->delete_js_files( $ghost_mode_enabled );
		} else {
			update_option( 'burst_js_write_error', true, false );
		}
	}

	/**
	 * Delete generated tracking JS files from the uploads directory.
	 *
	 * @param bool|null $keep_ghost_mode When a mode is passed, that mode's files are kept and
	 *                                   only the other mode's files are removed, so toggling
	 *                                   ghost mode does not leave orphaned public files behind.
	 *                                   Null removes the files of both modes.
	 */
	public function delete_js_files( ?bool $keep_ghost_mode = null ): void {
		if ( ! $this->user_can_manage() ) {
			return;
		}

		$files = [];
		if ( $keep_ghost_mode !== true ) {
			$files[] = $this->upload_dir( 'js', true ) . $this->get_frontend_js_filename( true );
			$files[] = $this->upload_dir( 'js', true ) . 'timeme.min.js';
		}
		if ( $keep_ghost_mode !== false ) {
			$files[] = $this->upload_dir( 'js' ) . $this->get_frontend_js_filename( false );
		}
		foreach ( $files as $file ) {
			if ( file_exists( $file ) ) {
				wp_delete_file( $file );
			}
		}
	}

	/**
	 * Create the js file for a newly created multisite site, in the context of that site.
	 *
	 * @param \WP_Site $new_site The site being initialized.
	 */
	public function create_js_file_for_new_site( \WP_Site $new_site ): void {
		switch_to_blog( (int) $new_site->blog_id );
		$this->create_js_file();
		restore_current_blog();
	}

	/**
	 * Schedule a combined js file refresh when an ecommerce integration plugin is (de)activated.
	 *
	 * Deferred to a single cron event on purpose: during the (de)activation request itself the
	 * baked should_load_ecommerce value would be stale — the ecommerce tracking filter was
	 * registered from the pre-change plugin state, and plugin_is_active() checks constants
	 * that are still loaded while a plugin is being deactivated.
	 *
	 * @param string $plugin Plugin file as passed by the (de)activated_plugin hooks.
	 */
	public function schedule_js_file_refresh_on_plugin_change( string $plugin ): void {
		if ( ! \Burst\burst_loader()->integrations->is_ecommerce_integration_plugin( $plugin ) ) {
			return;
		}

		if ( ! wp_next_scheduled( 'burst_create_js_file' ) ) {
			wp_schedule_single_event( time() + 10, 'burst_create_js_file' );
		}
	}

	/**
	 * Strip window export statements from JavaScript file.
	 *
	 * @param string $js_content The JavaScript content.
	 * @return string The processed JavaScript content.
	 */
	private function strip_window_exports( string $js_content ): string {
		$pattern    = '/,window\.burst_uid\s*=\s*burst_uid\s*,\s*window\.burst_use_cookies\s*=\s*burst_use_cookies\s*,\s*window\.burst_fingerprint\s*=\s*burst_fingerprint\s*,\s*window\.burst_update_hit\s*=\s*burst_update_hit\s*;?/';
		$js_content = preg_replace( $pattern, '', $js_content );
		return preg_replace( '/\n{3,}/', "\n\n", $js_content );
	}

	/**
	 * On Multisite site creation, run table init hook as well.
	 */
	public function run_table_init_hook(): void {
		// if already running, exit.
		if ( defined( 'BURST_INSTALL_TABLES_RUNNING' ) ) {
			return;
		}

		define( 'BURST_INSTALL_TABLES_RUNNING', true );

		// don't run on uninstall.
		if ( defined( 'BURST_UNINSTALLING' ) ) {
			return;
		}

		if ( ! $this->acquire_upgrade_lock() ) {
			return;
		}

		do_action( 'burst_install_tables' );
		// we need to run table creation across subsites as well.
		if ( is_multisite() ) {
			$sites = get_sites();
			if ( count( $sites ) > 0 ) {
				foreach ( $sites as $site ) {
					switch_to_blog( (int) $site->blog_id );
					do_action( 'burst_install_tables' );
					restore_current_blog();
				}
			}
		}
		$this->release_upgrade_lock();
	}

	/**
	 * Add some privacy info.
	 */
	public function add_privacy_info(): void {
		if ( ! function_exists( 'wp_add_privacy_policy_content' ) ) {
			return;
		}
		$content = sprintf(
		// translators: 1: opening anchor tag to the privacy statement, 2: closing anchor tag.
			__( 'This website uses Burst Statistics, a Privacy-Friendly Statistics Tool to analyze visitor behavior. For this functionality we (this website) collect anonymized data, stored locally without sharing it with other parties. For more information, please read the %1$s Privacy Statement %2$s from Burst.', 'burst-statistics' ),
			'<a href="https://burst-statistics.com/legal/privacy-statement/" target="_blank">',
			'</a>'
		);
		wp_add_privacy_policy_content(
			'Burst Statistics',
			wp_kses_post( wpautop( $content, false ) )
		);
	}

	/**
	 * Setup default settings used for tracking
	 */
	public function setup_defaults(): void {
		if ( get_option( 'burst_set_defaults' ) ) {
			update_option( 'burst_activation_time', time(), false );
			update_option( 'burst_last_cron_hit', time(), false );
			// Fresh install: download the GeoIP database (the burst_locations country
			// lookup is seeded by install_locations_table). No "available from" notice
			// is shown on a fresh install — only on an upgrade from an existing site.
			update_option( 'burst_import_geo_ip_on_activation', true, false );
			$this->update_option( 'combine_vars_and_script', true );
			$this->update_option( 'enable_turbo_mode', true );
			$this->update_option( 'privacy_level', 'cookie' );
			$this->update_option( 'plugin_update_suggestions', true );
			$this->create_js_file();

			$this->tasks->add_initial_tasks();
			flush_rewrite_rules();
			if ( ! $this->table_exists( 'burst_goals' ) ) {
				return;
			}

			// set default goal.
			// if there is no default goal, then insert one.
			$goals = ( new Goals() )->get_goals();
			$count = count( $goals );
			if ( $count === 0 ) {
				$goal        = new Goal();
				$goal->title = __( 'Default goal', 'burst-statistics' );
				$goal->save();
			}
			delete_option( 'burst_set_defaults' );
		}
	}

	/**
	 * Add custom links (Settings, Support, Upgrade) to the plugin actions row on the Plugins page.
	 *
	 * @hook plugin_action_links_$plugin
	 * @param array<int, string> $links An array of existing plugin action links.
	 * @return array<int, string> Modified array with additional plugin links.
	 */
	public function plugin_settings_link( array $links ): array {
		// Add "Upgrade to Pro" link at the start if not Pro version.
		if ( ! defined( 'BURST_PRO' ) ) {
			$upgrade_link
				= '<a style="color:#2e8a37;font-weight:bold" target="_blank" href="' . $this->get_website_url( 'pricing', [ 'utm_source' => 'plugin-overview' ] ) . '">'
					. __( 'Upgrade to Pro', 'burst-statistics' ) . '</a>';
			array_unshift( $links, $upgrade_link );
		}

		// Get menu links from configuration and add them after upgrade.
		$menu_links = $this->get_menu_links_from_config();
		foreach ( array_reverse( $menu_links ) as $menu_link ) {
			array_unshift( $links, $menu_link );
		}

		// Add support link at the end.
		$support_link = defined( 'BURST_FREE' )
			? 'https://wordpress.org/support/plugin/burst-statistics'
			: $this->get_website_url(
				'support',
				[
					'utm_source'  => 'plugin-overview',
					'utm_content' => 'support-link',
				]
			);
		$faq_link     = '<a target="_blank" href="' . $support_link . '">'
						. __( 'Support', 'burst-statistics' ) . '</a>';
		array_push( $links, $faq_link );

		return $links;
	}

	/**
	 * Get menu links from menu configuration for plugin action links
	 *
	 * @return array<int, string> Array of menu links HTML
	 */
	private function get_menu_links_from_config(): array {
		$menu_config = $this->app->menu->get();
		$menu_links  = [];

		foreach ( $menu_config as $menu_item ) {
			// Skip items that shouldn't appear in WordPress admin menu.
			if ( ! isset( $menu_item['show_in_plugin_overview'] ) || ! $menu_item['show_in_plugin_overview'] ) {
				continue;
			}

			// Check user capabilities.
			$capability = $menu_item['capabilities'] ?? 'view_burst_statistics';
			if ( ! current_user_can( $capability ) ) {
				continue;
			}

			$title     = $menu_item['title'] ?? '';
			$menu_slug = $menu_item['menu_slug'] ?? 'burst';
			$css_class = 'burst-' . ( $menu_item['id'] ?? 'menu' ) . '-link';

			$menu_links[] = '<a href="'
							. admin_url( 'admin.php?page=' . $menu_slug )
							. '" class="' . esc_attr( $css_class ) . '">'
							. esc_html( $title ) . '</a>';
		}

		return $menu_links;
	}

	/**
	 * Check if the current day falls within the Black Friday week.
	 */
	public static function is_bf(): bool {
		// Use current GMT timestamp.
		$now = time();
		// Get current year.
		$year = gmdate( 'Y' );

		// Calculate Thanksgiving.
		$thanksgiving = strtotime( "fourth thursday of november $year" );
		// Black Friday is the day after Thanksgiving.
		$black_friday = strtotime( '+1 day', $thanksgiving );

		// Monday before Black Friday.
		$start = strtotime( '-4 days', $black_friday );
		// Saturday after Black Friday, end of day.
		$end = strtotime( '+1 day 23:59:59', $black_friday );

		return ( $now >= $start && $now <= $end );
	}

	/**
	 * Check if this is the Cyber Monday period.
	 */
	public static function is_cm(): bool {
		// Use current GMT timestamp.
		$now = time();
		// Get current year.
		$year = gmdate( 'Y' );

		// Calculate Thanksgiving.
		$thanksgiving = strtotime( "fourth thursday of november $year" );
		// Black Friday is the day after Thanksgiving.
		$black_friday = strtotime( '+1 day', $thanksgiving );

		// Sunday after Black Friday.
		$start = strtotime( '+2 days', $black_friday );
		// Cyber Monday, end of day.
		$end = strtotime( '+3 days 23:59:59', $black_friday );

		return ( $now >= $start && $now <= $end );
	}

	/**
	 * Add a button and thickbox to deactivate the plugin
	 *
	 * @since  1.0
	 * @access public
	 */
	public function deactivate_popup(): void {
		// only on plugins page.
		$screen = get_current_screen();
		if ( empty( $screen ) || ( $screen->base !== 'plugins' && $screen->base !== 'plugins-network' ) ) {
			return;
		}
		$networkwide = $screen->base === 'plugins-network';
		$slug        = sanitize_title( BURST_PLUGIN_NAME );
		?>
		<?php add_thickbox(); ?>
		<style>

			#TB_ajaxContent.burst-deactivation-popup {
				text-align: center !important;
			}

			#TB_window.burst-deactivation-popup {
				height: min-content !important;
				margin-top: initial !important;
				margin-left: initial !important;
				display: flex;
				flex-direction: column;
				top: 50% !important;
				left: 50%;
				transform: translate(-50%, -50%);
				width: 500px !important;
				border-radius: 12px !important;
				min-width: min-content;
			}

			.burst-deactivation-popup #TB_title {
				padding-bottom: 20px;
				border-radius: 12px;
				border-bottom: none !important;
				background: #fff !important;
			}

			.burst-deactivation-popup #TB_ajaxWindowTitle {
				font-weight: bold;
				font-size: 20px;
				padding: 20px;
				background: #fff !important;
				border-radius: 12px 12px 0 0;
				width: calc(100% - 40px);
			}

			.burst-deactivation-popup .tb-close-icon {
				color: #333;
				width: 25px;
				height: 25px;
				top: 12px;
				right: 20px;
			}

			.burst-deactivation-popup .tb-close-icon:before {
				font: normal 25px/25px dashicons;
			}

			.burst-deactivation-popup #TB_closeWindowButton:focus .tb-close-icon {
				outline: 0;
				color: #666;
			}

			.burst-deactivation-popup #TB_closeWindowButton .tb-close-icon:hover {
				color: #666;
			}

			.burst-deactivation-popup #TB_closeWindowButton:focus {
				outline: 0;
			}

			.burst-deactivation-popup #TB_ajaxContent {
				width: 90% !important;
				height: initial !important;
				padding-left: 20px !important;
			}

			.burst-deactivation-popup .button-burst-tertiary.button {
				background-color: #D7263D !important;
				color: white !important;
				border-color: #D7263D;
			}

			.burst-deactivation-popup .button-burst-tertiary.button:hover {
				background-color: #f1f1f1 !important;
				color: #d7263d !important;
			}

			.burst-deactivate-notice-content h3, .burst-deactivate-notice-content ul {
			}

			.burst-deactivate-notice-footer {
				display: flex;
				gap: 10px;
				padding: 15px 10px 0 10px;
			}

			.burst-deactivation-popup ul {
				list-style: disc;
				padding-left: 20px;
			}

			.burst-deactivate-notice-footer .button {
				min-width: fit-content;
				white-space: nowrap;
				cursor: pointer;
				text-decoration: none;
				text-align: center;
			}
		</style>
		<script>
			jQuery(document).ready(function($) {
				$('#burst_close_tb_window').click(tb_remove);
				<?php //phpcs:ignore ?>
				$(document).on('click', '#deactivate-<?php echo $slug; ?>', function(e) {
					e.preventDefault();
					tb_show('', '#TB_inline?height=420&inlineId=deactivate_and_delete_data', 'null');
					$('#TB_window').addClass('burst-deactivation-popup');

				});
				<?php //phpcs:ignore ?>
				if ($('#deactivate-<?php echo $slug; ?>').length) {
					<?php //phpcs:ignore ?>
					$('.burst-button-deactivate').attr('href', $('#deactivate-<?php echo $slug; ?>').attr('href'));
				}

			});
		</script>

		<div id="deactivate_and_delete_data" style="display: none;">
			<div class="burst-deactivate-notice-content">
				<h4 style="margin:0 0 20px 0; text-align: left; font-size: 1.1em;">
					<?php esc_html_e( 'Are you sure? With Burst Statistics active:', 'burst-statistics' ); ?></h4>
				<ul style="text-align: left;">
					<li><?php esc_html_e( 'You have access to your analytical data', 'burst-statistics' ); ?></li>
					<li><?php esc_html_e( 'Asking consent for statistics often not required.', 'burst-statistics' ); ?></li>
					<li><?php esc_html_e( 'Your data securely on your own server.', 'burst-statistics' ); ?></li>
				</ul>
			</div>

			<?php
			$token                              = wp_create_nonce( 'burst_deactivate_plugin' );
			$deactivate_and_remove_all_data_url = add_query_arg(
				[
					'action'      => 'uninstall_delete_all_data',
					'networkwide' => $networkwide ? '1' : '0',
					'token'       => $token,
				],
				admin_url( 'plugins.php' )
			);
			?>
			<div class="burst-deactivate-notice-footer">
				<a class="button button-default" href="#"
					id="burst_close_tb_window"><?php esc_html_e( 'Cancel', 'burst-statistics' ); ?></a>
				<a class="button button-primary burst-button-deactivate"
					href="#"><?php esc_html_e( 'Deactivate', 'burst-statistics' ); ?></a>
				<a class="button burst-button-deactivate-delete button-burst-tertiary"
					href="<?php echo esc_url( $deactivate_and_remove_all_data_url ); ?>"><?php esc_html_e( 'Deactivate and delete all data', 'burst-statistics' ); ?></a>
			</div>
		</div>
		<?php
	}

	/**
	 * Deactivate the plugin, based on made choice regarding data
	 */
	public function listen_for_deactivation(): void {
		// check user role.
		if ( ! current_user_can( 'activate_plugins' ) ) {
			return;
		}

		// check nonce. ignore phpcs: no data is stored here. only verified.
		//phpcs:ignore
		if ( ! isset( $_GET['token'] ) || ( ! $this->verify_nonce( $_GET['token'], 'burst_deactivate_plugin' ) ) ) {
			return;
		}

		// check for action.
		//phpcs:ignore
		if ( isset( $_GET['action'] ) && $_GET['action'] === 'uninstall_delete_all_data' ) {
			define( 'BURST_NO_UPGRADE', true );
			define( 'BURST_UNINSTALLING', true );
			\Burst\burst_clear_scheduled_hooks();

			//phpcs:ignore
			$networkwide = isset( $_GET['networkwide'] ) && $_GET['networkwide'] === '1';
			if ( $networkwide && is_multisite() ) {
				$sites = get_sites();
				if ( count( $sites ) > 0 ) {
					foreach ( $sites as $site ) {
						switch_to_blog( (int) $site->blog_id );
						$this->delete_all_burst_data();
						// Before delete_all_burst_configuration(), which removes the capability its guard checks.
						$this->delete_js_files();
						$this->delete_all_burst_configuration();
						restore_current_blog();
					}
				}
			}
			$this->delete_all_burst_data();
			// Before delete_all_burst_configuration(), which removes the capability its guard checks.
			$this->delete_js_files();
			$this->delete_all_burst_configuration();
			\Burst\burst_clear_scheduled_hooks();
			$this->delete_all_burst_cron_events();

			deactivate_plugins( BURST_PLUGIN, false, $networkwide );
			$redirect_slug = $networkwide ? 'network/plugins.php' : 'plugins.php';

			wp_safe_redirect( admin_url( $redirect_slug ) );
			exit;
		}
	}

	/**
	 * Delete all scheduled cron events related to Burst, including single events.
	 */
	private function delete_all_burst_cron_events(): void {
		$crons = _get_cron_array();

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

		foreach ( $crons as $timestamp => $cron ) {
			foreach ( $cron as $hook => $events ) {
				if ( str_starts_with( $hook, 'burst_' ) ) {
					foreach ( $events as $event ) {
						wp_unschedule_event( $timestamp, $hook, $event['args'] );
					}
				}
			}
		}
	}

	/**
	 * Clear all data from the reset button in the settings.
	 *
	 * @param array  $output The initial output array.
	 * @param string $action The action to perform.
	 * @param ?array $data   Additional data for processing.
	 * @return array<string, mixed> The modified output array.
	 */
	public function maybe_delete_all_data( array $output, string $action, ?array $data ): array {
		// fix phpcs warning.
		unset( $data );
		if ( ! $this->user_can_manage() ) {
			return $output;
		}

		if ( $action === 'reset' ) {
			$this->reset();
			$output = [
				'success' => true,
				'message' => __( 'Successfully cleared data.', 'burst-statistics' ),
			];
		}

		return $output;
	}

	/**
	 * Reset data. Used by the WP CLI command and react reset button
	 */
	public function reset(): void {
		if ( ! $this->user_can_manage() ) {
			return;
		}
		// delete everything.
		$this->delete_all_burst_data();

		// immediately run setup defaults, so db tables get made.
		$this->setup_defaults();

		// ensure the tables are created.
		$this->release_upgrade_lock();
		$this->run_table_init_hook();
	}

	/**
	 * Clear plugin data
	 */
	public function delete_all_burst_data(): void {
		if ( ! $this->user_can_manage() ) {
			return;
		}

		global $wpdb;

		// tables to delete.
		$table_names = $this->get_table_list();

		// delete tables.
		foreach ( $table_names as $table_name ) {
			// guard against deleting tables from other plugins, as these can be added using the tables filter.
			if ( ! str_starts_with( $table_name, 'burst_' ) ) {
				continue;
			}
			$sql = "DROP TABLE IF EXISTS {$wpdb->prefix}$table_name";
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- table name is from a predefined list.
			$wpdb->query( $sql );
		}
	}

	/**
	 * Delete all options and capabilities
	 */
	public function delete_all_burst_configuration(): void {
		if ( ! current_user_can( 'activate_plugins' ) ) {
			return;
		}

		global $wp_roles;
		global $wpdb;

		// capabilities to delete.
		$roles        = $wp_roles->roles;
		$capabilities = [
			'manage_burst_statistics',
			'view_burst_statistics',
		];

		// delete user capabilities from all user roles.
		foreach ( $roles as $role_name => $role_info ) {
			foreach ( $capabilities as $capability ) {
				$wp_roles->remove_cap( $role_name, $capability );
			}
		}

		// delete all burst options.
		$wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE 'burst\_%'" );
		if ( is_multisite() ) {
			$wpdb->query( "DELETE FROM {$wpdb->sitemeta} WHERE meta_key LIKE 'burst\_%'" );
			$sites = get_sites( [ 'fields' => 'ids' ] );
			foreach ( $sites as $site_id ) {
				switch_to_blog( $site_id );
				$wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE 'burst\_%'" );
				restore_current_blog();
			}
		}

		// get all burst transients.
		$results = $wpdb->get_results(
			"SELECT `option_name` AS `name`, `option_value` AS `value`
                                FROM  $wpdb->options
                                WHERE `option_name` LIKE '%transient_burst%'
                                ORDER BY `option_name`",
			'ARRAY_A'
		);
		// loop through all burst transients and delete.
		foreach ( $results as $key => $value ) {
			$transient_name = substr( $value['name'], 11 );
			delete_transient( $transient_name );
		}
	}

	/**
	 * Drop tables during the site deletion.
	 *
	 * @param array $tables  The tables to drop.
	 * @param int   $blog_id The site ID.
	 * @return array<int, string> The modified list of tables to drop.
	 */
	public function ms_remove_tables(
		array $tables,
		int $blog_id
	): array {
		global $wpdb;

		$table_names = $this->get_table_list();
		foreach ( $table_names as $table_name ) {
			if ( ! str_starts_with( $table_name, 'burst_' ) ) {
				continue;
			}
			$tables[] = $wpdb->get_blog_prefix( $blog_id ) . $table_name;
		}

		return $tables;
	}

	/**
	 * Add ecommerce menu item to the admin menu
	 *
	 * @param array $menu_items The existing menu items.
	 * @return array The modified menu items including the ecommerce menu item.
	 */
	public function add_ecommerce_menu_item( array $menu_items ): array {
		if ( ! $this->has_admin_access() ) {
			return $menu_items;
		}

		$should_load_ecommerce = \Burst\burst_loader()->integrations->should_load_ecommerce();
		if ( ! $should_load_ecommerce ) {
			return $menu_items;
		}

		$ecommerce_menu_item = [
			'id'             => 'sales',
			'title'          => __( 'Sales', 'burst-statistics' ),
			'default_hidden' => false,
			'menu_items'     => [],
			'capabilities'   => 'view_sales_burst_statistics',
			'menu_slug'      => 'burst#/sales',
			'show_in_admin'  => true,
			'pro'            => true,
			'shareable'      => true,
		];

		// Put ecommerce menu item before the id: settings menu item.
		$settings_index = null;
		foreach ( $menu_items as $index => $item ) {
			if ( isset( $item['id'] ) && 'reporting' === $item['id'] ) {
				$settings_index = $index;
				break;
			}
		}

		if ( null !== $settings_index ) {
			array_splice( $menu_items, $settings_index, 0, [ $ecommerce_menu_item ] );
		} else {
			$menu_items[] = $ecommerce_menu_item;
		}

		return $menu_items;
	}

	/**
	 * Clean up orphaned block goals that were created but never saved (or deleted without trace).
	 */
	public function cleanup_orphaned_block_goals(): void {
		global $wpdb;
		$table_name   = $wpdb->prefix . 'burst_goals';
		$table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) ) === $table_name;
		if ( ! $table_exists ) {
			return;
		}

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$block_goals = $wpdb->get_results( "SELECT * FROM {$table_name} WHERE block_goal = 1", ARRAY_A );
		if ( empty( $block_goals ) ) {
			return;
		}

		// Query only post_content from non-trash, non-revision posts/pages.
		$post_statuses_escaped = [];
		foreach ( [ 'publish', 'draft', 'pending', 'private', 'future' ] as $status ) {
			$post_statuses_escaped[] = (string) esc_sql( $status );
		}
		$post_statuses_str = "'" . implode( "', '", $post_statuses_escaped ) . "'";

		$post_types = get_post_types( [ 'public' => true ] );
		if ( empty( $post_types ) || ! is_array( $post_types ) ) {
			$post_types = [ 'post', 'page' ];
		}
		$post_types_escaped = [];
		foreach ( $post_types as $type ) {
			if ( is_string( $type ) ) {
				$post_types_escaped[] = (string) esc_sql( $type );
			}
		}
		$post_types_str = "'" . implode( "', '", $post_types_escaped ) . "'";

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$posts          = $wpdb->get_col( "SELECT post_content FROM {$wpdb->posts} WHERE post_status IN ($post_statuses_str) AND post_type IN ($post_types_str) AND post_content LIKE '%data-burst-goal=%'" );
		$merged_content = is_array( $posts ) ? implode( ' ', $posts ) : '';

		foreach ( $block_goals as $goal ) {
			// Skip goals created within the last 3 hours to avoid race conditions while editing.
			if ( time() - (int) $goal['date_created'] < 3 * HOUR_IN_SECONDS ) {
				continue;
			}

			$goal_id = (int) $goal['ID'];
			$uid     = '';
			if ( preg_match( '/data-burst-goal="([^"]+)"/', $goal['selector'], $matches ) ) {
				$uid = $matches[1];
			}
			if ( empty( $uid ) ) {
				continue;
			}

			if ( strpos( $merged_content, $uid ) === false ) {
				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
				$has_data = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}burst_goal_statistics WHERE goal_id = %d", $goal_id ) ) > 0;

				if ( ! $has_data ) {
					// Delete goal completely if it has no stats.
					$goal_obj = new \Burst\Frontend\Goals\Goal( $goal_id );
					$goal_obj->delete();
				} elseif ( $goal['status'] !== 'inactive' ) {
					// Otherwise deactivate it to preserve stats.
					// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
					$wpdb->update( $table_name, [ 'status' => 'inactive' ], [ 'ID' => $goal_id ], [ '%s' ], [ '%d' ] );
					wp_cache_delete( 'burst_goal_' . $goal_id, 'burst' );
				}
			}
		}

		// Ensure updates are synchronized.
		do_action( 'burst_after_updated_goals' );
	}
}
