<?php

use SlimStat\Services\GeoService;
use SlimStat\Components\DateRangeHelper;
use SlimStat\Services\Admin\Notification\NotificationFactory;
class wp_slimstat_admin
{
    public static $screens_info      = [];
    public static $config_url        = '';
    public static $current_screen    = 'slimview1';
    public static $page_location     = 'slimstat';
    public static $meta_user_reports = [];
    public static $settings = [];
    public static $user_reports = [];
    public static $admin_notice    = '';
    public static $main_menu_slug = 'slimview1';

    protected static $data_for_column = [
        'url'   => [],
        'sql'   => [],
        'count' => [],
    ];

    /**
     * Init -- Sets things up.
     */
    public static function init()
    {
        // Redirect to the pro settings
        add_action('admin_menu', function () {
            if (is_admin() && isset($_GET['page']) && 'slimpro' === $_GET['page'] && wp_slimstat::pro_is_installed()) {
                wp_safe_redirect(admin_url('admin.php?page=slimconfig&tab=7'));
                exit();
            }
        });

        // Action for reset layout
        add_action('admin_post_slimstat_reset_layout', ['wp_slimstat_admin', 'handle_reset_layout']);

        // Define the default screens
        $has_network_reports = get_user_option('meta-box-order_slimstat_page_slimlayout-network', 1);

        self::$screens_info = [
            'slimview1' => [
                'is_report_group' => true,
                'show_in_sidebar' => true,
                'title'           => __('Real-time', 'wp-slimstat'),
                'capability'      => 'can_view',
                'callback'        => [self::class, 'wp_slimstat_include_view'],
            ],
            'slimview2' => [
                'is_report_group' => true,
                'show_in_sidebar' => true,
                'title'           => __('Overview', 'wp-slimstat'),
                'capability'      => 'can_view',
                'callback'        => [self::class, 'wp_slimstat_include_view'],
            ],
            'slimview3' => [
                'is_report_group' => true,
                'show_in_sidebar' => true,
                'title'           => __('Audience', 'wp-slimstat'),
                'capability'      => 'can_view',
                'callback'        => [self::class, 'wp_slimstat_include_view'],
            ],
            'slimview4' => [
                'is_report_group' => true,
                'show_in_sidebar' => true,
                'title'           => __('Site Analysis', 'wp-slimstat'),
                'capability'      => 'can_view',
                'callback'        => [self::class, 'wp_slimstat_include_view'],
            ],
            'slimview5' => [
                'is_report_group' => true,
                'show_in_sidebar' => true,
                'title'           => __('Traffic Sources', 'wp-slimstat'),
                'capability'      => 'can_view',
                'callback'        => [self::class, 'wp_slimstat_include_view'],
            ],
            'slimemail' => [
                'is_report_group' => false,
                'show_in_sidebar' => true,
                'title'           => wp_slimstat::pro_is_installed() ? __('Email Report', 'wp-slimstat') : __('Email Report (pro)', 'wp-slimstat'),
                'capability'      => 'can_view',
                'callback'        => [self::class, 'wp_slimstat_include_email_report'],
            ],
            'slimlayout' => [
                'is_report_group' => false,
                'show_in_sidebar' => true,
                'title'           => __('Customize', 'wp-slimstat'),
                'capability'      => 'can_customize',
                'callback'        => [self::class, 'wp_slimstat_include_layout'],
            ],
            'slimconfig' => [
                'is_report_group' => false,
                'show_in_sidebar' => true,
                'title'           => __('Settings', 'wp-slimstat'),
                'capability'      => 'can_admin',
                'callback'        => [self::class, 'wp_slimstat_include_config'],
            ],
            'slimpro' => [
                'is_report_group' => false,
                'show_in_sidebar' => current_user_can('manage_options'),
                'title'           => apply_filters('slimstat_upgrade_to_pro_title', __('Upgrade to Pro', 'wp-slimstat')),
                'capability'      => 'can_admin',
                'callback'        => [self::class, 'wp_slimstat_pro'],
            ],
            'dashboard' => [
                'is_report_group' => true,
                'show_in_sidebar' => false,
                'title'           => __('WordPress Dashboard', 'wp-slimstat'),
                'capability'      => '',
                'callback'        => '', // No callback and capabilities are needed if show_in_sidebar is false
            ],
            'inactive' => [
                'is_report_group' => true,
                'show_in_sidebar' => false,
                'title'           => __('Inactive Reports'),
                'capability'      => '',
                'callback'        => '', // No callback and capabilities are needed if show_in_sidebar is false
            ],
        ];
        self::$screens_info = apply_filters('slimstat_screens_info', self::$screens_info);

        // If the plugin was network activated, the tables might not have been created for this specific site
        $table_list = wp_slimstat::$wpdb->get_results(sprintf("SHOW TABLES LIKE '%sslim_stats'", $GLOBALS['wpdb']->prefix));
        if (empty($table_list)) {
            self::init_environment();
        }

        // Settings URL
        if (!is_network_admin()) {
            self::$config_url = get_admin_url($GLOBALS['blog_id'], 'admin.php?page=slimconfig&amp;tab=');
        } else {
            self::$config_url = network_admin_url('admin.php?page=slimconfig&amp;tab=');
        }

        // Current Screen
        if (!empty($_REQUEST['page']) && array_key_exists($_REQUEST['page'], self::$screens_info)) {
            self::$current_screen = $_REQUEST['page'];
        }

        // Page Location
        if ('no' != wp_slimstat::$settings['use_separate_menu']) {
            self::$page_location = 'admin';
        }

        // Is the menu position setting being updated?
        if (!empty($_POST['slimstat_update_settings']) && wp_verify_nonce($_POST['slimstat_update_settings'], 'slimstat_update_settings') && !empty($_POST['options']['use_separate_menu'])) {
            wp_slimstat::$settings['use_separate_menu'] = ('on' == $_POST['options']['use_separate_menu']) ? 'on' : 'no';
        }

        // Retrieve this user's custom report assignment (Customizer)
        // Superadmins can customize the layout at network level, to override per-site settings
        self::$meta_user_reports = get_user_option('meta-box-order_' . wp_slimstat_admin::$page_location . '_page_slimlayout-network', 1);

        // No network-wide settings found
        if (empty(self::$meta_user_reports)) {
            self::$meta_user_reports = get_user_option('meta-box-order_' . wp_slimstat_admin::$page_location . '_page_slimlayout', $GLOBALS['current_user']->ID);
        }

        // WPMU - New blog created
        $active_sitewide_plugins = get_site_option('active_sitewide_plugins');
        if (!empty($active_sitewide_plugins['wp-slimstat/wp-slimstat.php'])) {
            add_action('wpmu_new_blog', [self::class, 'new_blog']);
        }

        // WPMU - Blog Deleted
        add_filter('wpmu_drop_tables', [self::class, 'drop_tables'], 10, 2);

        // Display a notice that hightlights this version's features
        if (!empty($_GET['page']) && false !== strpos($_GET['page'], 'slimview') && (!empty(self::$admin_notice) && 'on' == wp_slimstat::$settings['notice_latest_news'] && is_super_admin())) {
            add_action('admin_notices', [self::class, 'show_latest_news']);

        }

        // Remove spammers from the database
        if ('on' == wp_slimstat::$settings['ignore_spammers']) {
            add_action('transition_comment_status', [self::class, 'remove_spam'], 15, 3);
        }

        // Add a menu to the admin bar
        if ('no' != wp_slimstat::$settings['use_separate_menu'] && is_admin_bar_showing()) {
            add_action('admin_bar_menu', [self::class, 'add_menu_to_adminbar'], 100);
            add_action('admin_enqueue_scripts', [self::class, 'enqueue_adminbar_styles']);
            add_action('wp_enqueue_scripts', [self::class, 'enqueue_adminbar_styles']);
        }

        if (function_exists('is_network_admin') && !is_network_admin()) {
            // Add the appropriate entries to the admin menu, if this user can view/admin  Slimstat
            add_action('admin_menu', [self::class, 'add_menus']);

            // Display the column in the Edit Posts / Pages screen
            if ('on' == wp_slimstat::$settings['add_posts_column']) {
                $post_types = get_post_types(['public' => true, 'show_ui' => true], 'names');
                include_once(plugin_dir_path(__FILE__) . 'view/wp-slimstat-reports.php');
                include_once(plugin_dir_path(__FILE__) . 'view/wp-slimstat-db.php');

                foreach ($post_types as $a_post_type) {
                    add_filter(sprintf('manage_%s_posts_columns', $a_post_type), [self::class, 'add_column_header']);
                    add_action(sprintf('manage_%s_posts_custom_column', $a_post_type), [self::class, 'add_post_column'], 10, 2);
                }

                if (false !== strpos($_SERVER['REQUEST_URI'], 'edit.php')) {
                    add_action('admin_enqueue_scripts', [self::class, 'wp_slimstat_stylesheet']);
                    add_action('wp', [self::class, 'init_data_for_column']);
                }
            }

            // Update the table structure and options, if needed
            if (!empty(wp_slimstat::$settings['version']) && SLIMSTAT_ANALYTICS_VERSION != wp_slimstat::$settings['version']) {
                add_action('admin_init', [self::class, 'update_tables_and_options']);
            }
        }

        // Initialize Reports system for SlimStat pages and AJAX requests
        $is_slimstat_page = (!empty($_GET['page']) && 0 === strpos($_GET['page'], 'slim'));
        $is_slimstat_ajax = (!empty($_POST['action']) && (
            'slimstat_load_report' === $_POST['action'] ||
            'slimstat_get_live_analytics_data' === $_POST['action']
        ));

        if ($is_slimstat_page || $is_slimstat_ajax) {
            // Initialize the new Reports system FIRST before legacy system loads
            \SlimStat\Reports\Bootstrap::get_instance()->init();
        }

        // Load the library of functions to generate the reports
        if ($is_slimstat_page || (!empty($_POST['action']) && 'slimstat_load_report' == $_POST['action'])) {
            include_once(plugin_dir_path(__FILE__) . 'view/wp-slimstat-reports.php');
            wp_slimstat_reports::init();

            if (!empty($_POST['report_id'])) {
                $report_id = sanitize_title($_POST['report_id'], 'slim_p0_00');

                if (!empty(wp_slimstat_reports::$reports[$report_id])) {
                    add_action('wp_ajax_slimstat_load_report', ['wp_slimstat_reports', 'callback_wrapper'], 10, 2);
                }
            }
        }

        // Dashboard Widgets
        if ('on' == wp_slimstat::$settings['add_dashboard_widgets']) {
            $sanitized_uri  = sanitize_url(wp_unslash($_SERVER['REQUEST_URI']));
            $request_length = strlen($sanitized_uri);
            $temp           = $request_length - 10;

            if (false !== strpos($sanitized_uri, '/wp-admin/index.php') || ($temp >= 0 && $temp <= $request_length && false !== strpos($sanitized_uri, '/wp-admin/', $temp))) {
                add_action('admin_enqueue_scripts', [self::class, 'wp_slimstat_enqueue_scripts']);
                add_action('admin_enqueue_scripts', [self::class, 'wp_slimstat_stylesheet']);
            }

            add_action('wp_dashboard_setup', [self::class, 'add_dashboard_widgets']);
        }

        // AJAX Handlers
        if (defined('DOING_AJAX') && DOING_AJAX) {
            $ajax_actions = [
                'slimstat_notice_latest_news'    => 'notices_handler',
                'slimstat_notice_geolite'        => 'notices_handler',
                'slimstat_notice_browscap'       => 'notices_handler',
                'slimstat_notice_caching'        => 'notices_handler',
                'slimstat_manage_filters'        => 'manage_filters',
                'slimstat_delete_pageview'       => 'delete_pageview',
                'slimstat_update_geoip_database' => 'update_geoip_database',
                'slimstat_check_geoip_database'  => 'check_geoip_database',
                'slimstat_get_filter_options'    => 'get_filter_options',
                'slimstat_get_online_visitors'   => 'get_online_visitors',
                'slimstat_get_adminbar_stats'    => 'get_adminbar_stats',
            ];
            foreach ($ajax_actions as $action => $handler) {
                add_action('wp_ajax_' . $action, [self::class, $handler]);
            }

            // Live Analytics AJAX handler is registered via init_hooks() in Bootstrap
            // No need to call it separately here - it's already registered
        }

        // Schedule a daily cron job to purge the data
        if (!wp_next_scheduled('wp_slimstat_purge')) {
            wp_schedule_event(time(), 'twicedaily', 'wp_slimstat_purge');
        }

        // Schedule a daily cron job to regenerate IP hashing salt (for GDPR compliance)
        if (!wp_next_scheduled('wp_slimstat_generate_daily_salt')) {
            wp_schedule_event(time(), 'daily', 'wp_slimstat_generate_daily_salt');
        }

        // Schedule a weekly cron job to update geoip database automatically
        if (!wp_next_scheduled('wp_slimstat_update_geoip_database')) {
            $nextRunInterval = wp_slimstat::get_schedule_interval('weekly');
            wp_schedule_event(time() + $nextRunInterval, 'weekly', 'wp_slimstat_update_geoip_database');
        }

        // Fallback: if WP-Cron is disabled or scheduling failed, trigger a non-blocking direct update
        // This ensures environments with DISABLE_WP_CRON still receive GeoIP database updates
        $cron_disabled = (defined('DISABLE_WP_CRON') && DISABLE_WP_CRON) || !wp_next_scheduled('wp_slimstat_update_geoip_database');
        $geoip_provider = \wp_slimstat::resolve_geolocation_provider();
        if ($cron_disabled && false !== $geoip_provider && is_admin() && !wp_doing_ajax()
            && current_user_can(\wp_slimstat::$settings['capability_can_admin'])) {
            // Update if DB is missing or last update is older than the most recent past scheduled window
            $last_update = (int) get_option('slimstat_last_geoip_dl', 0);

            // Calculate the most recent "first Tuesday + 2 days" that has already passed
            $this_month_update = strtotime('first Tuesday of this month') + (86400 * 2);
            $current_time = time();

            // If this month's update window hasn't arrived yet, use last month's window
            if ($current_time < $this_month_update) {
                $this_update = strtotime('first Tuesday of last month') + (86400 * 2);
            } else {
                $this_update = $this_month_update;
            }

            $needs_update = $last_update < $this_update;
            $db_missing = false;
            if (!$needs_update) {
                // Time check passed — only check DB existence if time says we're current
                try {
                    $uses_db = in_array($geoip_provider, \SlimStat\Services\GeoService::DB_PROVIDERS, true);
                    if ($uses_db) {
                        $service    = new \SlimStat\Services\Geolocation\GeolocationService($geoip_provider, []);
                        $db_missing = !file_exists($service->getProvider()->getDbPath());
                    }
                } catch (\Throwable $e) {
                    $db_missing = true;
                }
            }

            if ($needs_update || $db_missing) {
                // Fire admin-ajax in a non-blocking way to run the existing update handler
                $ajax_url = admin_url('admin-ajax.php');
                // Forward only WordPress authentication cookies for security
                $cookie_header = '';
                if (!headers_sent() && $_COOKIE !== [] && is_array($_COOKIE)) {
                    $pairs = [];
                    // Only forward WordPress authentication cookies
                    $allowed_cookie_prefixes = [
                        'wordpress_logged_in_',
                        'wordpress_sec_',
                        'wp-settings-',
                        'wp-settings-time-',
                    ];
                    foreach ($_COOKIE as $k => $v) {
                        $is_allowed = false;
                        foreach ($allowed_cookie_prefixes as $prefix) {
                            if (strpos($k, $prefix) === 0) {
                                $is_allowed = true;
                                break;
                            }
                        }
                        if ($is_allowed) {
                            $pairs[] = rawurlencode($k) . '=' . rawurlencode(sanitize_text_field(wp_unslash($v)));
                        }
                    }
                    $cookie_header = implode('; ', $pairs);
                }
                $args = [
                    'timeout'  => 0.01,
                    'blocking' => false,
                    'body'     => [
                        'action'   => 'slimstat_update_geoip_database',
                        'security' => wp_create_nonce('slimstat_geoip_action'),
                    ],
                    'headers' => $cookie_header !== '' && $cookie_header !== '0' ? ['Cookie' => $cookie_header] : [],
                ];
                // Best-effort call; ignore response
                wp_safe_remote_post($ajax_url, $args);
            }
        }

        // Add style to the admin menu
        add_action('admin_head', [self::class, 'styling_admin_menu']);

        // Init feedback
        self::initFeedback();

        // Add lock export button in report header
        add_filter('slimstat_report_header_buttons', fn ($_header_buttons, $_report_id) => self::add_lock_export_button($_header_buttons, $_report_id), 10, 2);

        // Sync index options with actual DB state — skip SHOW INDEX if option already confirmed
        foreach (self::get_index_definitions() as $def) {
            if ('yes' === get_option($def['option'])) {
                continue;
            }
            $exists = wp_slimstat::$wpdb->get_results(sprintf("SHOW INDEX FROM %sslim_stats WHERE Key_name = '%s'", $GLOBALS['wpdb']->prefix, $def['name']));
            if (!empty($exists)) {
                update_option($def['option'], 'yes');
            }
        }

        self::register_index_hooks();

        // Register the combined notice
        add_action('admin_notices', ['wp_slimstat_admin', 'show_indexes_notice']);

        // Initialize notification system
        if (class_exists('SlimStat\\Services\\Admin\\Notification\\NotificationManager')) {
            new \SlimStat\Services\Admin\Notification\NotificationManager();
        }
        // Initialize cron manager for notifications
        if (class_exists('SlimStat\\Services\\CronEventManager')) {
            new \SlimStat\Services\CronEventManager();
        }
    }

    // END: init

    /**
     * Add style to the admin menu
     */
    public static function styling_admin_menu()
    {
        if (!wp_slimstat::pro_is_installed()) {
            echo '<style> a.wp-slimstat-upgrade-to-pro {background-color: #f22f46 !important;color: #fff !important;font-weight: 600 !important;} </style>';
        }
    }

    /**
     * Clears the purge cron job
     */
    public static function deactivate()
    {
        wp_clear_scheduled_hook('wp_slimstat_purge');
        wp_clear_scheduled_hook('wp_slimstat_update_geoip_database');
    }

    /**
     *  Reset layout
     */
    public static function handle_reset_layout()
    {
        // Check nonce
        if (!wp_verify_nonce($_REQUEST['_wpnonce'], 'reset_layout')) {
            wp_die(__('Sorry, you are not allowed to access this page.', 'wp-slimstat'));
        }

        $GLOBALS['wpdb']->query(sprintf("DELETE FROM %susermeta WHERE meta_key LIKE '%%meta-box-order_admin_page_slimlayout%%'", $GLOBALS['wpdb']->prefix));
        $GLOBALS['wpdb']->query(sprintf("DELETE FROM %susermeta WHERE meta_key LIKE '%%mmetaboxhidden_admin_page_slimview%%'", $GLOBALS['wpdb']->prefix));
        $GLOBALS['wpdb']->query(sprintf("DELETE FROM %susermeta WHERE meta_key LIKE '%%meta-box-order_slimstat%%'", $GLOBALS['wpdb']->prefix));
        $GLOBALS['wpdb']->query(sprintf("DELETE FROM %susermeta WHERE meta_key LIKE '%%metaboxhidden_slimstat%%'", $GLOBALS['wpdb']->prefix));
        $GLOBALS['wpdb']->query(sprintf("DELETE FROM %susermeta WHERE meta_key LIKE '%%closedpostboxes_slimstat%%'", $GLOBALS['wpdb']->prefix));

        // Redirect to layout page
        wp_safe_redirect(admin_url('admin.php?page=slimlayout'));
        die();
    }

    /**
     * Support for WP MU network activations
     */
    public static function new_blog($_blog_id)
    {
        switch_to_blog($_blog_id);
        self::init_environment();
        restore_current_blog();
    }

    // END: new_blog

    /**
     * Support for WP MU site deletion
     */
    public static function drop_tables($_tables = [], $_blog_id = 1)
    {
        $_tables['slim_events'] = $GLOBALS['wpdb']->prefix . 'slim_events';
        $_tables['slim_stats']  = $GLOBALS['wpdb']->prefix . 'slim_stats';

        $_tables['slim_events_archive'] = $GLOBALS['wpdb']->prefix . 'slim_events_archive';
        $_tables['slim_stats_archive']  = $GLOBALS['wpdb']->prefix . 'slim_stats_archive';

        return $_tables;
    }

    // END: drop_tables

    /**
     * Creates tables, initializes options and schedules purge cron
     */
    public static function init_environment()
    {
        if (function_exists('apply_filters')) {
            $my_wpdb = apply_filters('slimstat_custom_wpdb', $GLOBALS['wpdb']);
        }

        // Create the tables
        self::init_tables($my_wpdb);

        // Initialize atomic visit ID counter (fix for issue #155 - performance regression)
        \SlimStat\Tracker\VisitIdGenerator::initializeCounter();

        // Ensure country/dt index exists for performance
        $has_index = $my_wpdb->get_results(sprintf("SHOW INDEX FROM %sslim_stats WHERE Key_name = 'idx_country_dt'", $GLOBALS['wpdb']->prefix));
        if (!$has_index || 0 === count($has_index)) {
            $my_wpdb->query(sprintf('CREATE INDEX idx_country_dt ON %sslim_stats (country, dt)', $GLOBALS['wpdb']->prefix));
        }
        update_option('slimstat_country_dt_indexed', 'yes');

        // --- Add (dt, screen_width, screen_height) index for Top Screen Resolutions ---
        $dt_screen_index = $my_wpdb->get_results(sprintf("SHOW INDEX FROM %sslim_stats WHERE Key_name = 'idx_dt_screen_width_screen_height'", $GLOBALS['wpdb']->prefix));
        if (empty($dt_screen_index)) {
            $my_wpdb->query(sprintf('CREATE INDEX idx_dt_screen_width_screen_height ON %sslim_stats (dt, screen_width, screen_height)', $GLOBALS['wpdb']->prefix));
        }
        update_option('slimstat_dt_screen_indexed', 'yes');

        // --- Add (dt, browser, browser_version) index for Top Browsers ---
        $dt_browser_index = $my_wpdb->get_results(sprintf("SHOW INDEX FROM %sslim_stats WHERE Key_name = 'idx_dt_browser_browser_version'", $GLOBALS['wpdb']->prefix));
        if (empty($dt_browser_index)) {
            $my_wpdb->query(sprintf('CREATE INDEX idx_dt_browser_browser_version ON %sslim_stats (dt, browser, browser_version)', $GLOBALS['wpdb']->prefix));
        }
        update_option('slimstat_dt_browser_indexed', 'yes');

        // --- Add (dt, platform) index for Top Platforms ---
        $dt_platform_index = $my_wpdb->get_results(sprintf("SHOW INDEX FROM %sslim_stats WHERE Key_name = 'idx_dt_platform'", $GLOBALS['wpdb']->prefix));
        if (empty($dt_platform_index)) {
            $my_wpdb->query(sprintf('CREATE INDEX idx_dt_platform ON %sslim_stats (dt, platform)', $GLOBALS['wpdb']->prefix));
        }
        update_option('slimstat_dt_platform_indexed', 'yes');

        // --- Add (dt, visit_id) covering index for visitor counter queries ---
        $dt_visit_index = $my_wpdb->get_results(sprintf("SHOW INDEX FROM %sslim_stats WHERE Key_name = '%sstats_dt_visit_idx'", $GLOBALS['wpdb']->prefix, $GLOBALS['wpdb']->prefix));
        if (empty($dt_visit_index)) {
            $my_wpdb->query(sprintf('CREATE INDEX %sstats_dt_visit_idx ON %sslim_stats (dt, visit_id)', $GLOBALS['wpdb']->prefix, $GLOBALS['wpdb']->prefix));
        }
        update_option('slimstat_dt_visit_indexed', 'yes');

        // Hard-flush rewrite rules so the adblock bypass rewrite is written to .htaccess.
        // Caching plugins (WP Rocket, W3TC) route requests via .htaccess before WordPress
        // loads — a soft flush (false) only updates the DB and would not help.
        flush_rewrite_rules();

        return true;
    }

    // END: init_environment

    /**
     * Creates and populates tables, if they aren't already there.
     */
    public static function init_tables($_wpdb = '')
    {
        // Is InnoDB available?
        $have_innodb = $_wpdb->get_results("SHOW VARIABLES LIKE 'have_innodb'", ARRAY_A);
        $use_innodb  = (!empty($have_innodb[0]) && 'YES' == $have_innodb[0]['Value']) ? 'ENGINE=InnoDB' : '';

        // Table that stores the actual data about visits
        $stats_table_sql = "
            CREATE TABLE IF NOT EXISTS {$GLOBALS['wpdb']->prefix}slim_stats (
                id INT UNSIGNED NOT NULL auto_increment,
                ip VARCHAR(39) DEFAULT NULL,
                other_ip VARCHAR(39) DEFAULT NULL,
				username VARCHAR(256) DEFAULT NULL,
				email VARCHAR(256) DEFAULT NULL,

				country VARCHAR(16) DEFAULT NULL,
				location VARCHAR(36) DEFAULT NULL,
				city VARCHAR(256) DEFAULT NULL,

				referer VARCHAR(2048) DEFAULT NULL,
				resource VARCHAR(2048) DEFAULT NULL,
				searchterms VARCHAR(2048) DEFAULT NULL,
				notes VARCHAR(2048) DEFAULT NULL,
				visit_id INT UNSIGNED NOT NULL DEFAULT 0,
				server_latency INT(10) UNSIGNED DEFAULT 0,
				page_performance INT(10) UNSIGNED DEFAULT 0,

				browser VARCHAR(40) DEFAULT NULL,
				browser_version VARCHAR(15) DEFAULT NULL,
				browser_type TINYINT UNSIGNED DEFAULT 0,
				platform VARCHAR(15) DEFAULT NULL,
				language VARCHAR(5) DEFAULT NULL,
				fingerprint VARCHAR(256) DEFAULT NULL,
				user_agent VARCHAR(2048) DEFAULT NULL,

				resolution VARCHAR(12) DEFAULT NULL,
				screen_width SMALLINT UNSIGNED DEFAULT 0,
				screen_height SMALLINT UNSIGNED DEFAULT 0,

				content_type VARCHAR(64) DEFAULT NULL,
				category VARCHAR(256) DEFAULT NULL,
				author VARCHAR(64) DEFAULT NULL,
				content_id BIGINT(20) UNSIGNED DEFAULT 0,

				outbound_resource VARCHAR(2048) DEFAULT NULL,

				tz_offset SMALLINT DEFAULT 0,
				dt_out INT(10) UNSIGNED DEFAULT 0,
				dt INT(10) UNSIGNED DEFAULT 0,

				CONSTRAINT PRIMARY KEY (id),
                INDEX {$GLOBALS['wpdb']->prefix}slim_stats_dt_idx (dt),
				INDEX {$GLOBALS['wpdb']->prefix}stats_resource_idx( resource( 20 ) ),
				INDEX {$GLOBALS['wpdb']->prefix}stats_browser_idx( browser( 10 ) ),
				INDEX {$GLOBALS['wpdb']->prefix}stats_searchterms_idx( searchterms( 15 ) ),
				INDEX {$GLOBALS['wpdb']->prefix}stats_fingerprint_idx( fingerprint( 20 ) ),
				INDEX {$GLOBALS['wpdb']->prefix}stats_dt_visit_idx (dt, visit_id)
			) COLLATE utf8_general_ci {$use_innodb}";

        // This table will track outbound links (clicks on links to external sites)
        $events_table_sql = "
			CREATE TABLE IF NOT EXISTS {$GLOBALS['wpdb']->prefix}slim_events (
				event_id INT(10) NOT NULL AUTO_INCREMENT,
				type TINYINT UNSIGNED DEFAULT 0,
				event_description VARCHAR(64) DEFAULT NULL,
				notes VARCHAR(256) DEFAULT NULL,
				position VARCHAR(32) DEFAULT NULL,
				id INT UNSIGNED NOT NULL DEFAULT 0,
				dt INT(10) UNSIGNED DEFAULT 0,

				CONSTRAINT PRIMARY KEY (event_id),
				INDEX {$GLOBALS['wpdb']->prefix}slim_stat_events_idx (dt),
				CONSTRAINT fk_{$GLOBALS['wpdb']->prefix}slim_events_id FOREIGN KEY (id) REFERENCES {$GLOBALS['wpdb']->prefix}slim_stats(id) ON UPDATE CASCADE ON DELETE CASCADE
			) COLLATE utf8_general_ci {$use_innodb}";

        $archive_table_sql = "
			CREATE TABLE IF NOT EXISTS {$GLOBALS['wpdb']->prefix}slim_stats_archive
			LIKE {$GLOBALS['wpdb']->prefix}slim_stats";

        $events_archive_table_sql = "
			CREATE TABLE IF NOT EXISTS {$GLOBALS['wpdb']->prefix}slim_events_archive (
				event_id INT(10) NOT NULL AUTO_INCREMENT,
				type TINYINT UNSIGNED DEFAULT 0,
				event_description VARCHAR(64) DEFAULT NULL,
				notes VARCHAR(256) DEFAULT NULL,
				position VARCHAR(32) DEFAULT NULL,
				id INT UNSIGNED NOT NULL DEFAULT 0,
				dt INT(10) UNSIGNED DEFAULT 0,

				CONSTRAINT PRIMARY KEY (event_id),
				INDEX {$GLOBALS['wpdb']->prefix}slim_stat_events_archive_idx (dt)
			) COLLATE utf8_general_ci {$use_innodb}";

        // Ok, let's create the table structure
        self::_create_table($stats_table_sql, $GLOBALS['wpdb']->prefix . 'slim_stats', $_wpdb);
        self::_create_table($events_table_sql, $GLOBALS['wpdb']->prefix . 'slim_events', $_wpdb);
        self::_create_table($archive_table_sql, $GLOBALS['wpdb']->prefix . 'slim_stats_archive', $_wpdb);
        self::_create_table($events_archive_table_sql, $GLOBALS['wpdb']->prefix . 'slim_events_archive', $_wpdb);

        // Let's save the version in the database
        if (empty(wp_slimstat::$settings['version'])) {
            wp_slimstat::$settings['version'] = SLIMSTAT_ANALYTICS_VERSION;
        }

        $index_defs = [
            ['name' => 'idx_country_dt', 'sql' => sprintf('CREATE INDEX idx_country_dt ON %sslim_stats (country, dt)', $GLOBALS['wpdb']->prefix), 'option' => 'slimstat_country_dt_indexed'],
            ['name' => 'idx_dt_screen_width_screen_height', 'sql' => sprintf('CREATE INDEX idx_dt_screen_width_screen_height ON %sslim_stats (dt, screen_width, screen_height)', $GLOBALS['wpdb']->prefix), 'option' => 'slimstat_dt_screen_indexed'],
            ['name' => 'idx_dt_browser_browser_version', 'sql' => sprintf('CREATE INDEX idx_dt_browser_browser_version ON %sslim_stats (dt, browser, browser_version)', $GLOBALS['wpdb']->prefix), 'option' => 'slimstat_dt_browser_indexed'],
            ['name' => 'idx_dt_platform', 'sql' => sprintf('CREATE INDEX idx_dt_platform ON %sslim_stats (dt, platform)', $GLOBALS['wpdb']->prefix), 'option' => 'slimstat_dt_platform_indexed'],
            // Speeds up "Currently Online" queries using dt_out > NOW()-300
            ['name' => 'idx_dt_out', 'sql' => sprintf('CREATE INDEX idx_dt_out ON %sslim_stats (dt_out)', $GLOBALS['wpdb']->prefix), 'option' => 'slimstat_dt_out_indexed'],
        ];
        foreach ($index_defs as $idx) {
            $exists = $_wpdb->get_results(sprintf("SHOW INDEX FROM %sslim_stats WHERE Key_name = '%s'", $GLOBALS['wpdb']->prefix, $idx['name']));
            if (empty($exists)) {
                $_wpdb->query($idx['sql']);
            }
            update_option($idx['option'], 'yes');
        }

    }

    // END: init_tables

    /**
     * Updates stuff around as needed (table schema, options, settings, files, etc)
     */
    public static function update_tables_and_options()
    {
        $my_wpdb = apply_filters('slimstat_custom_wpdb', $GLOBALS['wpdb']);

        // --- Updates for version 4.8.2 ---
        if (version_compare(wp_slimstat::$settings['version'], '4.8.2', '<')) {
            // Add new email column to database
            $my_wpdb->query(sprintf('ALTER TABLE %sslim_stats ADD COLUMN email VARCHAR(255) DEFAULT NULL AFTER username', $GLOBALS['wpdb']->prefix));
            $my_wpdb->query(sprintf('ALTER TABLE %sslim_stats_archive ADD COLUMN email VARCHAR(255) DEFAULT NULL AFTER username', $GLOBALS['wpdb']->prefix));
        }

        // --- END: Updates for version 4.8.2 ---

        // --- Updates for version 4.8.4 ---
        if (version_compare(wp_slimstat::$settings['version'], '4.8.4', '<')) {
            // Switch option to track WP users (from track to ignore)
            wp_slimstat::$settings['ignore_wp_users'] = (!empty(wp_slimstat::$settings['track_users']) && 'no' == wp_slimstat::$settings['track_users']) ? 'on' : 'no';

            // Remove unused options
            unset(wp_slimstat::$settings['track_users']);
            unset(wp_slimstat::$settings['enable_javascript']);
            unset(wp_slimstat::$settings['honor_dnt_header']);
            unset(wp_slimstat::$settings['no_maxmind_warning']);
            unset(wp_slimstat::$settings['no_browscap_warning']);
            unset(wp_slimstat::$settings['use_european_separators']);
            unset($wp_slimstat::$settings['date_format']);
            unset($wp_slimstat::$settings['time_format']);
            unset($wp_slimstat::$settings['expand_details']);

            // Add table indexes for improved performance (idempotent)
            $indexes = [
                ['name' => $GLOBALS['wpdb']->prefix . 'stats_resource_idx', 'sql' => sprintf('ALTER TABLE %sslim_stats ADD INDEX %sstats_resource_idx( resource( 20 ) )', $GLOBALS['wpdb']->prefix, $GLOBALS['wpdb']->prefix)],
                ['name' => $GLOBALS['wpdb']->prefix . 'stats_browser_idx', 'sql' => sprintf('ALTER TABLE %sslim_stats ADD INDEX %sstats_browser_idx( browser( 10 ) )', $GLOBALS['wpdb']->prefix, $GLOBALS['wpdb']->prefix)],
                ['name' => $GLOBALS['wpdb']->prefix . 'stats_searchterms_idx', 'sql' => sprintf('ALTER TABLE %sslim_stats ADD INDEX %sstats_searchterms_idx( searchterms( 15 ) )', $GLOBALS['wpdb']->prefix, $GLOBALS['wpdb']->prefix)],
                ['name' => $GLOBALS['wpdb']->prefix . 'stats_fingerprint_idx', 'sql' => sprintf('ALTER TABLE %sslim_stats ADD INDEX %sstats_fingerprint_idx( fingerprint( 20 ) )', $GLOBALS['wpdb']->prefix, $GLOBALS['wpdb']->prefix)],
            ];
            foreach ($indexes as $index) {
                $check_index = wp_slimstat::$wpdb->get_results(sprintf("SHOW INDEX FROM %sslim_stats WHERE Key_name = '%s'", $GLOBALS['wpdb']->prefix, $index['name']));
                if (empty($check_index)) {
                    wp_slimstat::$wpdb->query($index['sql']);
                }
            }
            wp_slimstat::$settings['db_indexes'] = 'on';
        }

        // --- END: Updates for version 4.8.4 ---

        // --- Updates for version 4.8.4.1 ---
        if (version_compare(wp_slimstat::$settings['version'], '4.8.4.1', '<')) {
            // Goodbye, browser plugins
            wp_slimstat::$wpdb->query(sprintf('ALTER TABLE %sslim_stats DROP COLUMN plugins', $GLOBALS['wpdb']->prefix));

            // Hello there, fingerprint and timezone offset
            $my_wpdb->query(sprintf('ALTER TABLE %sslim_stats ADD COLUMN fingerprint VARCHAR(256) DEFAULT NULL AFTER language', $GLOBALS['wpdb']->prefix));
            $my_wpdb->query(sprintf('ALTER TABLE %sslim_stats_archive ADD COLUMN fingerprint VARCHAR(255) DEFAULT NULL AFTER language', $GLOBALS['wpdb']->prefix));
            $my_wpdb->query(sprintf('ALTER TABLE %sslim_stats ADD COLUMN tz_offset SMALLINT DEFAULT 0 AFTER outbound_resource', $GLOBALS['wpdb']->prefix));
            $my_wpdb->query(sprintf('ALTER TABLE %sslim_stats_archive ADD COLUMN tz_offset SMALLINT DEFAULT 0 AFTER outbound_resource', $GLOBALS['wpdb']->prefix));
        }

        // --- END: Updates for version 4.8.4.1 ---

        // --- Updates for version 4.8.8 ---
        if (version_compare(wp_slimstat::$settings['version'], '4.8.8', '<')) {
            // Adding new index on the 'fingerprint' column for improved performance
            if ('on' == wp_slimstat::$settings['db_indexes']) {
                $my_wpdb->query(sprintf('ALTER TABLE %sslim_stats ADD INDEX %sstats_fingerprint_idx( fingerprint( 20 ) )', $GLOBALS['wpdb']->prefix, $GLOBALS['wpdb']->prefix));
            }

            $my_wpdb->query(sprintf("UPDATE %sslim_stats SET notes = CONCAT( '[', REPLACE( notes, ';', '][' ), ']' ) WHERE notes NOT LIKE '[%%'", $GLOBALS['wpdb']->prefix));
        }

        // --- Updates for version 5.4.0 ---
        if (version_compare(wp_slimstat::$settings['version'], '5.4.0', '<')) {
            // Migrate legacy 'adblock' tracking method to 'adblock_bypass' (renamed in v5.3.0)
            if (!empty(wp_slimstat::$settings['tracking_request_method']) && 'adblock' === wp_slimstat::$settings['tracking_request_method']) {
                wp_slimstat::$settings['tracking_request_method'] = 'adblock_bypass';
            }

            // Default use_separate_menu to 'on' if not already set
            if (empty(wp_slimstat::$settings['use_separate_menu'])) {
                wp_slimstat::$settings['use_separate_menu'] = 'on';
            }
        }

        // --- Updates for version 5.4.1 ---
        // Fix admin bar migration: empty('no') returned false in 5.4.0, missing users with legacy 'no' value
        // Safe because this runs once (version bumps to 5.4.1 after), users who disable later are already on 5.4.1+
        if (version_compare(wp_slimstat::$settings['version'], '5.4.1', '<')) {
            wp_slimstat::$settings['use_separate_menu'] = 'on';
        }

        // --- Updates for version 5.4.3 ---
        if (version_compare(wp_slimstat::$settings['version'], '5.4.3', '<')) {
            // Add (dt, visit_id) covering index for visitor counter queries
            $idx_name = $GLOBALS['wpdb']->prefix . 'stats_dt_visit_idx';
            $check = $my_wpdb->get_results(sprintf(
                "SHOW INDEX FROM %sslim_stats WHERE Key_name = '%s'",
                $GLOBALS['wpdb']->prefix, $idx_name
            ));
            if (empty($check)) {
                $result = $my_wpdb->query(sprintf(
                    'CREATE INDEX %s ON %sslim_stats (dt, visit_id)',
                    $idx_name, $GLOBALS['wpdb']->prefix
                ));
                if ($result !== false) {
                    update_option('slimstat_dt_visit_indexed', 'yes');
                }
                // If fails (large table timeout), show_indexes_notice() surfaces a retry button
            } else {
                update_option('slimstat_dt_visit_indexed', 'yes');
            }
        }

        // Clear stale query cache transients on upgrade to prevent data inconsistencies
        // (e.g., cached $pageviews causing percentage >100% in reports — see #270)
        $GLOBALS['wpdb']->query(
            "DELETE FROM {$GLOBALS['wpdb']->options} WHERE option_name LIKE '_transient_wp_slimstat_cache_%' OR option_name LIKE '_transient_timeout_wp_slimstat_cache_%' LIMIT 1000"
        );

        // Now we can update the version stored in the database
        wp_slimstat::$settings['version']            = SLIMSTAT_ANALYTICS_VERSION;
        wp_slimstat::$settings['notice_latest_news'] = 'on';
        wp_slimstat::update_option('slimstat_options', wp_slimstat::$settings);

        return true;
    }

    // END: update_tables_and_options

    public static function add_dashboard_widgets()
    {
        // If this user is whitelisted, we use the minimum capability
        $minimum_capability = 'read';
        if (false === strpos(wp_slimstat::$settings['can_view'], (string) $GLOBALS['current_user']->user_login) && !empty(wp_slimstat::$settings['capability_can_view'])) {
            $minimum_capability = wp_slimstat::$settings['capability_can_view'];
        }

        if (!current_user_can($minimum_capability)) {
            return;
        }

        // Initialize the new Reports system FIRST before legacy system loads
        \SlimStat\Reports\Bootstrap::get_instance()->init();

        // The Reports library is only loaded on the plugin's screens
        include_once(plugin_dir_path(__FILE__) . 'view/wp-slimstat-reports.php');
        wp_slimstat_reports::init();

        if (!empty(wp_slimstat_reports::$user_reports['dashboard']) && is_array(wp_slimstat_reports::$user_reports['dashboard'])) {
            foreach (wp_slimstat_reports::$user_reports['dashboard'] as $a_report_id) {
                if (empty(wp_slimstat_reports::$reports[$a_report_id])) {
                    continue;
                }
                wp_add_dashboard_widget($a_report_id, wp_slimstat_reports::$reports[$a_report_id]['title'], ['wp_slimstat_reports', 'callback_wrapper']);
            }
        }
    }

    // END: add_dashboard_widgets

    /**
     * Removes 'spammers' from the database when the corresponding comments are marked as spam
     */
    public static function remove_spam($_new_status = '', $_old_status = '', $_comment = '')
    {
        $my_wpdb = apply_filters('slimstat_custom_wpdb', $GLOBALS['wpdb']);

        if ('spam' == $_new_status && !empty($_comment->comment_author) && !empty($_comment->comment_author_IP)) {
            $my_wpdb->query(wp_slimstat::$wpdb->prepare("
				DELETE ts
				FROM {$GLOBALS['wpdb']->prefix}slim_stats ts
				WHERE username = %s OR INET_NTOA(ip) = %s", $_comment->comment_author, $_comment->comment_author_IP));
        }
    }

    // END: remove_spam

    /**
     * Loads a custom stylesheet file for the administration panels
     */
    public static function wp_slimstat_stylesheet($_hook = '')
    {
        wp_register_style('wp-slimstat', plugins_url('/admin/assets/css/admin.css', __DIR__), false, SLIMSTAT_ANALYTICS_VERSION);
		wp_enqueue_style('wp-slimstat');

		wp_register_style(
			'wp-slimstat-header-modern',
			plugins_url('/admin/assets/css/header-modern.css', __DIR__),
			['wp-slimstat'],
			SLIMSTAT_ANALYTICS_VERSION
		);
		wp_enqueue_style('wp-slimstat-header-modern');

        if (!empty(wp_slimstat::$settings['custom_css'])) {
            wp_add_inline_style('wp-slimstat', wp_slimstat::$settings['custom_css']);
        }
    }

    // END: wp_slimstat_stylesheet

    /**
     * Adds a shared body class to all Slimstat admin screens.
     */
    public static function add_admin_body_class($classes)
    {
        return $classes . ' slimstat-admin-page';
    }

    /**
     * Loads user-defined stylesheet code
     */
    public static function wp_slimstat_userdefined_stylesheet()
    {
        echo '<style type="text/css" media="screen">' . wp_slimstat::$settings['custom_css'] . '</style>';
    }

    // END: wp_slimstat_userdefined_stylesheet

    /**
     * Enqueues Javascript and styles needed in the admin
     */
    public static function wp_slimstat_enqueue_scripts($_hook = '')
    {
        $current_screen = get_current_screen();
        if ($current_screen && str_contains($current_screen->id ?? '', 'slim')) {
            wp_enqueue_script('dashboard');
            wp_enqueue_script('jquery-ui-datepicker');
            wp_enqueue_script('jquery-ui-sortable');
        }

        // Enqueue the built-in code editor to use on the Settings
        if ($current_screen) {
            wp_enqueue_code_editor(['type' => 'text/html']);
        }

        // Enqueue date range picker assets for report pages
        $should_load_datepicker = false;
        if (isset($_GET['page'])) {
            $page = sanitize_text_field($_GET['page']);
            if (str_contains($page, 'slim') && !str_contains($page, 'setting')) {
                $should_load_datepicker = true;
            }
        }

        if ($should_load_datepicker) {

            // Enqueue moment.js
            wp_enqueue_script('slimstat-moment', plugins_url('/admin/assets/js/daterangepicker/moment.min.js', __DIR__), [], '2.30.2', true);

            // Enqueue daterangepicker
            wp_enqueue_script('slimstat-daterangepicker', plugins_url('/admin/assets/js/daterangepicker/daterangepicker.min.js', __DIR__), ['jquery', 'slimstat-moment'], '3.1.0', true);

            // Enqueue our custom date picker
            wp_enqueue_script('slimstat-custom-datepicker', plugins_url('/admin/assets/js/daterangepicker/slimstat-daterangepicker.js', __DIR__), ['jquery', 'slimstat-daterangepicker'], SLIMSTAT_ANALYTICS_VERSION, true);

            // Enqueue date picker styles
            wp_enqueue_style('slimstat-daterangepicker-base', plugins_url('/admin/assets/css/daterangepicker/daterangepicker.css', __DIR__), [], '3.1.0');
            wp_enqueue_style('slimstat-daterangepicker-custom', plugins_url('/admin/assets/css/daterangepicker/slimstat-datepicker-styles.css', __DIR__), ['slimstat-daterangepicker-base'], SLIMSTAT_ANALYTICS_VERSION);

            // Localize date picker script
            $datepicker_params = [
                'ajax_url' => admin_url('admin-ajax.php'),
                'clear_cache_nonce' => wp_create_nonce('slimstat_clear_cache'),
                'options' => [
                    'wp_timezone' => DateRangeHelper::get_wp_timezone(),
                    'start_of_week' => DateRangeHelper::get_week_start(),
                    'date_format' => DateRangeHelper::get_date_format()
                ],
                'strings' => DateRangeHelper::get_localized_strings()
            ];
            wp_localize_script('slimstat-custom-datepicker', 'SlimStatDatePicker', $datepicker_params);
        }

        wp_enqueue_script('slimstat_admin', plugins_url('/admin/assets/js/admin.js', __DIR__), ['jquery-ui-dialog'], SLIMSTAT_ANALYTICS_VERSION, true);

        // Enqueue notification assets if notifications are enabled
        if (wp_slimstat::$settings['display_notifications'] == 'on') {
            wp_enqueue_style('slimstat_notifications', plugins_url('/admin/assets/css/notifications.css', __DIR__), [], SLIMSTAT_ANALYTICS_VERSION);
            wp_enqueue_style('slimstat_header_notifications', plugins_url('/admin/assets/css/header-notifications.css', __DIR__), [], SLIMSTAT_ANALYTICS_VERSION);
            wp_enqueue_script('slimstat_notifications', plugins_url('/admin/assets/js/notifications.js', __DIR__), ['jquery'], SLIMSTAT_ANALYTICS_VERSION, false);

            // Pass notification data to Javascript
            $notification_params = [
                'ajax_url' => admin_url('admin-ajax.php'),
                'nonce'    => wp_create_nonce('wp_rest'),
            ];
            wp_localize_script('slimstat_notifications', 'slimstat_admin', $notification_params);
        }

        // Pass some information to Javascript
        $params = [
            'async_load'        => empty(wp_slimstat::$settings['async_load']) ? 'no' : wp_slimstat::$settings['async_load'],
            'datepicker_image'  => plugins_url('/admin/assets/images/datepicker.png', __DIR__),
            'refresh_interval'  => intval(wp_slimstat::$settings['refresh_interval']),
            'page_location'     => self::$page_location,
            'clear_cache_nonce' => wp_create_nonce('slimstat_clear_cache'),
        ];
        wp_localize_script('slimstat_admin', 'SlimStatAdminParams', $params);
    }

    // END: wp_slimstat_enqueue_scripts

    /**
     * Adds a new entry in the admin menu, to view the stats
     */
    public static function add_menus($_s = '')
    {
        global $submenu;

        // If this user is whitelisted, we use the minimum capability
        $minimum_capability = 'read';
        if (is_network_admin()) {
            $minimum_capability = 'manage_network';
        } elseif (false === strpos(wp_slimstat::$settings['can_view'], (string) $GLOBALS['current_user']->user_login) && !empty(wp_slimstat::$settings['capability_can_view'])) {
            $minimum_capability = wp_slimstat::$settings['capability_can_view'];
        }

        // Find the first available location (screens with no reports assigned to them are hidden from the nav)
		$parent = '';
		if (is_array(self::$meta_user_reports)) {
			foreach (self::$screens_info as $a_screen_id => $a_screen_info) {
				if (!empty(self::$meta_user_reports[$a_screen_id]) && $a_screen_info['show_in_sidebar']) {
					$parent = $a_screen_id;
					break;
				}
			}
		}

		// If no parent was found in the user meta, use the first available screen as the parent
		if (empty($parent) && !empty(self::$screens_info)) {
			$parent = array_key_first(self::$screens_info);
		}

		// Don't show the menu if no screens are available at all
		if (empty($parent) || !isset(self::$screens_info[$parent])) {
			return null;
		}

		self::$main_menu_slug = $parent;

        // Build menu title with notification badge
        $menu_title = __('SlimStat', 'wp-slimstat');
        if (class_exists(NotificationFactory::class) && wp_slimstat::$settings['display_notifications'] === 'on') {
            $notification_count = NotificationFactory::getNewNotificationCount();
            if ($notification_count > 0) {
                $menu_title .= sprintf(
                    ' <span class="update-plugins count-%d"><span class="plugin-count">%s</span></span>',
                    $notification_count,
                    number_format_i18n($notification_count)
                );
            }
        }

        // Add the main menu
        add_menu_page(
            __('SlimStat', 'wp-slimstat'),
            $menu_title,
            $minimum_capability,
            $parent,
            [self::class, 'wp_slimstat_include_view'],
            'dashicons-chart-area'
        );

        foreach (self::$screens_info as $a_screen_id => $a_screen_info) {
            if (isset(self::$meta_user_reports[$a_screen_id]) && empty(self::$meta_user_reports[$a_screen_id])) {
                continue;
            }

            $minimum_capability = 'read';
            if (!empty($a_screen_info['capability']) && false === strpos(wp_slimstat::$settings[$a_screen_info['capability']], (string) $GLOBALS['current_user']->user_login) && !empty(wp_slimstat::$settings['capability_' . $a_screen_info['capability']])) {
                $minimum_capability = wp_slimstat::$settings['capability_' . $a_screen_info['capability']];
            }

            if ($a_screen_info['show_in_sidebar']) {
                $new_entry[] = add_submenu_page(
                    $parent,
                    $a_screen_info['title'],
                    $a_screen_info['title'],
                    $minimum_capability,
                    $a_screen_id,
                    $a_screen_info['callback']
                );
            }
        }

        if (isset($submenu[$parent])) {
            array_walk($submenu[$parent], function (&$item) {
                if (isset($item[2]) && 'slimpro' === $item[2]) {
                    $item[4] = isset($item[4]) ? $item[4] . ' wp-slimstat-upgrade-to-pro' : ' wp-slimstat-upgrade-to-pro';
                }
            });
        }

        // Load styles and Javascript needed to make the reports look nice and interactive
        foreach ($new_entry as $a_entry) {
            add_action('load-' . $a_entry, [self::class, 'wp_slimstat_stylesheet']);
            add_action('load-' . $a_entry, [self::class, 'wp_slimstat_enqueue_scripts']);
            add_action('load-' . $a_entry, [self::class, 'contextual_help']);
            add_action('load-' . $a_entry, function () {
                add_filter('admin_body_class', [wp_slimstat_admin::class, 'add_admin_body_class']);
            });
        }

        return $_s;
    }

    // END: add_menus

    /**
     * Enqueue admin bar modal styles globally (admin + frontend)
     */
    public static function enqueue_adminbar_styles()
    {
        if (is_admin_bar_showing()) {
            wp_enqueue_style(
                'slimstat-adminbar',
                plugins_url('/admin/assets/css/admin-bar-modal.css', __DIR__),
                [],
                SLIMSTAT_ANALYTICS_VERSION
            );

            // Enqueue admin bar realtime JS for stats auto-refresh (frontend + admin)
            // On frontend: self-polls every minute
            // On admin: defers to admin.js slimstat:minute_pulse
            wp_enqueue_script(
                'slimstat-adminbar-realtime',
                plugins_url('/admin/assets/js/adminbar-realtime.js', __DIR__),
                [],
                SLIMSTAT_ANALYTICS_VERSION,
                true
            );

            wp_localize_script('slimstat-adminbar-realtime', 'SlimStatAdminBar', [
                'ajax_url'  => admin_url('admin-ajax.php'),
                'security'  => wp_create_nonce('meta-box-order'),
                'is_pro'    => wp_slimstat::pro_is_installed(),
                'i18n'      => [
                    'was_last_day' => esc_html__('was %s last day', 'wp-slimstat'),
                    'online_users' => esc_html__('Online Users', 'wp-slimstat'),
                    'count_label'  => esc_html__('Count', 'wp-slimstat'),
                    'now'          => esc_html__('Now', 'wp-slimstat'),
                    'min_ago'      => esc_html__('min ago', 'wp-slimstat'),
                ],
            ]);
        }
    }

    // END: enqueue_adminbar_styles

    /**
     * Adds a new entry in the WordPress Admin Bar with stats modal
     */
    public static function add_menu_to_adminbar()
    {
        // If this user is whitelisted, we use the minimum capability
        $minimum_capability = 'read';
        if (is_network_admin()) {
            $minimum_capability = 'manage_network';
        } elseif (false === strpos(wp_slimstat::$settings['can_view'], (string) $GLOBALS['current_user']->user_login) && !empty(wp_slimstat::$settings['capability_can_view'])) {
            $minimum_capability = wp_slimstat::$settings['capability_can_view'];
        }

        if (!current_user_can($minimum_capability)) {
            return;
        }

        $wpdb = wp_slimstat::$wpdb;
        $table = "{$GLOBALS['wpdb']->prefix}slim_stats";
        $today_start = mktime(0, 0, 0);
        $yesterday_start = $today_start - 86400;
        $yesterday_end = $today_start - 1;

        // Sessions Today (unique sessions - using visit_id for anonymous/hashed IP compatibility)
        $sessions_today = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(DISTINCT visit_id) FROM {$table} WHERE dt >= %d AND visit_id > 0",
            $today_start
        ));

        // Views Today (pageviews)
        $views_today = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(id) FROM {$table} WHERE dt >= %d",
            $today_start
        ));

        // Yesterday's sessions (unique sessions - using visit_id for anonymous/hashed IP compatibility)
        $sessions_yesterday = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(DISTINCT visit_id) FROM {$table} WHERE dt BETWEEN %d AND %d AND visit_id > 0",
            $yesterday_start, $yesterday_end
        ));

        // Yesterday's views
        $views_yesterday = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(id) FROM {$table} WHERE dt BETWEEN %d AND %d",
            $yesterday_start, $yesterday_end
        ));

        // Referrals Today (external referrers only)
        $site_host = parse_url(home_url(), PHP_URL_HOST);
        $referrals_today = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(id) FROM {$table} WHERE dt >= %d AND referer IS NOT NULL AND referer NOT LIKE %s",
            $today_start, '%' . $wpdb->esc_like($site_host) . '%'
        ));

        // Referrals Yesterday
        $referrals_yesterday = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(id) FROM {$table} WHERE dt BETWEEN %d AND %d AND referer IS NOT NULL AND referer NOT LIKE %s",
            $yesterday_start, $yesterday_end, '%' . $wpdb->esc_like($site_host) . '%'
        ));

        // Online Users — same 30-minute window query as header.php
        $current_minute_start = (int) floor(wp_slimstat::now() / 60) * 60;
        $window_minutes = 30;
        $window_start = $current_minute_start - (($window_minutes - 1) * 60);

        $online_count = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM (
                SELECT visit_id, MAX(
                    CASE
                        WHEN dt_out IS NOT NULL AND dt_out > 0 AND dt_out >= dt THEN dt_out
                        ELSE dt
                    END
                ) AS last_activity
                FROM {$table}
                WHERE visit_id > 0
                    AND (dt >= %d OR (dt_out IS NOT NULL AND dt_out >= %d))
                GROUP BY visit_id
                HAVING (FLOOR(last_activity / 60) * 60 + 59) >= %d
            ) live_sessions",
            $window_start, $window_start, $window_start
        ));
        $online_count = max(0, $online_count);

        // Determine premium status early (needed for chart data)
        $is_pro = wp_slimstat::pro_is_installed();

        // Query minute-by-minute data for the CSS bar chart (30-minute window)
        // Reuse LiveAnalyticsReport's session-spanning query for consistent data (#221)
        if ($is_pro) {
            $live_report  = new \SlimStat\Reports\Types\Analytics\LiveAnalyticsReport();
            $chart_result = $live_report->get_users_chart_data();
            $minute_data  = $chart_result['data'];
            $max_count    = $chart_result['max_value'];
        } else {
            // Fake placeholder data for non-Pro users
            $minute_data = [3, 5, 4, 7, 6, 8, 5, 9, 7, 6, 8, 10, 7, 5, 6, 8, 9, 7, 6, 5, 8, 10, 9, 7, 6, 8, 5, 7, 6, 8];
            $max_count = 10;
        }

        // Build chart HTML
        $chart_bars = '';
        $total_bars = count($minute_data);
        foreach ($minute_data as $i => $count) {
            $height_pct = round(($count / $max_count) * 100);
            $is_peak = ($count === $max_count && $count > 0);
            $bar_class = $is_peak ? ' slimstat-adminbar__chart-bar--peak' : '';
            $minutes_ago = $total_bars - 1 - $i; // 29 for first bar, 0 for last bar
            $time_text = $minutes_ago === 0
                ? esc_html__('Now', 'wp-slimstat')
                : sprintf('%d %s', $minutes_ago, esc_html__('min ago', 'wp-slimstat'));
            $chart_bars .= sprintf(
                '<div class="slimstat-adminbar__chart-bar%s" style="height:%d%%" data-count="%d" data-minutes-ago="%d">'
                . '<span class="slimstat-adminbar__chart-tooltip">'
                . '<strong>%s</strong>'
                . '%s: %d<br>'
                . '%s'
                . '</span></div>',
                $bar_class,
                $count > 0 ? max($height_pct, 3) : 0, // 0% for empty, min 3% for non-zero
                $count,
                $minutes_ago,
                esc_html__('Online Users', 'wp-slimstat'),
                esc_html__('Count', 'wp-slimstat'),
                $count,
                $time_text
            );
        }
        $view_url = get_admin_url($GLOBALS['blog_id'], 'admin.php?page=');
        $overview_url = $view_url . 'slimview2';
        $upgrade_url = 'https://wp-slimstat.com/pricing/?utm_source=wp-slimstat&utm_medium=link&utm_campaign=adminbar';

        // Add parent node
        $GLOBALS['wp_admin_bar']->add_menu([
            'id'    => 'slimstat-header',
            'title' => '<span class="ab-icon dashicons dashicons-chart-area" style="font-size:1rem;margin-top:3px"></span>'
                     . sprintf(__('Online: %s', 'wp-slimstat'), '<span id="slimstat-adminbar-online-header">' . number_format_i18n($online_count) . '</span>'),
            'href'  => $overview_url,
        ]);

        // Add stats grid node
        // For non-Pro users, show fake data for Views and Referrals
        $views_display = $is_pro ? number_format_i18n($views_today) : '248';
        $views_yesterday_display = $is_pro ? number_format_i18n($views_yesterday) : '312';
        $referrals_display = $is_pro ? number_format_i18n($referrals_today) : '18';
        $referrals_yesterday_display = $is_pro ? number_format_i18n($referrals_yesterday) : '24';
        $blur_class = $is_pro ? '' : ' slimstat-adminbar__stat-card--blur';

        $stats_html = '<div class="slimstat-adminbar__stats-grid">'
            // Online Users (top left)
            . '<div class="slimstat-adminbar__stat-card">'
            . '<div class="slimstat-adminbar__stat-title">' . esc_html__('Online Users', 'wp-slimstat')
            . ' <span class="slimstat-adminbar__realtime-dot"></span></div>'
            . '<div class="slimstat-adminbar__stat-count" id="slimstat-adminbar-online-count">' . number_format_i18n($online_count) . '</div>'
            . '<div class="slimstat-adminbar__realtime-badge">'
            . '<span class="slimstat-adminbar__realtime-pulse"></span> '
            . esc_html__('Realtime', 'wp-slimstat') . '</div>'
            . '</div>'
            // Sessions Today (top right)
            . '<div class="slimstat-adminbar__stat-card">'
            . '<div class="slimstat-adminbar__stat-title">' . esc_html__('Sessions Today', 'wp-slimstat') . '</div>'
            . '<div class="slimstat-adminbar__stat-count" id="slimstat-adminbar-sessions-count">' . number_format_i18n($sessions_today) . '</div>'
            . '<div class="slimstat-adminbar__stat-comparison" id="slimstat-adminbar-sessions-compare">'
            . sprintf(esc_html__('was %s last day', 'wp-slimstat'), number_format_i18n($sessions_yesterday))
            . '</div></div>'
            // Views Today (bottom left) - blur for non-Pro
            . '<div class="slimstat-adminbar__stat-card' . $blur_class . '">'
            . '<div class="slimstat-adminbar__stat-title">' . esc_html__('Views Today', 'wp-slimstat') . '</div>'
            . '<div class="slimstat-adminbar__stat-count" id="slimstat-adminbar-views-count">' . $views_display . '</div>'
            . '<div class="slimstat-adminbar__stat-comparison" id="slimstat-adminbar-views-compare">'
            . sprintf(esc_html__('was %s last day', 'wp-slimstat'), $views_yesterday_display)
            . '</div></div>'
            // Referrals Today (bottom right) - blur for non-Pro
            . '<div class="slimstat-adminbar__stat-card' . $blur_class . '">'
            . '<div class="slimstat-adminbar__stat-title">' . esc_html__('Referrals Today', 'wp-slimstat') . '</div>'
            . '<div class="slimstat-adminbar__stat-count" id="slimstat-adminbar-referrals-count">' . $referrals_display . '</div>'
            . '<div class="slimstat-adminbar__stat-comparison" id="slimstat-adminbar-referrals-compare">'
            . sprintf(esc_html__('was %s last day', 'wp-slimstat'), $referrals_yesterday_display)
            . '</div></div>'
            . '</div>';

        $GLOBALS['wp_admin_bar']->add_node([
            'id'     => 'slimstat-adminbar-stats',
            'parent' => 'slimstat-header',
            'title'  => $stats_html,
            'meta'   => ['class' => 'slimstat-adminbar__stats-wrapper'],
        ]);

        // Add chart node
        $chart_wrapper_class = $is_pro ? 'slimstat-adminbar__chart-container' : 'slimstat-adminbar__chart-container slimstat-adminbar__chart-blur';
        $chart_html = '<div class="' . $chart_wrapper_class . '">'
            . '<div class="slimstat-adminbar__chart-bars" id="slimstat-adminbar-chart-bars">' . $chart_bars . '</div>'
            . '</div>';

        $GLOBALS['wp_admin_bar']->add_node([
            'id'     => 'slimstat-adminbar-chart',
            'parent' => 'slimstat-header',
            'title'  => $chart_html,
            'meta'   => ['class' => 'slimstat-adminbar__chart-wrapper'],
        ]);

        // Add CTA node (free users only)
        if (!$is_pro) {
            $cta_html = '<div class="slimstat-adminbar__cta">'
                . '<div class="slimstat-adminbar__cta-text">'
                . esc_html__('Unlock the Full Power of SlimStat Analytics', 'wp-slimstat')
                . '</div>'
                . '<a href="' . esc_url($upgrade_url) . '" target="_blank" class="slimstat-adminbar__cta-button">'
                . esc_html__('Unlock SlimStat Pro', 'wp-slimstat') . '</a>'
                . '</div>';

            $GLOBALS['wp_admin_bar']->add_node([
                'id'     => 'slimstat-adminbar-cta',
                'parent' => 'slimstat-header',
                'title'  => $cta_html,
                'meta'   => ['class' => 'slimstat-adminbar__cta-wrapper'],
            ]);
        }

        // Add footer node
        $footer_html = '<div class="slimstat-adminbar__footer">'
            . '<div class="slimstat-adminbar__footer-logo">'
            . '<svg width="20" height="20" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">'
            . '<path fill-rule="evenodd" clip-rule="evenodd" d="M0 15C0 6.71582 6.7069 0 14.9801 0C20.2546 0 24.8865 2.72788 27.5572 6.84316L19.371 15.1743H19.3643V15.1877C19.0765 15.4893 18.5946 15.496 18.2934 15.2011C18.2599 15.1743 18.2331 15.1408 18.2064 15.1005L15.9239 11.9638C13.9627 9.27614 10.047 9.03485 7.77787 11.4678L0.589029 19.1756C0.194112 17.8217 0 16.4142 0 15ZM2.69079 23.5858C5.40167 27.4665 9.89302 30.0067 14.9801 30.0067C23.2533 30.0067 29.9602 23.2909 29.9602 15.0067C29.9602 13.7399 29.8062 12.5134 29.5117 11.3405L22.604 18.3646C20.3148 20.7172 16.466 20.4424 14.5316 17.7949L12.2491 14.6582C12.0015 14.3231 11.5329 14.2426 11.1916 14.4906C11.1514 14.5174 11.1179 14.5509 11.0845 14.5845L2.69079 23.5858Z" fill="#F22F46"/>'
            . '</svg>'
            . '<span class="slimstat-adminbar__footer-brand">SlimStat</span>'
            . '</div>'
            . '<a href="' . esc_url($overview_url) . '" class="slimstat-adminbar__footer-link">'
            . esc_html__('Explore Details', 'wp-slimstat')
            . ' <span class="dashicons dashicons-external" style="font-size:12px"></span>'
            . '</a></div>';

        $GLOBALS['wp_admin_bar']->add_node([
            'id'     => 'slimstat-adminbar-footer',
            'parent' => 'slimstat-header',
            'title'  => $footer_html,
            'meta'   => ['class' => 'slimstat-adminbar__footer-wrapper'],
        ]);
    }

    // END: add_menu_to_adminbar

    /**
     * Includes the appropriate panel to view the stats
     */
    public static function wp_slimstat_include_view()
    {
        include(__DIR__ . '/view/index.php');
    }

    // END: wp_slimstat_include_view

    /**
     * Includes the screen to arrange the reports
     */
    public static function wp_slimstat_include_layout()
    {
        include(__DIR__ . '/view/layout.php');
    }

    // END: wp_slimstat_include_layout

    /**
     * Includes the email report screen
     */
    public static function wp_slimstat_include_email_report()
    {
        include(__DIR__ . '/view/email-report.php');
    }

    // END: wp_slimstat_include_email_report

    /**
     * Handles the upgrade to pro from the free version
     */
    public static function wp_slimstat_pro()
    {
        include(__DIR__ . '/view/upgrade-pro.php');
    }

    // END: wp_slimstat_include_addons

    /**
     * Includes the appropriate panel to configure Slimstat
     */
    public static function wp_slimstat_include_config()
    {
        include(__DIR__ . '/config/index.php');
    }

    // END: wp_slimstat_include_config

    /**
     * Retrieves all the information to be used in the custom column on posts, pages and CPTs
     */
    public static function init_data_for_column()
    {
        if (!is_array($GLOBALS['wp_query']->posts)) {
            return 0;
        }

        foreach ($GLOBALS['wp_query']->posts as $a_post) {
            self::$data_for_column['url'][$a_post->ID] = parse_url(get_permalink($a_post->ID));
            self::$data_for_column['url'][$a_post->ID] = self::$data_for_column['url'][$a_post->ID]['path'] . (empty(self::$data_for_column['url'][$a_post->ID]['query']) ? '' : '?' . self::$data_for_column['url'][$a_post->ID]['query']);
            self::$data_for_column['sql'][$a_post->ID] = self::$data_for_column['url'][$a_post->ID] . '%';
        }

        /**
         * https://wordpress.org/support/topic/you-have-an-error-in-your-sql-syntax-22/#post-12565619
         */
        if (empty(self::$data_for_column) || empty(self::$data_for_column['url'])) {
            return 0;
        }

        wp_slimstat_db::init('interval equals -' . wp_slimstat::$settings['posts_column_day_interval']);

        $column = ('on' == wp_slimstat::$settings['posts_column_pageviews']) ? 'id' : 'ip';
        $where  = wp_slimstat_db::get_combined_where('(' . implode(' OR ', array_fill(1, count(self::$data_for_column['url']), 'resource LIKE %s')) . ')', '*', true);

        $sql = wp_slimstat::$wpdb->prepare("
			SELECT resource, COUNT( DISTINCT {$column} ) as counthits
			FROM {$GLOBALS['wpdb']->prefix}slim_stats
			WHERE " . $where . '
			GROUP BY resource
			LIMIT 0, ' . wp_slimstat_db::$filters_normalized['misc']['limit_results'], self::$data_for_column['sql']);

        $results = wp_slimstat_db::get_results($sql);

        foreach (self::$data_for_column['url'] as $post_id => $a_url) {
            self::$data_for_column['count'][$post_id] = 0;

            foreach ($results as $i => $a_row) {
                if (false !== strpos($a_row['resource'], (string) $a_url)) {
                    self::$data_for_column['count'][$post_id] += $a_row['counthits'];
                    unset($results[$i]);
                }
            }
        }

        return null;
    }

    // END: init_data_for_column

    /**
     * Adds a new column header to the Posts panel (to show the number of pageviews for each post)
     */
    public static function add_column_header($_columns = [])
    {
        if (0 == wp_slimstat::$settings['posts_column_day_interval']) {
            wp_slimstat::$settings['posts_column_day_interval'] = 28;
        }

        if ('on' == wp_slimstat::$settings['posts_column_pageviews']) {
            $_columns['wp-slimstat'] = '<span class="slimstat-icon" title="' . sprintf(__('Pageviews in the last %s days', 'wp-slimstat'), wp_slimstat::$settings['posts_column_day_interval']) . '"><span class="screen-reader-text">' . __('Views') . '</span></span>';
        } else {
            $_columns['wp-slimstat'] = '<span class="slimstat-icon" title="' . sprintf(__('Unique IPs in the last %s days', 'wp-slimstat'), wp_slimstat::$settings['posts_column_day_interval']) . '"></span>';
        }

        return $_columns;
    }

    // END: add_comment_column_header

    /**
     * Adds a new column to the Posts management panel
     */
    public static function add_post_column($_column_name, $_post_id)
    {
        if ('wp-slimstat' != $_column_name || empty(self::$data_for_column['url'][$_post_id])) {
            return 0;
        }

        $count = empty(self::$data_for_column['count'][$_post_id]) ? 0 : self::$data_for_column['count'][$_post_id];

        echo '<a href="' . wp_slimstat_reports::fs_url('resource starts_with ' . self::$data_for_column['url'][$_post_id] . '&&&interval equals -' . wp_slimstat::$settings['posts_column_day_interval']) . '">' . $count . '</a>';
        return null;
    }

    // END: add_column

    /**
     * Displays an alert message
     */
    public static function show_message($_message = '', $_type = 'info', $_dismiss_handle = '')
    {
        if (empty($_message)) {
            return 0;
        }

        $_message = wpautop(wp_kses_post($_message));

        if (!empty($_dismiss_handle)) {
            echo '<div id="slimstat-notice-' . esc_attr($_dismiss_handle) . '" class="notice is-dismissible slimstat-notice notice-' . esc_attr($_type) . '">' . $_message . '</div>';
        } else {
            echo '<div class="notice notice-' . esc_attr($_type) . ' slimstat-notice">' . $_message . '</div>';
        }

        return null;
    }

    // END: show_message

    /**
     * Displays a message related to the current version of Slimstat
     */
    public static function show_latest_news()
    {
        self::show_message(self::$admin_notice, 'info', 'latest-news');
    }

    // END: show_latest_news


    /**
     * Handles the Ajax request to hide the admin notice
     */
    public static function notices_handler()
    {
        $tag = current_filter();

        if (!empty($tag) && current_user_can('manage_options') && wp_verify_nonce($_POST['security'], 'meta-box-order')) {
            $tag                         = str_replace('wp_ajax_slimstat_', '', $tag);
            wp_slimstat::$settings[$tag] = 'no';

            // Save the default values in the database
            wp_slimstat::update_option('slimstat_options', wp_slimstat::$settings);
        }

        exit();
    }

    // END: notices_handler

    /**
     * Deletes a given pageview from the database
     */
    public static function delete_pageview()
    {
        $my_wpdb     = apply_filters('slimstat_custom_wpdb', $GLOBALS['wpdb']);
        $pageview_id = intval($_POST['pageview_id']);

        // Delete page view if user has enough access
        $current_user_can_delete = (current_user_can(wp_slimstat::$settings['capability_can_admin']) && !is_network_admin());
        if (!$current_user_can_delete || !wp_verify_nonce($_POST['security'], 'meta-box-order')) {
            return;
        }
        $my_wpdb->query(sprintf('DELETE ts FROM %sslim_stats ts WHERE ts.id = %d', $GLOBALS['wpdb']->prefix, $pageview_id));
        exit();
    }

    // END: delete_pageview

    /**
     * Deletes a given pageview from the database
     */
    public static function rmdir($path)
    {
        if (!file_exists($path)) {
            return true;
        }

        if (!is_dir($path)) {
            return unlink($path);
        }

        foreach (scandir($path) as $a_item) {
            if ('.' === $a_item || '..' === $a_item) {
                continue;
            }

            if (!wp_slimstat_admin::rmdir($path . DIRECTORY_SEPARATOR . $a_item)) {
                return false;
            }
        }

        return rmdir($path);
    }

    // END: delete_pageview

    /**
     * Handles the Ajax requests to load, save or delete existing filters
     */
    public static function manage_filters()
    {
        check_ajax_referer('meta-box-order', 'security');

        // If this user is whitelisted, we use the minimum capability
        $minimum_capability = 'read';
        if (false === strpos(wp_slimstat::$settings['can_view'], (string) $GLOBALS['current_user']->user_login) && !empty(wp_slimstat::$settings['capability_can_view'])) {
            $minimum_capability = wp_slimstat::$settings['capability_can_view'];
        }

        if (!current_user_can($minimum_capability)) {
            return;
        }

        // Initialize the new Reports system FIRST before legacy system loads
        \SlimStat\Reports\Bootstrap::get_instance()->init();

        include_once(plugin_dir_path(__FILE__) . 'view/wp-slimstat-reports.php');
        wp_slimstat_reports::init();

        $saved_filters = get_option('slimstat_filters', []);

        switch (sanitize_key(wp_unslash($_POST['type'] ?? ''))) {
            case 'save':
                $new_filter = json_decode(stripslashes_deep(sanitize_text_field($_POST['filter_array'])), true);

                // Check if this filter is already saved
                foreach ($saved_filters as $a_saved_filter) {
                    $filter_found = 0;

                    if (count($a_saved_filter) !== count($new_filter) || count(array_intersect_key($a_saved_filter, $new_filter)) !== count($new_filter)) {
                        $filter_found = 1;
                        continue;
                    }

                    foreach ($a_saved_filter as $a_key => $a_value) {
                        $filter_found += ($a_value == $new_filter[$a_key]) ? 0 : 1;
                    }

                    if (0 == $filter_found) {
                        echo __('Already saved', 'wp-slimstat');
                        break;
                    }
                }

                if (empty($saved_filters) || $filter_found > 0) {
                    $saved_filters[] = $new_filter;
                    update_option('slimstat_filters', $saved_filters);
                    echo __('Saved', 'wp-slimstat');
                }

                break;

            case 'delete':
                unset($saved_filters[intval($_POST['filter_id'])]);
                update_option('slimstat_filters', $saved_filters);

                // no break here - We want to return the new list of filters!

            default:
                echo '<div id="slim_filters_overlay">';
                foreach ($saved_filters as $a_filter_id => $a_filter_data) {

                    $filter_html    = [];
                    $filter_strings = [];
                    foreach ($a_filter_data as $a_filter_label => $a_filter_details) {
                        $filter_value_no_slashes = htmlentities(str_replace('\\', '', $a_filter_details[1]), ENT_QUOTES, 'UTF-8');
                        $filter_html[]           = strtolower(wp_slimstat_db::$columns_names[$a_filter_label][0]) . ' ' . __(str_replace('_', ' ', $a_filter_details[0]), 'wp-slimstat') . ' ' . $filter_value_no_slashes;
                        $filter_strings[]        = sprintf('%s %s %s', $a_filter_label, $a_filter_details[0], $filter_value_no_slashes);
                    }

                    echo '<p><a class="slimstat-font-cancel slimstat-delete-filter" data-filter-id="' . esc_attr($a_filter_id) . '" title="' . __('Delete this filter', 'wp-slimstat') . '" href="#"></a> <a class="slimstat-filter-link" data-reset-filters="true" href="' . wp_slimstat_reports::fs_url(implode('&&&', $filter_strings)) . '">' . implode(', ', $filter_html) . '</a></p>';
                }

                echo '</div>';
                break;
        }

        exit();
    }

    // END: manage_filters

    /**
     * Check AJAX capability for view access.
     * Returns true if user has permission, sends JSON error and returns false otherwise.
     *
     * @since 5.4.3
     * @return bool
     */
    private static function check_ajax_view_capability()
    {
        $minimum_capability = 'read';
        if (false === strpos(wp_slimstat::$settings['can_view'], (string) $GLOBALS['current_user']->user_login) && !empty(wp_slimstat::$settings['capability_can_view'])) {
            $minimum_capability = wp_slimstat::$settings['capability_can_view'];
        }

        if (!current_user_can($minimum_capability)) {
            wp_send_json_error(['message' => esc_html__('Insufficient permissions', 'wp-slimstat')], 403);
            return false;
        }

        return true;
    }

    /**
     * Get online visitors count (30-minute window, session-spanning).
     * Shared by get_online_visitors() and get_adminbar_stats().
     *
     * @since 5.4.3
     * @return int
     */
    private static function query_online_count()
    {
        $wpdb = wp_slimstat::$wpdb;
        $table = "{$GLOBALS['wpdb']->prefix}slim_stats";
        $current_minute_start = (int) floor(wp_slimstat::now() / 60) * 60;
        $window_start = $current_minute_start - (29 * 60); // 30-minute window

        $count = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM (
                SELECT visit_id, MAX(
                    CASE
                        WHEN dt_out IS NOT NULL AND dt_out > 0 AND dt_out >= dt THEN dt_out
                        ELSE dt
                    END
                ) AS last_activity
                FROM {$table}
                WHERE visit_id > 0
                    AND (dt >= %d OR (dt_out IS NOT NULL AND dt_out >= %d))
                GROUP BY visit_id
                HAVING (FLOOR(last_activity / 60) * 60 + 59) >= %d
            ) live_sessions",
            $window_start, $window_start, $window_start
        ));

        return max(0, $count);
    }

    /**
     * AJAX handler to get current online visitors count
     */
    public static function get_online_visitors()
    {
        check_ajax_referer('meta-box-order', 'security');
        if (!self::check_ajax_view_capability()) {
            return;
        }

        $online_visitors = self::query_online_count();

        wp_send_json_success([
            'count' => $online_visitors,
            'formatted' => number_format_i18n($online_visitors)
        ]);
    }

    // END: get_online_visitors

    /**
     * AJAX handler: Returns all admin bar modal stats in a single request.
     * Called every minute by adminbar-realtime.js and admin.js.
     *
     * @since 5.4.3
     */
    public static function get_adminbar_stats()
    {
        check_ajax_referer('meta-box-order', 'security');
        if (!self::check_ajax_view_capability()) {
            return;
        }

        $wpdb = wp_slimstat::$wpdb;
        $table = "{$GLOBALS['wpdb']->prefix}slim_stats";
        $is_pro = wp_slimstat::pro_is_installed();

        // --- Online count (always fresh — fast indexed query) ---
        $online_count = self::query_online_count();

        // --- Today stats (transient-cached, 60s TTL) ---
        $blog_id = get_current_blog_id();
        $transient_key = 'slimstat_adminbar_today_' . $blog_id;
        $today_stats = get_transient($transient_key);

        if (false === $today_stats) {
            $today_start = strtotime('today', current_time('timestamp'));
            $yesterday_start = $today_start - DAY_IN_SECONDS;
            $yesterday_end = $today_start - 1;
            $site_host = parse_url(home_url(), PHP_URL_HOST);
            $referer_like = '%' . $wpdb->esc_like($site_host) . '%';

            // Sessions + views: 2 queries instead of 4 using conditional aggregates
            $row = $wpdb->get_row($wpdb->prepare(
                "SELECT
                    COUNT(DISTINCT CASE WHEN dt >= %d AND visit_id > 0 THEN visit_id END) AS sessions_today,
                    COUNT(DISTINCT CASE WHEN dt BETWEEN %d AND %d AND visit_id > 0 THEN visit_id END) AS sessions_yesterday,
                    SUM(CASE WHEN dt >= %d THEN 1 ELSE 0 END) AS views_today,
                    SUM(CASE WHEN dt BETWEEN %d AND %d THEN 1 ELSE 0 END) AS views_yesterday
                FROM {$table}
                WHERE dt >= %d",
                $today_start,
                $yesterday_start, $yesterday_end,
                $today_start,
                $yesterday_start, $yesterday_end,
                $yesterday_start
            ));

            // Referrals: 1 query with conditional aggregates
            $ref_row = $wpdb->get_row($wpdb->prepare(
                "SELECT
                    SUM(CASE WHEN dt >= %d THEN 1 ELSE 0 END) AS referrals_today,
                    SUM(CASE WHEN dt BETWEEN %d AND %d THEN 1 ELSE 0 END) AS referrals_yesterday
                FROM {$table}
                WHERE dt >= %d AND referer IS NOT NULL AND referer NOT LIKE %s",
                $today_start,
                $yesterday_start, $yesterday_end,
                $yesterday_start,
                $referer_like
            ));

            $today_stats = [
                'sessions'             => (int) ($row->sessions_today ?? 0),
                'sessions_yesterday'   => (int) ($row->sessions_yesterday ?? 0),
                'views'                => (int) ($row->views_today ?? 0),
                'views_yesterday'      => (int) ($row->views_yesterday ?? 0),
                'referrals'            => (int) ($ref_row->referrals_today ?? 0),
                'referrals_yesterday'  => (int) ($ref_row->referrals_yesterday ?? 0),
            ];

            $ttl = max(60 - (current_time('timestamp') % 60), 1); // align to next minute boundary
            set_transient($transient_key, $today_stats, $ttl);
        }

        // --- Chart data (uses LiveAnalyticsReport's own 60s transient) ---
        $chart_data = null;
        if ($is_pro) {
            try {
                $live_report = new \SlimStat\Reports\Types\Analytics\LiveAnalyticsReport();
                $chart_result = $live_report->get_users_chart_data();
                $chart_data = [
                    'data'       => $chart_result['data'],
                    'max_value'  => $chart_result['max_value'],
                    'peak_index' => $chart_result['peak_index'],
                ];
            } catch (\Exception $e) {
                // Graceful degradation — return stats without chart data
                $chart_data = null;
            }
        }

        // --- Build response ---
        $response = [
            'online' => [
                'count'     => $online_count,
                'formatted' => number_format_i18n($online_count),
            ],
            'sessions' => [
                'count'     => $today_stats['sessions'],
                'formatted' => number_format_i18n($today_stats['sessions']),
                'yesterday' => number_format_i18n($today_stats['sessions_yesterday']),
            ],
            'is_pro' => $is_pro,
        ];

        if ($is_pro) {
            $response['views'] = [
                'count'     => $today_stats['views'],
                'formatted' => number_format_i18n($today_stats['views']),
                'yesterday' => number_format_i18n($today_stats['views_yesterday']),
            ];
            $response['referrals'] = [
                'count'     => $today_stats['referrals'],
                'formatted' => number_format_i18n($today_stats['referrals']),
                'yesterday' => number_format_i18n($today_stats['referrals_yesterday']),
            ];
            $response['chart'] = $chart_data;
        }

        wp_send_json_success($response);
    }

    // END: get_adminbar_stats

    /**
     * Helper function to get icon URL for filter options
     */
    private static function get_filter_icon_url($dimension, $value)
    {
        $icon_url = '';

        switch ($dimension) {
            case 'country':
                // Country flags are SVG files named by country code (lowercase)
                $country_code = strtolower($value);
                $flag_rel = '/admin/assets/images/flags/' . $country_code . '.svg';
                $flag_path = SLIMSTAT_ANALYTICS_DIR . $flag_rel;
                if (is_readable($flag_path)) {
                    $icon_url = SLIMSTAT_ANALYTICS_URL . $flag_rel;
                }
                break;

            case 'browser':
                // Browser icons are PNG files named by browser name (lowercase)
                $browser_name = strtolower($value);
                $browser_rel = '/admin/assets/images/browsers/' . $browser_name . '.png';
                $browser_path = SLIMSTAT_ANALYTICS_DIR . $browser_rel;
                if (is_readable($browser_path)) {
                    $icon_url = SLIMSTAT_ANALYTICS_URL . $browser_rel;
                }
                break;

            case 'language':
                // Language flags use the last part of the language code (e.g., en-US -> us)
                $language_parts = explode('-', $value);
                $last_part = strtolower(end($language_parts));
                $flag_rel = '/admin/assets/images/flags/' . $last_part . '.svg';
                $flag_path = SLIMSTAT_ANALYTICS_DIR . $flag_rel;
                if (is_readable($flag_path)) {
                    $icon_url = SLIMSTAT_ANALYTICS_URL . $flag_rel;
                }
                break;

            case 'platform':
                // Platform/OS icons are WEBP files with abbreviated names
                $os_map = [
                    'win' => 'win',
                    'windows' => 'win',
                    'mac' => 'mac',
                    'macosx' => 'mac',
                    'linux' => 'lin',
                    'ubuntu' => 'ubu',
                    'android' => 'and',
                    'ios' => 'ios',
                    'chrome os' => 'chr',
                    'chromeos' => 'chr',
                ];

                $os_lower = strtolower($value);
                $os_icon = null;

                // Check if exact match exists in map
                if (isset($os_map[$os_lower])) {
                    $os_icon = $os_map[$os_lower];
                } else {
                    // Check if value contains any of the keys
                    foreach ($os_map as $key => $icon) {
                        if (strpos($os_lower, $key) !== false) {
                            $os_icon = $icon;
                            break;
                        }
                    }
                }

                if ($os_icon) {
                    $os_rel = '/admin/assets/images/os/' . $os_icon . '.webp';
                    $os_path = SLIMSTAT_ANALYTICS_DIR . $os_rel;
                    if (is_readable($os_path)) {
                        $icon_url = SLIMSTAT_ANALYTICS_URL . $os_rel;
                    }
                }
                break;

            case 'username':
                // For users, we'll use WordPress gravatar
                // This will be handled separately in the JavaScript
                break;
        }

        return $icon_url;
    }

    /**
     * AJAX handler to get distinct filter options for a selected dimension
     */
    public static function get_filter_options()
    {
        check_ajax_referer('meta-box-order', 'security');

        // If this user is whitelisted, we use the minimum capability
        $minimum_capability = 'read';
        if (false === strpos(wp_slimstat::$settings['can_view'], (string) $GLOBALS['current_user']->user_login) && !empty(wp_slimstat::$settings['capability_can_view'])) {
            $minimum_capability = wp_slimstat::$settings['capability_can_view'];
        }

        if (!current_user_can($minimum_capability)) {
            wp_send_json_error('Insufficient permissions');
            return;
        }

        $dimension = sanitize_text_field($_POST['dimension'] ?? '');

        // Validate dimension exists in columns_names
        include_once(plugin_dir_path(__FILE__) . 'view/wp-slimstat-db.php');

        // We only need the columns_names array, not the full init with filters
        if (empty(wp_slimstat_db::$columns_names)) {
            wp_slimstat_db::$columns_names = [
                'id' => ['ID', 'number'],
                'ip' => ['IP', 'varchar'],
                'other_ip' => ['Other IP', 'varchar'],
                'username' => ['Username', 'varchar'],
                'email' => ['Email', 'varchar'],
                'country' => ['Country', 'varchar'],
                'location' => ['Location', 'varchar'],
                'city' => ['City', 'varchar'],
                'referer' => ['Referer', 'varchar'],
                'resource' => ['Resource', 'varchar'],
                'searchterms' => ['Search Terms', 'varchar'],
                'notes' => ['Notes', 'varchar'],
                'visit_id' => ['Visit ID', 'number'],
                'server_latency' => ['Server Latency', 'number'],
                'page_performance' => ['Page Performance', 'number'],
                'browser' => ['Browser', 'varchar'],
                'browser_version' => ['Browser Version', 'varchar'],
                'browser_type' => ['Browser Type', 'number'],
                'platform' => ['Platform', 'varchar'],
                'language' => ['Language', 'varchar'],
                'fingerprint' => ['Fingerprint', 'varchar'],
                'user_agent' => ['User Agent', 'varchar'],
                'resolution' => ['Resolution', 'varchar'],
                'screen_width' => ['Screen Width', 'number'],
                'screen_height' => ['Screen Height', 'number'],
                'content_type' => ['Content Type', 'varchar'],
                'category' => ['Category', 'varchar'],
                'author' => ['Author', 'varchar'],
                'content_id' => ['Content ID', 'number'],
                'outbound_resource' => ['Outbound Resource', 'varchar'],
                'tz_offset' => ['Timezone Offset', 'number'],
                'dt_out' => ['Date Time Out', 'number'],
                'dt' => ['Date Time', 'number'],
            ];
        }

        if (empty($dimension) || !isset(wp_slimstat_db::$columns_names[$dimension])) {
            wp_send_json_error('Invalid dimension');
            return;
        }

        // Get time range parameters from AJAX request
        $time_range_type = sanitize_text_field($_POST['time_range_type'] ?? 'last_28_days');
        $time_range_from = sanitize_text_field($_POST['time_range_from'] ?? '');
        $time_range_to = sanitize_text_field($_POST['time_range_to'] ?? '');

        // Calculate time range timestamps
        $time_start = null;
        $time_end = null;

        if ($time_range_type === 'custom' && !empty($time_range_from) && !empty($time_range_to)) {
            // Custom date range
            $time_start = strtotime($time_range_from);
            $time_end = strtotime($time_range_to . ' 23:59:59');
        } else {
            // Preset date range
            $preset_range = DateRangeHelper::get_range_by_preset($time_range_type);
            if ($preset_range) {
                $time_start = $preset_range['start'];
                $time_end = $preset_range['end'];
            }
        }

        // Fallback to last 28 days if no valid time range
        if (empty($time_start) || empty($time_end)) {
            $preset_range = DateRangeHelper::get_range_by_preset('last_28_days');
            if ($preset_range) {
                $time_start = $preset_range['start'];
                $time_end = $preset_range['end'];
            }
        }

        // Get distinct values for this dimension via SlimStat\Utils\Query abstraction
        $table_name = $GLOBALS['wpdb']->prefix . 'slim_stats';

        // Limit results to prevent overwhelming the dropdown (filterable for customization)
        $limit = apply_filters('slimstat_filter_options_limit', 500, $dimension);
        $limit = absint($limit); // Ensure it's a positive integer

        // Enforce reasonable bounds to prevent abuse
        if ($limit < 1 || $limit > 5000) {
            $limit = 500; // Reset to default if out of reasonable range
        }

        // Sanitize column name to prevent SQL injection (only allow known columns)
        $allowed_columns = array_keys(wp_slimstat_db::$columns_names);
        if (!in_array($dimension, $allowed_columns, true)) {
            wp_send_json_error('Invalid column');
            return;
        }

        // Additional sanitization layer for column name (defense in depth)
        $safe_dimension = esc_sql($dimension);

        // Get distinct non-empty values
        $column_type = wp_slimstat_db::$columns_names[$dimension][1];

        // Build SQL query directly to avoid Query class interference with global filters
        $where_clauses = [];

        // Apply time range filter
        if (!empty($time_start) && !empty($time_end)) {
            $where_clauses[] = $GLOBALS['wpdb']->prepare('dt BETWEEN %d AND %d', intval($time_start), intval($time_end));
        }

        if ($column_type === 'varchar') {
            // Exclude NULLs and empty strings for varchar columns
            $where_clauses[] = $safe_dimension . ' IS NOT NULL';
            $where_clauses[] = $safe_dimension . " <> ''";
        } else {
            // Exclude NULLs and zeros for numeric columns
            $where_clauses[] = $safe_dimension . ' IS NOT NULL';
            $where_clauses[] = $safe_dimension . ' <> 0';
        }

        $where_sql = !empty($where_clauses) ? 'WHERE ' . implode(' AND ', $where_clauses) : '';

        $sql = sprintf(
            'SELECT DISTINCT %s as value FROM %s %s ORDER BY %s ASC LIMIT %d',
            $safe_dimension,
            $table_name,
            $where_sql,
            $safe_dimension,
            $limit
        );

        // Execute query — use wp_slimstat::$wpdb so External DB addon
        // queries the correct database.
        $results = wp_slimstat::$wpdb->get_results($sql, ARRAY_A);

        // Check for database errors
        if (wp_slimstat::$wpdb->last_error) {
            wp_send_json_error('Database query failed');
            return;
        }

        // Ensure results is an array
        if (!is_array($results)) {
            $results = [];
        }

        // Split multi-value columns into individual values.
        // These columns store multiple entries in a single DB field:
        //   outbound_resource: "url1;;;url2;;;url3"
        //   notes:             "[tag1][tag2][tag3]"
        //   category:          "1,5,12"
        $multi_value_separators = [
            'outbound_resource' => ';;;',
            'category'          => ',',
        ];

        if (isset($multi_value_separators[$dimension])) {
            $separator = $multi_value_separators[$dimension];
            $expanded = [];
            foreach ($results as $row) {
                if (empty($row['value'])) continue;
                foreach (explode($separator, $row['value']) as $val) {
                    $val = trim($val);
                    if ($val !== '') $expanded[] = ['value' => $val];
                }
            }
            $results = $expanded;
        } elseif ($dimension === 'notes') {
            $expanded = [];
            foreach ($results as $row) {
                if (empty($row['value'])) continue;
                preg_match_all('/\[([^\]]+)\]/', $row['value'], $matches);
                foreach ($matches[1] as $val) {
                    $val = trim($val);
                    if ($val !== '') $expanded[] = ['value' => $val];
                }
            }
            $results = $expanded;
        }

        // Cap expanded results to prevent explosion from splitting
        if (count($results) > $limit) {
            $results = array_slice($results, 0, $limit);
        }

        $options = [];
        $seen_values = []; // Track values to prevent duplicates (case-insensitive)
        $dimensions_with_icons = ['country', 'browser', 'language', 'platform', 'username'];
        $has_icons = in_array($dimension, $dimensions_with_icons, true);

        foreach ($results as $row) {
            if (!empty($row['value'])) {
                // Sanitize output to prevent XSS
                $sanitized_value = sanitize_text_field($row['value']);

                // Trim whitespace
                $sanitized_value = trim($sanitized_value);

                // Skip empty values after trimming
                if (empty($sanitized_value)) {
                    continue;
                }

                // Check for duplicates using case-insensitive comparison
                $value_key = strtolower($sanitized_value);
                if (isset($seen_values[$value_key])) {
                    continue; // Skip duplicate
                }

                // Mark this value as seen
                $seen_values[$value_key] = true;

                // Limit individual option length to prevent DOM issues
                if (strlen($sanitized_value) > 255) {
                    $sanitized_value = substr($sanitized_value, 0, 255) . '...';
                }

                if ($has_icons) {
                    // Return object with value and icon
                    $icon_url = self::get_filter_icon_url($dimension, $sanitized_value);

                    // For username, get user gravatar
                    if ($dimension === 'username' && empty($icon_url)) {
                        $user = get_user_by('login', $sanitized_value);
                        if ($user) {
                            $icon_url = get_avatar_url($user->ID, ['size' => 32]);
                        } else {
                            $icon_url = get_avatar_url($sanitized_value, ['size' => 32]);
                        }
                    }

                    $options[] = [
                        'value' => $sanitized_value,
                        'label' => $sanitized_value,
                        'icon' => $icon_url
                    ];
                } else {
                    // Return simple string for backward compatibility
                    $options[] = $sanitized_value;
                }
            }
        }

        wp_send_json_success($options);
        exit();
    }

    // END: get_filter_options

	public static function update_geoip_database()
	{
		check_ajax_referer('slimstat_geoip_action', 'security');

		if (!current_user_can(\wp_slimstat::$settings['capability_can_admin'])) {
			wp_send_json_error(__('Permission denied', 'wp-slimstat'));
			return;
		}

		try {
			$provider = \wp_slimstat::resolve_geolocation_provider();
			if (false === $provider) {
				wp_send_json_error(__('Geolocation is disabled.', 'wp-slimstat'));
				return;
			}
            if ('cloudflare' === $provider) {
                update_option('slimstat_last_geoip_dl', time());
                wp_send_json_success(__('Cloudflare geolocation does not require a database.', 'wp-slimstat'));
                return;
            }

            // License validation is handled by the MaxMind provider; do not pre-check here

            $service = new \SlimStat\Services\Geolocation\GeolocationService($provider, []);
            $ok      = $service->updateDatabase();

			if ($ok) {
                update_option('slimstat_last_geoip_dl', time());
                wp_send_json_success(__('GeoIP Database Successfully Updated!', 'wp-slimstat'));
            } else {
                // Log the error for debugging
				$error_message = __('Failed to update GeoIP Database.', 'wp-slimstat');
				if ('maxmind' === $provider) {
					$error_message .= ' ' . __('Please check your MaxMind license key and try again.', 'wp-slimstat');
				}
				$geoip_error = get_option('slimstat_geoip_error', []);
				if (!empty($geoip_error) && !empty($geoip_error['error'])) {
					$error_message .= ' ' . sprintf(__('Details: %s', 'wp-slimstat'), $geoip_error['error']);
				}
				wp_send_json_error($error_message);
            }
        } catch (\Throwable $exception) {
            \wp_slimstat::log('GeoIP update AJAX error: ' . $exception->getMessage() . ' in ' . $exception->getFile() . ':' . $exception->getLine(), 'error');
            wp_send_json_error(__('An unexpected error occurred while updating the GeoIP database.', 'wp-slimstat'));
        }
    }

	public static function check_geoip_database()
	{
		check_ajax_referer('slimstat_geoip_action', 'security');

		if (!current_user_can(\wp_slimstat::$settings['capability_can_admin'])) {
			wp_send_json_error(__('Permission denied', 'wp-slimstat'));
			return;
		}

		try {
			$provider = \wp_slimstat::resolve_geolocation_provider();
			if (false === $provider) {
				wp_send_json_error(__('Geolocation is disabled.', 'wp-slimstat'));
				return;
			}
            if ('cloudflare' === $provider) {
                wp_send_json_success(__('Cloudflare geolocation is active. No database to check.', 'wp-slimstat'));
                return;
            }
            $service = new \SlimStat\Services\Geolocation\GeolocationService($provider, []);
            $exists  = file_exists($service->getProvider()->getDbPath());
            $result  = [ 'notice' => $exists ? __('GeoIP Database is present and ready.', 'wp-slimstat') : __('GeoIP Database not found.', 'wp-slimstat') ];

            wp_send_json_success($result['notice']);
        } catch (\Throwable $exception) {
            \wp_slimstat::log('GeoIP check AJAX error: ' . $exception->getMessage() . ' in ' . $exception->getFile() . ':' . $exception->getLine(), 'error');
            wp_send_json_error(__('An unexpected error occurred while checking the GeoIP database.', 'wp-slimstat'));
        }
    }

    /**
     * Contextual help
     */
    public static function contextual_help()
    {
        // This contextual help is only available to those using WP 3.3 or newer
        if (empty($GLOBALS['wp_version']) || version_compare($GLOBALS['wp_version'], '3.3', '<')) {
            return true;
        }

        $screen = get_current_screen();

        $screen->add_help_tab(
            [
                'id'      => 'wp-slimstat-definitions',
                'title'   => __('Definitions', 'wp-slimstat'),
                'content' => '<ul>
                    <li><b>' . __('Pageview', 'wp-slimstat') . '</b>: ' . __('A request to load a single HTML file ("page"). This should be contrasted with a "hit", which refers to a request for any file from a web server. Slimstat logs a pageview each time the tracking code is executed', 'wp-slimstat') . '</li>
                    <li><b>' . __('(Human) Visit', 'wp-slimstat') . '</b>: ' . __("A period of interaction between a visitor's browser and your website, ending when the browser is closed or when the user has been inactive on that site for 30 minutes", 'wp-slimstat') . '</li>
                    <li><b>' . __('Known Visitor', 'wp-slimstat') . '</b>: ' . __('Any user who has left a comment on your blog, and is thus identified by WordPress as a returning visitor', 'wp-slimstat') . '</li>
                    <li><b>' . __('Unique IP', 'wp-slimstat') . '</b>: ' . __('Used to differentiate between multiple requests to download a file from one internet address (IP) and requests originating from many distinct addresses; since this measurement looks only at the internet address a pageview came from, it is useful, but not perfect', 'wp-slimstat') . '</li>
                    <li><b>' . __('Originating IP', 'wp-slimstat') . '</b>: ' . __('the originating IP address of a client connecting to a web server through an HTTP proxy or load balancer', 'wp-slimstat') . '</li>
                    <li><b>' . __('Direct Traffic', 'wp-slimstat') . '</b>: ' . __('All those people showing up to your Web site by typing in the URL of your Web site coming or from a bookmark; some people also call this "default traffic" or "ambient traffic"', 'wp-slimstat') . '</li>
                    <li><b>' . __('Search Engine', 'wp-slimstat') . '</b>: ' . __('Google, Yahoo, MSN, Ask, others; this bucket will include both your organic as well as your paid (PPC/SEM) traffic, so be aware of that', 'wp-slimstat') . '</li>
                    <li><b>' . __('Search Terms', 'wp-slimstat') . '</b>: ' . __('Keywords used by your visitors to find your website on a search engine', 'wp-slimstat') . '</li>
                    <li><b>' . __('SERP', 'wp-slimstat') . '</b>: ' . __('Short for search engine results page, the Web page that a search engine returns with the results of its search. The value shown represents your rank (or position) within that list of results', 'wp-slimstat') . '</li>
                    <li><b>' . __('User Agent', 'wp-slimstat') . '</b>: ' . __('Any program used for accessing a website; this includes browsers, robots, spiders and any other program that was used to retrieve information from the site', 'wp-slimstat') . '</li>
                    <li><b>' . __('Outbound Link', 'wp-slimstat') . '</b>: ' . __('A link from one domain to another is said to be outbound from its source anchor and inbound to its target. This report lists all the links to other websites followed by your visitors.', 'wp-slimstat') . '</li>
                </ul>',
            ]
        );
        $screen->add_help_tab(
            [
                'id'      => 'wp-slimstat-basic-filters',
                'title'   => __('Basic Filters', 'wp-slimstat'),
                'content' => '<ul>
                    <li><b>' . __('Browser', 'wp-slimstat') . '</b>: ' . __('User agent (Firefox, Chrome, ...)', 'wp-slimstat') . '</li>
                    <li><b>' . __('Country Code', 'wp-slimstat') . '</b>: ' . __('2-letter code (us, ru, de, it, ...)', 'wp-slimstat') . '</li>
                    <li><b>' . __('IP', 'wp-slimstat') . '</b>: ' . __("Visitor's public IP address", 'wp-slimstat') . '</li>
                    <li><b>' . __('Search Terms', 'wp-slimstat') . '</b>: ' . __('Keywords used by your visitors to find your website on a search engine', 'wp-slimstat') . '</li>
                    <li><b>' . __('Language Code', 'wp-slimstat') . '</b>: ' . __('Please refer to this <a target="_blank" href="https://msdn.microsoft.com/en-us/library/ee825488(v=cs.20).aspx">language culture page</a> (first column) for more information', 'wp-slimstat') . '</li>
                    <li><b>' . __('Operating System', 'wp-slimstat') . '</b>: ' . __('Accepts identifiers like win7, win98, macosx, ...; please refer to <a target="_blank" href="https://php.net/manual/en/function.get-browser.php">this manual page</a> for more information', 'wp-slimstat') . '</li>
                    <li><b>' . __('Permalink', 'wp-slimstat') . '</b>: ' . __('URL accessed on your site', 'wp-slimstat') . '</li>
                    <li><b>' . __('Referer', 'wp-slimstat') . '</b>: ' . __('Complete address of the referrer page', 'wp-slimstat') . '</li>
                    <li><b>' . __("Visitor's Name", 'wp-slimstat') . '</b>: ' . __("Visitors' names according to the cookie set by WordPress after they leave a comment", 'wp-slimstat') . '</li>
                </ul>',
            ]
        );

        $screen->add_help_tab(
            [
                'id'      => 'wp-slimstat-advanced-filters',
                'title'   => __('Advanced Filters', 'wp-slimstat'),
                'content' => '<ul>
                        <li><b>' . __('Browser Version', 'wp-slimstat') . '</b>: ' . __('user agent version (9.0, 11, ...)', 'wp-slimstat') . '</li>
                        <li><b>' . __('Browser Type', 'wp-slimstat') . '</b>: ' . __('1 = search engine crawler, 2 = mobile device, 3 = syndication reader, 0 = all others', 'wp-slimstat') . '</li>
                        <li><b>' . __('Pageview Attributes', 'wp-slimstat') . '</b>: ' . __('this field is set to <em>[pre]</em> if the resource has been accessed through <a target="_blank" href="https://developer.mozilla.org/en/Link_prefetching_FAQ">Link Prefetching</a> or similar techniques', 'wp-slimstat') . '</li>
                        <li><b>' . __('Post Author', 'wp-slimstat') . '</b>: ' . __('author associated to that post/page when the resource was accessed', 'wp-slimstat') . '</li>
                        <li><b>' . __('Post Category ID', 'wp-slimstat') . '</b>: ' . __('ID of the category/term associated to the resource, when available', 'wp-slimstat') . '</li>
                        <li><b>' . __('Originating IP', 'wp-slimstat') . '</b>: ' . __("visitor's originating IP address, if available", 'wp-slimstat') . '</li>
                        <li><b>' . __('Resource Content Type', 'wp-slimstat') . '</b>: ' . __('post, page, cpt:<em>custom-post-type</em>, cpt:attachment, singular, post_type_archive, tag, taxonomy, category, date, author, archive, search, feed, home; please refer to the <a target="_blank" href="https://codex.wordpress.org/Conditional_Tags">Conditional Tags</a> manual page for more information', 'wp-slimstat') . '</li>
                        <li><b>' . __('Screen Resolution', 'wp-slimstat') . '</b>: ' . __('viewport width and height (1024x768, 800x600, ...)', 'wp-slimstat') . '</li>
                        <li><b>' . __('Visit ID', 'wp-slimstat') . '</b>: ' . __('generally used in conjunction with <em>is not empty</em>, identifies human visitors', 'wp-slimstat') . '</li>
                        <li><b>' . __('Date Filters', 'wp-slimstat') . '</b>: ' . __('you can specify the timeframe by entering a number in the <em>interval</em> field; use -1 to indicate <em>to date</em> (i.e., day=1, month=1, year=blank, interval=-1 will set a year-to-date filter)', 'wp-slimstat') . '</li>
                        <li><b>' . __('SERP Position', 'wp-slimstat') . '</b>: ' . __('set the filter to Referer contains cd=N&, where N is the position you are looking for', 'wp-slimstat') . '</li>
                </ul>',
            ]
        );
        return null;
    }

    // END: contextual_help

    /**
     * Creates a table in the database
     */
    protected static function _create_table($_sql = '', $_tablename = '', $_wpdb = '')
    {
        $_wpdb->query($_sql);

        // Let's make sure this table was actually created
        foreach ($_wpdb->get_col(sprintf("SHOW TABLES LIKE '%s'", $_tablename), 0) as $a_table) {
            if ($a_table == $_tablename) {
                return true;
            }
        }

        return false;
    }

    // END: _create_table

    /**
     * Init FeedbackBird widget a third-party service to get feedbacks from users
     *
     * @url https://feedbackbird.io
     */
    private static function initFeedback()
    {
        add_action('admin_enqueue_scripts', function () {
            $screen = get_current_screen();

            if (stristr($screen->id, 'slimview')) {
                wp_register_script('feedbackbird-widget', 'https://cdn.jsdelivr.net/gh/feedbackbird/assets@master/wp/app.js?uid=01H5FBKA9Z5M2VJWQXZSX4Q7MS', [], null, true);
                add_filter('script_loader_tag', function ($tag, $handle) {
                    if ('feedbackbird-widget' === $handle) {
                        $tag = str_replace('<script ', '<script defer ', $tag);
                    }
                    return $tag;
                }, 10, 2);
                wp_enqueue_script('feedbackbird-widget');
                wp_add_inline_script('feedbackbird-widget', sprintf('var feedbackBirdObject = %s;', wp_json_encode([
                    'user_email' => function_exists('wp_get_current_user') ? esc_attr(wp_get_current_user()->user_email) : '',
                    'platform'   => 'wordpress-admin',
                    'config'     => [
                        'color'         => '#e8294c',
                        'button'        => __('Feedback', 'wp-sms'),
                        'subtitle'      => __('Feel free to share your thoughts!', 'wp-sms'),
                        'opening_style' => 'modal',
                    ],
                    'meta' => [
                        'php_version'    => PHP_VERSION,
                        'active_plugins' => array_map(fn ($plugin, $pluginPath) => [
                            'name'    => $plugin['Name'],
                            'version' => $plugin['Version'],
                            'status'  => is_plugin_active($pluginPath) ? 'active' : 'deactivate',
                        ], get_plugins(), array_keys(get_plugins())),
                    ],
                ])));

                add_filter('script_loader_tag', function ($tag, $handle, $src) {
                    if ('feedbackbird-widget' === $handle) {
                        return preg_replace('/^<script /i', '<script type="module" crossorigin="crossorigin" ', $tag);
                    }

                    return $tag;
                }, 10, 3);
            }
        });
    }

    public static function get_template($template, $args = [], $return = false)
    {
        // Push Args - use EXTR_SKIP to prevent variable overwriting for security
        if (is_array($args) && isset($args)) :
            extract($args, EXTR_SKIP);
        endif;

        // Check Load single file or array list
        if (is_string($template)) {
            $template = explode(' ', $template);
        }

        // Load File
        foreach ($template as $file) {
            $template_file = WP_PLUGIN_DIR . sprintf('/wp-slimstat/admin/view/partials/%s.php', $file);

            if (!file_exists($template_file)) {
                continue;
            }

            if ($return) {
                ob_start();
                require $template_file;

                return ob_get_clean();
            }

            // include File
            include $template_file;
        }

        return null;
    }

    public static function add_lock_export_button($_header_buttons = '', $_report_id = '')
    {
        // If the pro is active don't show it
        $pro_plugin_slug = 'wp-slimstat-pro/wp-slimstat-pro.php';
        if (is_plugin_active($pro_plugin_slug)) {
            return $_header_buttons;
        }

        // Define which reports get this new functionality
        if (empty(\wp_slimstat_reports::$reports[$_report_id]['callback_args']) || !array_key_exists('raw', \wp_slimstat_reports::$reports[$_report_id]['callback_args'])) {
            return $_header_buttons;
        }
        $utm_medium = empty($_report_id) ? 'report-unknown' : $_report_id;
        return '<a class="slimstat-upgrade-pro slimstat-filter-link slimstat-filter-temp button-export-to-xls slimstat-font-download is-not-pro noslimstat" title="' . __('Upgrade to Pro', 'wp-slimstat-pro') . '" href="https://wp-slimstat.com/pricing/?utm_source=admin&utm_medium=' . $utm_medium . '&utm_campaign=export" target="_blank"><span class="dashicons dashicons-download"></span>' . __('Export', 'wp-slimstat-pro') . '</a> ' . $_header_buttons;
    }

    public static function add_header()
    {
        if (isset($_GET['page']) && ('slimlayout' === $_GET['page'] || 'slimconfig' === $_GET['page'])) {
            return self::get_template('header', ['is_pro' => wp_slimstat::pro_is_installed()]);
        }

        return null;
    }

    /**
     * Index definitions for all AJAX-managed database indexes.
     * Each entry maps an AJAX action (nonce) to its index metadata.
     */
    private static function get_index_definitions(): array
    {
        $prefix = $GLOBALS['wpdb']->prefix;
        return [
            'slimstat_add_country_dt_index' => [
                'name'    => 'idx_country_dt',
                'columns' => 'country, dt',
                'option'  => 'slimstat_country_dt_indexed',
            ],
            'slimstat_add_dt_screen_index' => [
                'name'    => 'idx_dt_screen_width_screen_height',
                'columns' => 'dt, screen_width, screen_height',
                'option'  => 'slimstat_dt_screen_indexed',
            ],
            'slimstat_add_dt_browser_index' => [
                'name'    => 'idx_dt_browser_browser_version',
                'columns' => 'dt, browser, browser_version',
                'option'  => 'slimstat_dt_browser_indexed',
            ],
            'slimstat_add_dt_platform_index' => [
                'name'    => 'idx_dt_platform',
                'columns' => 'dt, platform',
                'option'  => 'slimstat_dt_platform_indexed',
            ],
            'slimstat_add_dt_out_index' => [
                'name'    => 'idx_dt_out',
                'columns' => 'dt_out',
                'option'  => 'slimstat_dt_out_indexed',
            ],
            'slimstat_add_dt_visit_index' => [
                'name'    => $prefix . 'stats_dt_visit_idx',
                'columns' => 'dt, visit_id',
                'option'  => 'slimstat_dt_visit_indexed',
            ],
        ];
    }

    /**
     * Generic AJAX handler for ensuring a database index exists.
     */
    private static function ajax_ensure_index(string $nonce, string $index_name, string $columns, string $option_key): void
    {
        check_ajax_referer($nonce);
        if (!current_user_can('manage_options')) {
            wp_send_json_error(__('Insufficient permissions.', 'wp-slimstat'));
        }
        $wpdb = wp_slimstat::$wpdb;
        $table = $GLOBALS['wpdb']->prefix . 'slim_stats';
        $exists = $wpdb->get_results(sprintf("SHOW INDEX FROM %s WHERE Key_name = '%s'", $table, $index_name));
        if (!empty($exists)) {
            update_option($option_key, 'yes');
            wp_send_json_success(__('Index already exists.', 'wp-slimstat'));
        }
        $result = $wpdb->query(sprintf('CREATE INDEX %s ON %s (%s)', $index_name, $table, $columns));
        if (false !== $result) {
            update_option($option_key, 'yes');
            wp_send_json_success(__('Index added successfully.', 'wp-slimstat'));
        }
        wp_send_json_error(__('Unable to add index.', 'wp-slimstat'));
    }

    /**
     * Register AJAX hooks for all index management actions.
     */
    public static function register_index_hooks(): void
    {
        foreach (self::get_index_definitions() as $action => $def) {
            add_action('wp_ajax_' . $action, function () use ($action, $def) {
                self::ajax_ensure_index($action, $def['name'], $def['columns'], $def['option']);
            });
        }
    }

    public static function show_indexes_notice()
    {
		// If new migration system is active, suppress legacy performance notice
		if (class_exists(\SlimStat\Migration\Admin\MigrationAdmin::class)) {
			return;
		}

        if (!current_user_can('manage_options')) {
            return;
        }
        $indexes = [
            [
                'option' => 'slimstat_dt_out_indexed',
                'id'     => 'dt-out',
                'label'  => __('Currently Online Reports', 'wp-slimstat'),
                'desc'   => __('Index on <code>dt_out</code>', 'wp-slimstat'),
                'key'    => 'idx_dt_out',
                'ajax'   => 'slimstat_add_dt_out_index',
                'btn'    => __('Apply', 'wp-slimstat'),
            ],
            [
                'option' => 'slimstat_country_dt_indexed',
                'id'     => 'country-dt',
                'label'  => __('World Map & Country Reports', 'wp-slimstat'),
                'desc'   => __('Index on <code>country</code> and <code>dt</code>', 'wp-slimstat'),
                'key'    => 'idx_country_dt',
                'ajax'   => 'slimstat_add_country_dt_index',
                'btn'    => __('Apply', 'wp-slimstat'),
            ],
            [
                'option' => 'slimstat_dt_screen_indexed',
                'id'     => 'dt-screen',
                'label'  => __('Screen Resolution Reports', 'wp-slimstat'),
                'desc'   => __('Index on <code>dt</code>, <code>screen_width</code>, <code>screen_height</code>', 'wp-slimstat'),
                'key'    => 'idx_dt_screen_width_screen_height',
                'ajax'   => 'slimstat_add_dt_screen_index',
                'btn'    => __('Apply', 'wp-slimstat'),
            ],
            [
                'option' => 'slimstat_dt_browser_indexed',
                'id'     => 'dt-browser',
                'label'  => __('Browser Reports', 'wp-slimstat'),
                'desc'   => __('Index on <code>dt</code>, <code>browser</code>, <code>browser_version</code>', 'wp-slimstat'),
                'key'    => 'idx_dt_browser_browser_version',
                'ajax'   => 'slimstat_add_dt_browser_index',
                'btn'    => __('Apply', 'wp-slimstat'),
            ],
            [
                'option' => 'slimstat_dt_platform_indexed',
                'id'     => 'dt-platform',
                'label'  => __('Platform Reports', 'wp-slimstat'),
                'desc'   => __('Index on <code>dt</code>, <code>platform</code>', 'wp-slimstat'),
                'key'    => 'idx_dt_platform',
                'ajax'   => 'slimstat_add_dt_platform_index',
                'btn'    => __('Apply', 'wp-slimstat'),
            ],
            [
                'option' => 'slimstat_dt_visit_indexed',
                'id'     => 'dt-visit',
                'label'  => __('Visitor Counter Performance', 'wp-slimstat'),
                'desc'   => __('Index on <code>dt</code>, <code>visit_id</code>', 'wp-slimstat'),
                'key'    => $GLOBALS['wpdb']->prefix . 'stats_dt_visit_idx',
                'ajax'   => 'slimstat_add_dt_visit_index',
                'btn'    => __('Apply', 'wp-slimstat'),
            ],
        ];

        $pending = array_filter($indexes, function ($idx) {
            $db = wp_slimstat::$wpdb;
            $exists = $db->get_results(sprintf("SHOW INDEX FROM %sslim_stats WHERE Key_name = '%s'", $GLOBALS['wpdb']->prefix, $idx['key']));
            return empty($exists);
        });
        if ([] === $pending) {
            return;
        }
        $ajax_url = admin_url('admin-ajax.php');

        // Generate nonces for each AJAX action
        $nonces = [];
        foreach ($pending as $idx) {
            $nonces[$idx['ajax']] = wp_create_nonce($idx['ajax']);
        }

        echo '<div class="notice slimstat-indexes-notice slimstat-notice" style="border-left: 6px solid #0073aa; background: #fff; box-shadow: 0 2px 8px #0001; padding: 24px 24px 16px 24px; margin-bottom: 24px; position: relative; min-width: 400px; max-width: 700px;">';
        echo '<h2 style="margin-top:0; font-size:1.3em; color:#0073aa;">' . __('Improve SlimStat Report Performance', 'wp-slimstat') . '</h2>';
        echo '<p style="margin-bottom:18px;">' . __('To speed up SlimStat reports, please apply the following database optimizations. These changes are safe and will not affect your data.', 'wp-slimstat') . '</p>';
        echo '<ul id="slimstat-index-list" style="list-style:none; margin:0 0 18px 0; padding:0;">';
        foreach ($pending as $idx) {
            echo '<li id="slimstat-index-' . $idx['id'] . '" style="margin-bottom:12px; display:flex; align-items:center;">'
                . '<div style="flex:1 1 0;">'
                . '<div class="slimstat-index-label" style="font-weight:600;">' . $idx['label'] . '</div>'
                . '<div class="slimstat-index-desc" style="color:#666; font-size:0.97em; margin-top:2px;">' . $idx['desc'] . '</div>'
                . '</div>'
                . '<span class="slimstat-index-lamp" style="margin-left:18px; min-width:30px; display:inline-block; font-size:1.5em; vertical-align:middle;">'
                . '<span class="dashicons dashicons-lightbulb" style="color:#ccc;"></span>'
                . '</span>'
                . '<span class="slimstat-index-status" style="margin-left:10px; min-width:120px; display:inline-block;"></span>'
                . '</li>';
        }
        echo '</ul>';
        echo '<div id="slimstat-index-progress-bar" style="height:8px; background:#e5e5e5; border-radius:4px; overflow:hidden; margin-bottom:10px;">'
            . '<div id="slimstat-index-progress" style="height:100%; width:0; background:linear-gradient(90deg,#0073aa,#00c3aa); transition:width 0.4s;"></div>'
            . '</div>';
        echo '<button class="button button-primary" id="slimstat-apply-all" style="margin-bottom:10px; min-width:120px; font-size:1.1em;">' . __('Apply All', 'wp-slimstat') . '</button>';
        echo '<div style="color:#888; font-size:0.95em;">' . __('Do not close this tab until all optimizations are complete.', 'wp-slimstat') . '</div>';
        echo '</div>';
        ?>
        <script>
        jQuery(function($){
            var indexes = <?php echo wp_json_encode(array_values($pending)); ?>;
            var nonces = <?php echo wp_json_encode($nonces); ?>;
            var total = indexes.length, done = 0;
            function updateProgress() {
                var percent = Math.round((done/total)*100);
                $('#slimstat-index-progress').css('width', percent+'%');
                if (done === total) setTimeout(function(){ $('.slimstat-indexes-notice').fadeOut(); }, 2000);
            }
            function markDone(id) {
                var lamp = $('#slimstat-index-'+id+' .slimstat-index-lamp .dashicons');
                lamp.css('color','#ffc107'); // yellow lamp
                lamp.addClass('slimstat-lamp-on');
                $('#slimstat-index-'+id).css('opacity',0.9);
            }
            $('#slimstat-apply-all').on('click', function(e){
                e.preventDefault();
                var btn = $(this);
                btn.prop('disabled', true);
                window.onbeforeunload = function(){ return '<?php echo esc_js(__('Please wait for SlimStat optimizations to finish.', 'wp-slimstat')); ?>'; };
                function next(i) {
                    if (i >= indexes.length) {
                        window.onbeforeunload = null;
                        return;
                    }
                    var idx = indexes[i];
                    var li = $('#slimstat-index-'+idx.id);
                    li.find('.slimstat-index-status').html('<span style="color:#0073aa;">' + '<?php echo esc_js(__('In progress...', 'wp-slimstat')); ?>' + '</span> <span class="spinner is-active" style="float:none;display:inline-block;vertical-align:middle;"></span>');
                    $.post('<?php echo $ajax_url; ?>', {
                        action: idx.ajax,
                        _ajax_nonce: nonces[idx.ajax]
                    }, function(response){
                        if (response.success) {
                            markDone(idx.id);
                            done++;
                            li.find('.slimstat-index-status').html('<span style="color:green;">' + '<?php echo esc_js(__('Done!', 'wp-slimstat')); ?>' + '</span>');
                            updateProgress();
                            next(i+1);
                        } else {
                            li.find('.slimstat-index-status').html('<span style="color:red;">' + '<?php echo esc_js(__('Error: ', 'wp-slimstat')); ?>' + '</span>' + (response.data || ''));
                            btn.prop('disabled', false);
                            window.onbeforeunload = null;
                        }
                    });
                }
                next(0);
            });
        });
        </script>
        <?php
    }

}
// END: class declaration
