<?php
/**
 * The main plugin class.
 *
 * @package BoostBot
 * @since   1.0.0
 */

// Prevent direct access
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

/**
 * Class BoostBot
 *
 * The core plugin class that orchestrates all plugin functionality.
 *
 * @since 1.0.0
 */
class BoostBot {

    /**
     * The loader that registers all hooks.
     *
     * @var BoostBot_Loader
     */
    protected $loader;

    /**
     * Plugin version.
     *
     * @var string
     */
    protected $version;

    /**
     * The database manager instance.
     *
     * @var BoostBot_DB_Manager
     */
    protected $db;

    /**
     * The settings manager instance.
     *
     * @var BoostBot_Settings
     */
    protected $settings;

    /**
     * The API manager instance.
     *
     * @var BoostBot_API_Manager
     */
    protected $api_manager;

    /**
     * The license manager instance.
     *
     * @var BoostBot_License_Manager
     */
    protected $license_manager;

    /**
     * The i18n instance.
     *
     * @var BoostBot_i18n
     */
    protected $i18n;

    /**
     * The language manager instance.
     *
     * @var BoostBot_Language_Manager
     */
    protected $language_manager;

    /**
     * The single instance of the class.
     *
     * @var BoostBot|null
     */
    private static $instance = null;

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

    /**
     * Constructor.
     */
    public function __construct() {
        $this->version = BOOSTBOT_VERSION;

        $this->load_dependencies();

        if ( class_exists( 'BoostBot_Logger' ) ) {
            try {
                BoostBot_Logger::init();
            } catch ( \Throwable $e ) {
                BoostBot_Logger::error( '[Logger] Init failed: ' . $e->getMessage() );
            }
        }

        $this->set_locale();
        $this->define_admin_hooks();
        if ( defined( 'BOOSTBOT_SELF_HOSTED' ) && BOOSTBOT_SELF_HOSTED ) {
            require_once BOOSTBOT_PLUGIN_DIR . 'includes/updater/class-plugin-updater.php';
            new BoostBot_Plugin_Updater();
        }
        $this->define_public_hooks();

        // Low-credits monitor (cron + admin notice + email alert).
        // For WP.org builds, only run if user has opted in to cloud service.
        if ( BoostBot_Activator::is_cloud_service_allowed() ) {
            BoostBot_Low_Credits_Monitor::init();
        }

        // Proactive registration monitor (activation registration + daily health check).
        // For WP.org builds, only run if user has opted in to cloud service.
        if ( BoostBot_Activator::is_cloud_service_allowed() ) {
            BoostBot_Registration_Monitor::init();
        }

        // Spam alert monitor (admin notice + email when visitors abuse message limits).
        BoostBot_Spam_Alert_Monitor::init();

        // Log initialization
        if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
            BoostBot_Logger::debug( 'BoostBot class initialized', array( 'version' => $this->version ) );
        }
    }

    /**
     * Load the required dependencies.
     *
     * @return void
     */
    private function load_dependencies() {
        // Load functions file
        require_once BOOSTBOT_PLUGIN_DIR . 'includes/utilities/functions.php';

        // Create the loader instance
        $this->loader = new BoostBot_Loader();

        // Create the database manager instance
        $this->db = new BoostBot_DB_Manager();

        // Initialize API Manager (Week 2)
        $this->api_manager = BoostBot_API_Manager::get_instance();
        $this->api_manager->init();

        // Initialize Usage Tracker (Week 2)
        BoostBot_Usage_Tracker::init();

        // Initialize License Manager (Week 2)
        $this->license_manager = new BoostBot_License_Manager();

        // Initialize PayPal Handler (Week 2)
        // Week 26: when backend-managed billing is configured, disable legacy PayPal/IPN handlers.
        $backend_base_url = '';
        if ( class_exists( 'BoostBot_Settings' ) ) {
            $backend_base_url = trim( (string) BoostBot_Settings::get( 'backend_base_url', '' ) );
        }
        $backend_enabled = ! empty( $backend_base_url );
        if ( ! $backend_enabled && class_exists( 'BoostBot_PayPal_Handler' ) ) {
            $paypal_handler = new BoostBot_PayPal_Handler();
            $paypal_handler->init();
        }

        // Initialize i18n (Week 2.5)
        $this->i18n = BoostBot_i18n::get_instance();
        $this->i18n->init();

        // Initialize language manager (multilingual support)
        $this->language_manager = BoostBot_Language_Manager::get_instance();

        // Register Knowledge Base cron hooks (Week 6)
        $this->register_indexing_hooks();

        // Register CRM / Integrations hooks (Outgoing webhooks)
        if ( class_exists( 'BoostBot_CRM_Webhook_Service' ) ) {
            BoostBot_CRM_Webhook_Service::register_hooks();
        }
    }

    /**
     * Register hooks for knowledge base indexing.
     *
     * @since 1.0.0
     * @return void
     */
    private function register_indexing_hooks() {
        // Initialize Background Indexer (Week 6)
        BoostBot_Background_Indexer::init();

        // Loopback self-test hook (scheduled on plugin activation).
        add_action( 'boostbot_loopback_self_test', array( 'BoostBot_Background_Indexer', 'test_loopback' ) );

        // Week 30: Recovery cron for stuck queue rows.
        // Changed from hourly to every 5 minutes for faster stuck item recovery.
        // Note: Inline recovery in process_batch() provides immediate recovery,
        // this cron is a safety net for edge cases.
        // Always register the custom schedule so cron can find it when rescheduling.
        add_filter( 'cron_schedules', array( $this, 'add_cron_schedules' ) );

        if ( ! ( defined( 'DOING_CRON' ) && DOING_CRON ) ) {
            // Only create new scheduled events outside of cron to avoid recursion.
            $next_run = wp_next_scheduled( BoostBot_Background_Indexer::RECOVERY_HOOK );
            if ( ! $next_run ) {
                wp_schedule_event( time() + 60, 'boostbot_five_minutes', BoostBot_Background_Indexer::RECOVERY_HOOK );
            }
        }

        // Frontend + admin heartbeat: kick stalled indexing on ANY page load.
        // This is the key mechanism that ensures indexing progresses independently
        // of having the Knowledge Base admin page open. Any visitor to the site
        // will trigger this lightweight check (throttled to once per 60 seconds).
        add_action( 'wp_loaded', array( $this, 'maybe_kick_stalled_indexing_frontend' ) );

        // Initialize Widget Manager (Week 7)
        BoostBot_Widget_Manager::init();

        // Content update hooks - auto-reindex when content changes
        add_action( 'save_post', array( $this, 'on_content_update' ), 10, 2 );
        add_action( 'delete_post', array( $this, 'on_content_delete' ) );

        // Deferred deep-index for single items (scheduled from on_content_update)
        add_action( 'boostbot_auto_deep_index_single', array( $this, 'auto_deep_index_single' ), 10, 2 );

        // WooCommerce hooks (if WooCommerce is active)
        if ( class_exists( 'WooCommerce' ) ) {
            add_action( 'woocommerce_update_product', array( $this, 'on_product_update' ) );
            add_action( 'woocommerce_delete_product', array( $this, 'on_content_delete' ) );

            require_once BOOSTBOT_PLUGIN_DIR . 'includes/hooks/class-bundle-pricing-hooks.php';
            BoostBot_Bundle_Pricing_Hooks::init();
        }
    }

    /**
     * Process index queue items.
     *
     * @since 1.0.0
     * @return void
     */
    public function process_index_queue() {
        $index = new BoostBot_Knowledge_Index();

        // Process up to 10 items per batch
        $processed = 0;
        $max_items = 10;

        while ( $processed < $max_items && $index->process_queue_item() ) {
            $processed++;
        }

        // Check if there are more items to process
        $queue = get_option( 'boostbot_index_queue', array() );

        if ( ! empty( $queue ) ) {
            // Schedule next batch
            if ( ! wp_next_scheduled( 'boostbot_process_index_queue' ) ) {
                wp_schedule_single_event( time() + 5, 'boostbot_process_index_queue' );
            }
        } else {
            // Queue complete - update status
            $status = get_option( 'boostbot_indexing_status', array() );
            $status['status'] = 'completed';
            $status['completed_at'] = current_time( 'mysql' );
            update_option( 'boostbot_indexing_status', $status );

            // Clear indexing flag
            delete_transient( 'boostbot_indexing_in_progress' );

            BoostBot_Logger::info( 'Indexing completed', $status );
        }
    }

    /**
     * Handle content update (auto-reindex).
     *
     * @since 1.0.0
     * @param int     $post_id Post ID.
     * @param WP_Post $post    Post object.
     * @return void
     */
    public function on_content_update( $post_id, $post ) {
        // Skip revisions and auto-drafts
        if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) {
            return;
        }

        // Only index published content
        if ( 'publish' !== $post->post_status ) {
            return;
        }

        // Only index posts and pages (products handled separately)
        $allowed_types = array( 'post', 'page' );
        if ( ! in_array( $post->post_type, $allowed_types, true ) ) {
            return;
        }

        // Basic index (always runs — lightweight, no credits)
        $index = new BoostBot_Knowledge_Index();
        $index->index_content( $post_id, $post->post_type );

        // Smart auto-deep-index: if enabled, check whether this post needs deep indexing
        if ( ! (bool) BoostBot_Settings::get( 'auto_deep_index_enabled', true ) ) {
            return;
        }

        if ( ! class_exists( 'BoostBot_Index_Rules' ) ) {
            return;
        }

        if ( ! BoostBot_Index_Rules::is_post_included( $post_id ) ) {
            return;
        }

        $existing = $index->get_by_content_id( $post_id, $post->post_type );
        $needs_deep = false;

        if ( ! $existing ) {
            $needs_deep = true;
        } elseif ( empty( $existing->full_content ) ) {
            $needs_deep = true;
        } else {
            // Check for chunks via direct query (method is private in KnowledgeIndex)
            global $wpdb;
            $chunks_table = self::sanitize_identifier( $wpdb->prefix . 'boostbot_knowledge_chunks' );
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $chunks_table sanitized via sanitize_identifier(); values bind via prepare().
            $chunk_count = (int) $wpdb->get_var(
                $wpdb->prepare( "SELECT COUNT(*) FROM {$chunks_table} WHERE knowledge_id = %d", (int) $existing->id )
            );
            if ( 0 === $chunk_count ) {
                $needs_deep = true;
            }
        }

        if ( ! $needs_deep ) {
            return;
        }

        // Throttle: don't schedule if already scheduled for this post recently
        $throttle_key = 'boostbot_auto_deep_' . $post_id;
        if ( get_transient( $throttle_key ) ) {
            return;
        }
        set_transient( $throttle_key, 1, 120 );

        // Schedule deferred deep indexing (runs ~30s later in background)
        wp_schedule_single_event(
            time() + 30,
            'boostbot_auto_deep_index_single',
            array( $post_id, $post->post_type )
        );
    }

    /**
     * Deferred deep-index for a single content item.
     *
     * Triggered by wp_schedule_single_event from on_content_update
     * when a post is covered by index rules but lacks deep content.
     *
     * @param int    $post_id      Post ID.
     * @param string $content_type Content type.
     * @return void
     */
    public function auto_deep_index_single( $post_id, $content_type ) {
        $post = get_post( $post_id );
        if ( ! $post || 'publish' !== $post->post_status ) {
            return;
        }

        $index = new BoostBot_Knowledge_Index();
        $result = $index->index_content( (int) $post_id, $content_type, array(
            'mode' => 'deep_incremental',
        ) );

        if ( $result ) {
            BoostBot_Logger::info( 'Auto deep-indexed content on save', array(
                'post_id' => $post_id,
                'type'    => $content_type,
            ) );
        }
    }

    /**
     * Handle product update (WooCommerce).
     *
     * @since 1.0.0
     * @param int $product_id Product ID.
     * @return void
     */
    public function on_product_update( $product_id ) {
        $product = wc_get_product( $product_id );
        if ( ! $product || 'publish' !== $product->get_status() ) {
            return;
        }

        $index = new BoostBot_Knowledge_Index();
        $index->index_content( $product_id, 'product' );
    }

    /**
     * Handle content deletion.
     *
     * @since 1.0.0
     * @param int $post_id Post ID.
     * @return void
     */
    public function on_content_delete( $post_id ) {
        $post = get_post( $post_id );
        if ( ! $post ) {
            return;
        }

        $type = $post->post_type;
        if ( 'product' === $type && class_exists( 'WooCommerce' ) ) {
            $type = 'product';
        } elseif ( ! in_array( $type, array( 'post', 'page' ), true ) ) {
            return;
        }

        $index = new BoostBot_Knowledge_Index();
        $index->delete_entry( $post_id, $type );
    }

    /**
     * Set the locale for internationalization.
     *
     * @return void
     */
    private function set_locale() {
        $this->loader->add_action( 'init', $this, 'load_textdomain' );
    }

    /**
     * Load the plugin text domain for translation.
     *
     * @return void
     */
    public function load_textdomain() {
        /**
         * WordPress 4.6+ handles loading text domain automatically from WordPress.org.
         * Keeping the method for backward compatibility.
         */
    }

    /**
     * Register all admin-related hooks.
     *
     * @return void
     */
    private function define_admin_hooks() {
        // Only load admin hooks in admin area
        if ( ! is_admin() ) {
            return;
        }

        // Run pending migrations on admin init
        $this->loader->add_action( 'admin_init', $this, 'maybe_run_migrations' );
        // Post-activation onboarding redirect (first install)
        $this->loader->add_action( 'admin_init', $this, 'maybe_redirect_after_activation' );
        // Week 26.7: PayPal return handler (capture approved order so webhook can grant credits)
        $this->loader->add_action( 'admin_init', $this, 'maybe_handle_paypal_return' );

        // Server-side indexing heartbeat: any admin page load will restart stalled indexing.
        // This covers environments where WP-Cron is unreliable and admin React heartbeat hasn't loaded yet.
        $this->loader->add_action( 'admin_init', $this, 'maybe_kick_stalled_indexing' );

        // Admin menu
        $this->loader->add_action( 'admin_menu', $this, 'add_admin_menu' );

        // Admin scripts and styles
        $this->loader->add_action( 'admin_enqueue_scripts', $this, 'enqueue_admin_assets' );

        // Admin notice dismissal script (loads on all admin pages where notices may appear)
        $this->loader->add_action( 'admin_enqueue_scripts', $this, 'enqueue_admin_notice_assets' );

        // Plugin action links
        $this->loader->add_filter( 'plugin_action_links_' . BOOSTBOT_PLUGIN_BASENAME, $this, 'add_plugin_action_links' );

        // Pre-uninstall export popup (only on plugins.php)
        $this->loader->add_action( 'admin_enqueue_scripts', $this, 'maybe_enqueue_uninstall_popup' );

        // Connection status admin notice
        $this->loader->add_action( 'admin_notices', $this, 'show_connection_notice' );

    }

    /**
     * Serve Apple Pay domain association file for any site running BoostBot.
     *
     * Apple Pay requires /.well-known/apple-developer-merchantid-domain-association
     * to be accessible on every domain that shows the Apple Pay button. By intercepting
     * this request early, every WordPress site with BoostBot installed gets Apple Pay
     * domain verification automatically.
     */
    public function maybe_serve_apple_pay_domain_file() {
        if ( ! isset( $_SERVER['REQUEST_URI'] ) ) {
            return;
        }

        $path = wp_parse_url( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ), PHP_URL_PATH );
        if ( $path !== '/.well-known/apple-developer-merchantid-domain-association' ) {
            return;
        }

        $file = BOOSTBOT_PLUGIN_DIR . 'assets/apple-developer-merchantid-domain-association';
        if ( ! file_exists( $file ) ) {
            return;
        }

        header( 'Content-Type: application/octet-stream' );
        header( 'Content-Length: ' . filesize( $file ) );
        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile
        readfile( $file );
        exit;
    }

    /**
     * Week 26.7: Handle PayPal return from checkout.
     *
     * PayPal redirects back with:
     * - ?bb_purchase=success&token=<order_id>&PayerID=...
     *
     * We must capture the order (server-side) so PayPal emits PAYMENT.CAPTURE.COMPLETED,
     * which triggers our webhook to grant credits.
     */
    public function maybe_handle_paypal_return() {
        if ( ! current_user_can( 'manage_options' ) ) {
            return;
        }

        // Only handle on BoostBot admin pages.
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- PayPal return URL handler; gated by current_user_can('manage_options') at method entry.
        $page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( (string) $_GET['page'] ) ) : '';
        if ( 'boostbot' !== $page && 'boostbot-settings' !== $page ) {
            return;
        }

        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- PayPal return URL handler; gated by current_user_can('manage_options') at method entry.
        $bb_purchase = isset( $_GET['bb_purchase'] ) ? sanitize_text_field( wp_unslash( (string) $_GET['bb_purchase'] ) ) : '';
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- PayPal return URL handler; gated by current_user_can('manage_options') at method entry.
        $bb_indexing_purchase = isset( $_GET['bb_indexing_purchase'] ) ? sanitize_text_field( wp_unslash( (string) $_GET['bb_indexing_purchase'] ) ) : '';
        $is_chat_purchase = ( 'success' === $bb_purchase );
        $is_indexing_purchase = ( 'success' === $bb_indexing_purchase );
        if ( ! $is_chat_purchase && ! $is_indexing_purchase ) {
            return;
        }

        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- PayPal return URL handler; gated by current_user_can('manage_options') at method entry.
        $order_id = isset( $_GET['token'] ) ? sanitize_text_field( wp_unslash( (string) $_GET['token'] ) ) : '';
        if ( empty( $order_id ) ) {
            return;
        }

        if ( $is_chat_purchase ) {
            if ( ! class_exists( 'BoostBot_Credits_Backend_Client' ) ) {
                return;
            }
            $client = new BoostBot_Credits_Backend_Client();
            $site_id = $client->ensure_site_id();
            if ( is_wp_error( $site_id ) ) {
                return;
            }
            $capture_result = $client->capture_paypal_order( (string) $site_id, $order_id );

            if ( ! is_wp_error( $capture_result ) && class_exists( 'BoostAffiliate' ) ) {
                $pkg_id = isset( $capture_result['package_id'] ) ? (string) $capture_result['package_id'] : '';
                $price_map = array(
                    'starter'    => 50,
                    'basic'      => 100,
                    'plus'       => 150,
                    'business'   => 250,
                    'growth'     => 500,
                    'scale'      => 1000,
                    'enterprise' => 1500,
                );
                $amt = isset( $price_map[ $pkg_id ] ) ? (float) $price_map[ $pkg_id ] : 0;
                if ( $amt > 0 ) {
                    BoostAffiliate::purchase( $amt, 'ILS', $pkg_id );
                }
            }

            // Regardless of capture outcome, redirect to clear params and avoid re-capture on refresh.
            $redirect = admin_url( 'admin.php?page=boostbot' ) . '#/usage';
            wp_safe_redirect( $redirect );
            exit;
        }

        // Week 27: indexing credits PayPal return handler.
        if ( $is_indexing_purchase ) {
            if ( ! class_exists( 'BoostBot_Indexing_Credits_Backend_Client' ) ) {
                return;
            }
            $client = new BoostBot_Indexing_Credits_Backend_Client();
            $site_id = $client->ensure_site_id();
            if ( is_wp_error( $site_id ) ) {
                return;
            }
            $client->capture_paypal_order( (string) $site_id, $order_id );
            $redirect = admin_url( 'admin.php?page=boostbot' ) . '#/knowledge-base';
            wp_safe_redirect( $redirect );
            exit;
        }
    }

    /**
     * Server-side heartbeat: kick stalled indexing on any admin page load.
     *
     * This is a lightweight safety net that ensures indexing continues even when:
     * - HTTP loopback is blocked by the host
     * - WP-Cron is disabled or unreliable
     * - The admin is not on the Knowledge Base page (so the React heartbeat hasn't loaded yet)
     *
     * Runs on admin_init (every admin page load). Uses a transient-based throttle
     * to avoid running heavy checks on every page load — at most once per 30 seconds.
     *
     * @since 1.14.1
     * @return void
     */
    public function maybe_kick_stalled_indexing() {
        self::check_and_kick_stalled( 'admin_init', 30 );
    }

    /**
     * Frontend heartbeat: kick stalled indexing on ANY page load (visitor or admin).
     *
     * This ensures indexing progresses even when no admin is logged in, as long as
     * the site has any traffic at all (visitors, bots, etc.). Uses a longer throttle
     * (60 seconds) to minimize overhead on every frontend request.
     *
     * Also spawns WP-Cron if WordPress doesn't do it natively (e.g. DISABLE_WP_CRON
     * is true but no external cron is configured).
     *
     * @since 1.15.0
     * @return void
     */
    public function maybe_kick_stalled_indexing_frontend() {
        // Skip AJAX/cron/REST contexts — they have their own scheduling.
        if ( ( defined( 'DOING_AJAX' ) && DOING_AJAX ) ||
             ( defined( 'DOING_CRON' ) && DOING_CRON ) ||
             ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
            return;
        }

        self::check_and_kick_stalled( 'frontend', 60 );

        // If DISABLE_WP_CRON is set and there's a pending cron event, spawn it.
        // This covers the common misconfiguration: DISABLE_WP_CRON=true with no
        // system-level crontab. Without this, WP-Cron events never fire.
        if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {
            $next = wp_next_scheduled( BoostBot_Background_Indexer::CRON_HOOK );
            if ( $next && $next <= time() ) {
                spawn_cron();
            }
        }
    }

    /**
     * Shared stall-detection logic used by both admin and frontend heartbeats.
     *
     * @since 1.15.0
     * @param string $source   Label for logging (e.g. 'admin_init', 'frontend').
     * @param int    $throttle Minimum seconds between checks.
     * @return void
     */
    private static function check_and_kick_stalled( string $source, int $throttle ): void {
        $throttle_key = 'boostbot_stall_check_ts';
        $last_check   = (int) get_transient( $throttle_key );
        if ( $last_check && ( time() - $last_check ) < $throttle ) {
            return;
        }
        set_transient( $throttle_key, time(), $throttle + 30 );

        if ( ! class_exists( 'BoostBot_Background_Indexer' ) ) {
            return;
        }

        if ( BoostBot_Background_Indexer::is_stalled() ) {
            BoostBot_Logger::info( $source . ' heartbeat: detected stalled indexing, scheduling next batch' );
            BoostBot_Background_Indexer::schedule_next_batch();
        }
    }

    /**
     * Redirect to the setup wizard right after activation (first install only).
     *
     * Activation sets a short-lived transient. We consume it here and redirect the
     * admin to the Settings wizard tab, unless onboarding was already completed.
     *
     * @return void
     */
    public function maybe_redirect_after_activation() {
        // Safety: never redirect during AJAX/cron.
        if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
            return;
        }
        if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
            return;
        }

        if ( ! current_user_can( 'manage_options' ) ) {
            return;
        }

        // Avoid redirect loops on bulk activation.
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only activation-redirect check; admin-capability gated.
        if ( isset( $_GET['activate-multi'] ) ) {
            return;
        }

        if ( ! get_transient( 'boostbot_activation_redirect' ) ) {
            return;
        }

        // If wizard was already completed, just consume transient and stop.
        $completed = false;
        if ( class_exists( 'BoostBot_Settings' ) ) {
            $completed = (bool) BoostBot_Settings::get( 'onboarding_completed', false );
        }
        if ( $completed ) {
            delete_transient( 'boostbot_activation_redirect' );
            return;
        }

        delete_transient( 'boostbot_activation_redirect' );

        // Send the admin directly to the wizard tab inside Settings.
        $redirect = admin_url( 'admin.php?page=boostbot-settings' ) . '#/settings?tab=wizard';
        wp_safe_redirect( $redirect );
        exit;
    }

    /**
     * Register all public-facing hooks.
     *
     * @return void
     */
    private function define_public_hooks() {
        // Always register the hooks - the callbacks will check conditions
        // wp_enqueue_scripts and wp_footer only run on frontend anyway
        $this->loader->add_action( 'wp_enqueue_scripts', $this, 'enqueue_public_assets' );
        $this->loader->add_action( 'wp_footer', $this, 'render_chat_widget' );
        $this->loader->add_filter( 'template_include', $this, 'maybe_use_showcase_canvas_template', 99 );

        // Apple Pay domain verification: serve the domain association file so Apple Pay
        // works on every site running the BoostBot plugin, not just manually configured ones.
        $this->loader->add_action( 'init', $this, 'maybe_serve_apple_pay_domain_file', 1 );
    }

    /**
     * Is this request a BoostBot visual audit request?
     *
     * @return bool
     */
    private function is_visual_audit_request(): bool {
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only query flag, no state mutation; caller enforces caps before acting.
        return isset( $_GET['boostbot_visual_audit'] ) && $_GET['boostbot_visual_audit'] === '1';
    }

    /**
     * Who is allowed to use showcase mode?
     *
     * We intentionally allow logged-in content editors (not only admins), because the
     * visual-audit page is private and is used during development/QA.
     */
    private function is_showcase_allowed_user(): bool {
        return is_user_logged_in() && current_user_can( 'edit_posts' );
    }

    /**
     * Visual audit canvas template (admin-only, for development/QA).
     *
     * @param string $template Current template path.
     * @return string
     */
    public function maybe_use_showcase_canvas_template( $template ) {
        if ( $this->is_visual_audit_request() && $this->is_showcase_allowed_user() ) {
            $audit = BOOSTBOT_PLUGIN_DIR . 'templates/boostbot-visual-audit.php';
            return file_exists( $audit ) ? $audit : $template;
        }

        return $template;
    }

    /**
     * Add admin menu pages.
     *
     * @return void
     */
    public function add_admin_menu() {
        // Main menu
        add_menu_page(
            __( 'BoostBot', 'boostbot' ),
            __( 'BoostBot', 'boostbot' ),
            'manage_options',
            'boostbot',
            array( $this, 'render_admin_page' ),
            'dashicons-format-chat',
            30
        );

        // Dashboard submenu
        add_submenu_page(
            'boostbot',
            __( 'Dashboard', 'boostbot' ),
            __( 'Dashboard', 'boostbot' ),
            'manage_options',
            'boostbot',
            array( $this, 'render_admin_page' )
        );

        // Conversations submenu
        add_submenu_page(
            'boostbot',
            __( 'Conversations', 'boostbot' ),
            __( 'Conversations', 'boostbot' ),
            'manage_options',
            'boostbot-conversations',
            array( $this, 'render_admin_page' )
        );

        // Leads submenu
        add_submenu_page(
            'boostbot',
            __( 'Leads', 'boostbot' ),
            __( 'Leads', 'boostbot' ),
            'manage_options',
            'boostbot-leads',
            array( $this, 'render_admin_page' )
        );

        // Settings submenu
        add_submenu_page(
            'boostbot',
            __( 'Settings', 'boostbot' ),
            __( 'Settings', 'boostbot' ),
            'manage_options',
            'boostbot-settings',
            array( $this, 'render_admin_page' )
        );

        // Hidden preview-only page for visual audit (not in menu)
        add_submenu_page(
            null, // Hidden - no parent menu
            __( 'תצוגה מקדימה בלבד', 'boostbot' ),
            __( 'תצוגה מקדימה בלבד', 'boostbot' ),
            'edit_posts', // Allow editors
            'boostbot-preview-only',
            array( $this, 'render_preview_only_page' )
        );
    }

    /**
     * Render the admin page container.
     *
     * @return void
     */
    public function render_admin_page() {
        ?>
        <div id="boostbot-admin-root" class="wrap">
            <div class="boostbot-loading">
                <p><?php esc_html_e( 'Loading BoostBot...', 'boostbot' ); ?></p>
            </div>
        </div>
        <?php
    }

    /**
     * Render a preview-only page for visual audit comparison.
     * This page loads the admin bundle but only renders the preview component.
     *
     * @return void
     */
    public function render_preview_only_page() {
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Preview page registered via add_submenu_page with edit_posts cap; input sanitize_key'd.
        $widget = isset( $_GET['widget'] ) ? sanitize_key( wp_unslash( $_GET['widget'] ) ) : 'leadform';
        ?>
        <!doctype html>
        <html <?php language_attributes(); ?>>
        <head>
            <meta charset="<?php bloginfo( 'charset' ); ?>" />
            <meta name="viewport" content="width=device-width, initial-scale=1" />
            <title><?php echo esc_html__( 'תצוגה מקדימה – BoostBot', 'boostbot' ); ?> - <?php echo esc_html( ucfirst( $widget ) ); ?></title>
            <?php
            $preview_css = 'html,body{margin:0!important;padding:0!important;background:#f5f5f5!important;min-height:100vh}'
                . '#wpadminbar,#adminmenumain,#adminmenuback,#adminmenuwrap,.notice,.update-nag,#wpfooter,#screen-meta,#screen-meta-links{display:none!important}'
                . '#wpcontent,#wpbody,#wpbody-content{margin:0!important;padding:0!important}'
                . '.wrap{margin:0!important;padding:0!important}'
                . '#boostbot-preview-only-root{display:flex;align-items:center;justify-content:center;min-height:100vh;padding:20px;box-sizing:border-box}'
                . '.preview-loading{text-align:center;color:#666}';
            wp_add_inline_style( 'boostbot-admin', $preview_css );
            wp_head();
            ?>
        </head>
        <body class="boostbot-preview-only">
            <div id="boostbot-preview-only-root" data-widget="<?php echo esc_attr( $widget ); ?>">
                <div class="preview-loading">
                    <p><?php esc_html_e( 'Loading Preview...', 'boostbot' ); ?></p>
                </div>
            </div>
            <?php wp_footer(); ?>
        </body>
        </html>
        <?php
        exit; // Prevent WordPress from adding more content
    }

    /**
     * Enqueue admin notice dismissal script on pages where BoostBot notices may appear.
     *
     * @param string $hook The current admin page hook.
     * @return void
     */
    public function enqueue_admin_notice_assets( $hook ) {
        if ( 'index.php' !== $hook && strpos( $hook, 'boostbot' ) === false ) {
            return;
        }
        wp_enqueue_script(
            'boostbot-admin-notices',
            BOOSTBOT_PLUGIN_URL . 'assets/js/boostbot-admin-notices.js',
            array(),
            $this->version,
            true
        );
    }

    /**
     * Enqueue admin scripts and styles.
     *
     * @param string $hook The current admin page hook.
     * @return void
     */
    public function enqueue_admin_assets( $hook ) {
        // Only load on BoostBot pages
        if ( strpos( $hook, 'boostbot' ) === false ) {
            return;
        }

        // WordPress media library (for image uploads in widgets)
        wp_enqueue_media();

        // Admin styles - only load if file exists (CSS may be inlined in JS)
        $css_file = BOOSTBOT_PLUGIN_DIR . 'assets/dist/admin/boostbot-admin.css';
        if ( file_exists( $css_file ) ) {
            wp_enqueue_style(
                'boostbot-admin',
                BOOSTBOT_PLUGIN_URL . 'assets/dist/admin/boostbot-admin.css',
                array(),
                $this->version
            );
        }

        // Admin scripts (React app built with Vite - no WordPress dependencies needed)
        $admin_js_path = BOOSTBOT_PLUGIN_DIR . 'assets/dist/admin/boostbot-admin.js';
        $admin_js_version = file_exists($admin_js_path) ? filemtime($admin_js_path) : $this->version;
        wp_enqueue_script(
            'boostbot-admin',
            BOOSTBOT_PLUGIN_URL . 'assets/dist/admin/boostbot-admin.js',
            array(), // No dependencies - Vite bundles everything
            $admin_js_version,
            true
        );

        // Debug: help confirm which admin bundle version is actually being served (WP_DEBUG only)
        if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
            wp_add_inline_script(
                'boostbot-admin',
                'console.log("[BoostBot] admin bundle filemtime ver:", ' . wp_json_encode( $admin_js_version ) . ');',
                'after'
            );
        }

        // Get i18n strings and direction config
        $i18n_strings = [];
        $direction_config = [];
        if ( $this->i18n ) {
            $i18n_strings = $this->i18n->get_js_strings();
            $direction_config = $this->i18n->get_direction_config();
        }

        $admin_currency_symbol = class_exists( 'WooCommerce' ) && function_exists( 'get_woocommerce_currency_symbol' )
            ? html_entity_decode( get_woocommerce_currency_symbol(), ENT_QUOTES, 'UTF-8' )
            : '₪';
        $currency_by_language = array();
        if ( class_exists( 'WooCommerce' ) && class_exists( 'BoostBot_Polylang_Context' ) && class_exists( 'BoostBot_Language_Manager' ) ) {
            $enabled_languages = BoostBot_Language_Manager::get_instance()->get_enabled_languages();
            foreach ( $enabled_languages as $lang_code ) {
                $currency_by_language[ $lang_code ] = BoostBot_Polylang_Context::get_currency_for_language( $lang_code )['symbol'];
            }
        }

        // Localize script with data
        wp_localize_script(
            'boostbot-admin',
            'boostbotAdmin',
            array(
                'apiUrl'      => rest_url( 'boostbot/v1/' ),
                'nonce'       => wp_create_nonce( 'wp_rest' ),
                'version'     => $this->version,
                'isRtl'       => is_rtl(),
                'direction'   => is_rtl() ? 'rtl' : 'ltr',
                'locale'      => get_locale(),
                'adminUrl'    => admin_url(),
                'pluginUrl'   => BOOSTBOT_PLUGIN_URL,
                'i18n'        => $i18n_strings,
                'directionConfig' => $direction_config,
                'languageConfig'  => $this->language_manager->get_language_config(),
                'backendBaseUrl'  => BoostBot_Settings::get( 'backend_base_url', '' ),
                'backendSiteId'   => BoostBot_Settings::get( 'backend_site_id', '' ),
                'selfHosted'  => ( defined( 'BOOSTBOT_SELF_HOSTED' ) && BOOSTBOT_SELF_HOSTED ),
                'woocommerce_active' => class_exists( 'WooCommerce' ),
                'woocommerce_currency_symbol' => $admin_currency_symbol,
                'woocommerce_currency_by_language' => $currency_by_language,
            )
        );

        // Set script translations for @wordpress/i18n
        wp_set_script_translations(
            'boostbot-admin',
            'boostbot',
            BOOSTBOT_PLUGIN_DIR . 'languages'
        );

        if ( defined( 'BOOSTBOT_SELF_HOSTED' ) && BOOSTBOT_SELF_HOSTED ) {
            // phpcs:disable WordPress.WP.EnqueuedResourceParameters.MissingVersion
            wp_enqueue_script(
                'boostbot-gtag',
                'https://www.googletagmanager.com/gtag/js?id=G-4RR5QT255Z',
                array(),
                null,
                false
            );
            // phpcs:enable WordPress.WP.EnqueuedResourceParameters.MissingVersion
            wp_add_inline_script(
                'boostbot-gtag',
                "window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag('js',new Date());gtag('config','G-4RR5QT255Z');",
                'after'
            );

            if ( get_transient( 'boostbot_activation_redirect' ) ) {
                delete_transient( 'boostbot_activation_redirect' );
                wp_add_inline_script(
                    'boostbot-gtag',
                    "gtag('event','plugin_installed',{send_to:'G-4RR5QT255Z',event_category:'installation',event_label:'boostbot_plugin',plugin_version:'" . esc_js( $this->version ) . "'});",
                    'after'
                );
            }
        }
    }

    /**
     * Check if the backend is reachable. Result is cached as a transient for 5 minutes.
     * Only call from admin contexts (REST endpoints, admin notices) to avoid
     * adding HTTP latency on frontend page loads.
     *
     * @return bool
     */
    public function check_backend_reachability(): bool {
        $backend_url = BoostBot_Settings::get( 'backend_base_url', '' );
        if ( empty( $backend_url ) ) {
            set_transient( 'boostbot_backend_reachable', 'no', 5 * MINUTE_IN_SECONDS );
            return false;
        }

        $response = wp_remote_get( $backend_url . '/api/health', array(
            'timeout'   => 5,
            'sslverify' => true,
        ) );

        if ( is_wp_error( $response ) ) {
            set_transient( 'boostbot_backend_reachable', 'no', 5 * MINUTE_IN_SECONDS );
            return false;
        }

        $code = wp_remote_retrieve_response_code( $response );
        $body = json_decode( wp_remote_retrieve_body( $response ), true );
        $db_ok = ! empty( $body['data']['db_ok'] ) || ! empty( $body['db_ok'] );
        $reachable = ( $code === 200 && $db_ok );

        set_transient( 'boostbot_backend_reachable', $reachable ? 'yes' : 'no', 5 * MINUTE_IN_SECONDS );
        return $reachable;
    }

    /**
     * Show an admin notice when the backend is unreachable or not configured.
     *
     * @return void
     */
    public function show_connection_notice() {
        if ( ! current_user_can( 'manage_options' ) ) {
            return;
        }
        if ( wp_doing_ajax() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
            return;
        }

        $site_id     = BoostBot_Settings::get( 'backend_site_id', '' );
        $backend_url = BoostBot_Settings::get( 'backend_base_url', '' );

        if ( empty( $site_id ) || empty( $backend_url ) ) {
            $message = sprintf(
                /* translators: %s: BoostBot settings page URL */
                __( 'BoostBot: Not yet connected to the backend. <a href="%s">Configure in Settings</a>', 'boostbot' ),
                admin_url( 'admin.php?page=boostbot-settings#/settings' )
            );
            printf( '<div class="notice notice-warning is-dismissible"><p>%s</p></div>', wp_kses_post( $message ) );
            return;
        }

        $cached = get_transient( 'boostbot_backend_reachable' );
        if ( $cached === false ) {
            $this->check_backend_reachability();
            $cached = get_transient( 'boostbot_backend_reachable' );
        }

        if ( $cached === 'no' ) {
            $message = sprintf(
                /* translators: %s: BoostBot settings page URL */
                __( 'BoostBot: Unable to reach the backend server. The chat widget is hidden from visitors. <a href="%s">Check Settings</a>', 'boostbot' ),
                admin_url( 'admin.php?page=boostbot-settings#/settings' )
            );
            printf( '<div class="notice notice-error is-dismissible"><p>%s</p></div>', wp_kses_post( $message ) );
        }
    }

    /**
     * Enqueue public scripts and styles.
     *
     * @return void
     */
    public function enqueue_public_assets() {
        $is_active = BoostBot_Settings::get( 'is_active', true );
        if ( ! $is_active ) {
            return;
        }

        // Frontend styles (placeholder - will be replaced with built React widget)
        // Dynamic version for cache busting
        $widget_css_path = BOOSTBOT_PLUGIN_DIR . 'assets/dist/frontend/boostbot-widget.css';
        $widget_css_version = file_exists( $widget_css_path ) ? filemtime( $widget_css_path ) : $this->version;

        // Ensure we control the final registration/version (other legacy enqueuers may register the same handle).
        wp_deregister_style( 'boostbot-widget' );
        // phpcs:ignore WordPressVIPMinimum.Performance.EnqueuedStylesScope -- Site-wide chat widget intentionally loaded on all frontend contexts.
        wp_enqueue_style(
            'boostbot-widget',
            BOOSTBOT_PLUGIN_URL . 'assets/dist/frontend/boostbot-widget.css',
            array(),
            $widget_css_version
        );

        // Template styles (Pre-built Templates for AI responses)
        // phpcs:ignore WordPressVIPMinimum.Performance.EnqueuedStylesScope -- Site-wide chat widget intentionally loaded on all frontend contexts.
        wp_enqueue_style(
            'boostbot-templates',
            BOOSTBOT_PLUGIN_URL . 'assets/css/templates.css',
            array( 'boostbot-widget' ),
            $this->version
        );

        // Widget view styles (consolidated from template view PHP files)
        // phpcs:ignore WordPressVIPMinimum.Performance.EnqueuedStylesScope -- Site-wide chat widget intentionally loaded on all frontend contexts.
        wp_enqueue_style(
            'boostbot-views',
            BOOSTBOT_PLUGIN_URL . 'assets/css/boostbot-views.css',
            array( 'boostbot-templates' ),
            $this->version
        );

        // Widget view scripts (coupon copy, etc.)
        // phpcs:ignore WordPressVIPMinimum.Performance.EnqueuedScriptsScope -- Site-wide chat widget intentionally loaded on all frontend contexts.
        // The array $args form (with 'strategy' => 'defer') requires WP 6.3, but the plugin
        // supports WP 6.2 — feature-detect and fall back to the boolean in_footer form.
        wp_enqueue_script(
            'boostbot-views',
            BOOSTBOT_PLUGIN_URL . 'assets/js/boostbot-views.js',
            array(),
            $this->version,
            is_wp_version_compatible( '6.3' )
                ? array( 'in_footer' => true, 'strategy' => 'defer' )
                : true
        );

        // Frontend scripts (placeholder - will be replaced with built React widget)
        // Dynamic version for cache busting
        $widget_js_path = BOOSTBOT_PLUGIN_DIR . 'assets/dist/frontend/boostbot-widget.js';
        $widget_version = file_exists( $widget_js_path ) ? filemtime( $widget_js_path ) : $this->version;

        // Ensure we control the final registration/version (other legacy enqueuers may register the same handle).
        wp_deregister_script( 'boostbot-widget' );
        // phpcs:ignore WordPressVIPMinimum.Performance.EnqueuedScriptsScope -- Site-wide chat widget intentionally loaded on all frontend contexts.
        wp_enqueue_script(
            'boostbot-widget',
            BOOSTBOT_PLUGIN_URL . 'assets/dist/frontend/boostbot-widget.js',
            array(),
            $widget_version,
            is_wp_version_compatible( '6.3' )
                ? array( 'in_footer' => true, 'strategy' => 'defer' )
                : true
        );

        // Debug: help confirm which frontend bundle version is actually being served (WP_DEBUG only)
        if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
            wp_add_inline_script(
                'boostbot-widget',
                'console.log("[BoostBot] widget bundle filemtime ver:", ' . wp_json_encode( $widget_version ) . ');',
                'after'
            );
        }

        // Get translated option values
        $bot_name = BoostBot_Settings::get( 'bot_name', __( 'BoostBot', 'boostbot' ) );
        $welcome_message = BoostBot_Settings::get( 'welcome_message', __( 'Hi! How can I help you?', 'boostbot' ) );

        // Apply translation plugin filters if available
        if ( $this->i18n ) {
            $bot_name = $this->i18n->get_translated_option( 'bot_name', $bot_name );
            $welcome_message = $this->i18n->get_translated_option( 'welcome_message', $welcome_message );
        }

        // Widget gating: hide chat when backend is unreachable or site not registered.
        // Reads cached transient only (no HTTP on frontend pages).
        $chat_site_id  = BoostBot_Settings::get( 'backend_site_id', '' );
        $chat_available = ! empty( $chat_site_id ) && get_transient( 'boostbot_backend_reachable' ) !== 'no';

        // Localize script with config (using boostbot_config to match frontend expectations)
        wp_localize_script(
            'boostbot-widget',
            'boostbot_config',
            array(
                'chat_available' => $chat_available,
                'api_url'      => rest_url( 'boostbot/v1/' ),
                'nonce'        => '',
                'site_url'     => home_url(),
                'pluginUrl'    => BOOSTBOT_PLUGIN_URL,
                'is_rtl'       => is_rtl(),
                'locale'       => get_locale(),
                // Admin-only flags (safe): used for visual audit/showcase tooling.
                'is_admin'     => $this->is_showcase_allowed_user(),
                'current_page' => array(
                    'url'   => urldecode( get_permalink() ?: home_url() ),
                    'title' => get_the_title(),
                    'type'  => get_post_type() ?: 'page',
                    'id'    => get_the_ID() ?: 0,
                ),
                'user'         => is_user_logged_in() ? array(
                    'id'    => get_current_user_id(),
                    'name'  => wp_get_current_user()->display_name,
                    'email' => wp_get_current_user()->user_email,
                ) : null,
                'woocommerce' => class_exists('WooCommerce') ? ( function() {
                    $language = class_exists( 'BoostBot_Language' ) ? BoostBot_Language::get_current() : '';
                    $currency = class_exists( 'BoostBot_Polylang_Context' ) && class_exists( 'BoostBot_Language' )
                        ? BoostBot_Polylang_Context::get_currency_for_language( $language )
                        : array(
                            'code'   => function_exists( 'get_woocommerce_currency' ) ? get_woocommerce_currency() : 'ILS',
                            'symbol' => function_exists('get_woocommerce_currency_symbol')
                                ? html_entity_decode( get_woocommerce_currency_symbol(), ENT_QUOTES, 'UTF-8' )
                                : '₪',
                        );
                    $woo_service = class_exists( 'BoostBot_WooCommerce_Service' ) ? new BoostBot_WooCommerce_Service() : null;

                    return array(
                        'active'          => true,
                        'currency_symbol' => $currency['symbol'],
                        'currency_code'   => $currency['code'],
                        'is_product'      => function_exists('is_product') && is_product(),
                        'is_shop'         => function_exists('is_shop') && is_shop(),
                        'is_cart'         => function_exists('is_cart') && is_cart(),
                        'is_checkout'     => function_exists('is_checkout') && is_checkout(),
                        'cart_url'        => $woo_service ? $woo_service->get_store_page_url( 'cart', $language ) : '',
                        'checkout_url'    => $woo_service ? $woo_service->get_store_page_url( 'checkout', $language ) : '',
                        'product'         => null,
                    );
                } )() : array('active' => false),
            )
        );

        // Add product data to the woocommerce config if on a product page
        if (class_exists('WooCommerce') && function_exists('is_product') && is_product()) {
            global $product;
            if ($product instanceof \WC_Product) {
                // We need to re-localize or find a way to inject this. 
                // Since wp_localize_script was just called, we can use wp_add_inline_script to extend it.
                $product_data = array(
                    'id'            => $product->get_id(),
                    'name'          => $product->get_name(),
                    'price'         => $product->get_price(),
                    'image'         => wp_get_attachment_url($product->get_image_id()),
                    'currency_code' => class_exists( 'BoostBot_Polylang_Context' ) && class_exists( 'BoostBot_Language' )
                        ? BoostBot_Polylang_Context::get_currency_for_language( BoostBot_Language::get_current() )['code']
                        : ( function_exists( 'get_woocommerce_currency' ) ? get_woocommerce_currency() : 'ILS' ),
                );
                wp_add_inline_script(
                    'boostbot-widget',
                    'if (window.boostbot_config && window.boostbot_config.woocommerce) { window.boostbot_config.woocommerce.product = ' . wp_json_encode($product_data) . '; }',
                    'after'
                );
            }
        }

    }

    /**
     * Render the chat widget container in the footer.
     *
     * @return void
     */
    public function render_chat_widget() {
        // Check if chatbot is active
        $is_active = BoostBot_Settings::get( 'is_active', true );
        if ( ! $is_active ) {
            return;
        }
        ?>
        <!-- 🔥 BoostBot Widget Container v6 -->
        <div id="boostbot-widget-root"></div>
        <?php
    }

    /**
     * Add plugin action links.
     *
     * @param array $links Existing plugin action links.
     * @return array Modified plugin action links.
     */
    /**
     * Add custom cron schedules.
     *
     * @since 1.14.0
     * @param array $schedules Existing schedules.
     * @return array Modified schedules.
     */
    public function add_cron_schedules( $schedules ) {
        // 5-minute schedule for index queue recovery
        if ( ! isset( $schedules['boostbot_five_minutes'] ) ) {
            $schedules['boostbot_five_minutes'] = array(
                'interval' => 300, // 5 minutes in seconds
                'display'  => __( 'Every 5 Minutes (BoostBot)', 'boostbot' ),
            );
        }
        return $schedules;
    }

    public function add_plugin_action_links( $links ) {
        $settings_link = sprintf(
            '<a href="%s">%s</a>',
            admin_url( 'admin.php?page=boostbot-settings' ),
            __( 'Settings', 'boostbot' )
        );

        array_unshift( $links, $settings_link );

        return $links;
    }

    /**
     * Enqueue the pre-uninstall export popup on the plugins page.
     *
     * Shows a modal when the user clicks "Delete" for BoostBot,
     * suggesting they export their configuration and data first.
     *
     * @since 1.0.3
     * @param string $hook The current admin page hook.
     */
    public function maybe_enqueue_uninstall_popup( $hook ) {
        if ( 'plugins.php' !== $hook ) {
            return;
        }

        wp_enqueue_script(
            'boostbot-uninstall-popup',
            BOOSTBOT_PLUGIN_URL . 'assets/js/boostbot-uninstall-popup.js',
            array(),
            $this->version,
            true
        );

        wp_localize_script(
            'boostbot-uninstall-popup',
            'boostbotUninstall',
            array(
                'apiUrl' => rest_url( 'boostbot/v1/' ),
                'nonce'  => wp_create_nonce( 'wp_rest' ),
                'i18n'   => array(
                    'title'        => __( 'לפני שאתם עוזבים...', 'boostbot' ),
                    'subtitle'     => __( 'רוצים לייצא את נתוני BoostBot לפני המחיקה? ניתן לייבא אותם מחדש בעתיד.', 'boostbot' ),
                    'configTitle'  => __( 'הגדרות התוסף', 'boostbot' ),
                    'configDesc'   => __( 'הגדרות, ווידג\'טים, בוטים, אישיויות, עיצוב, ותצורות נוספות.', 'boostbot' ),
                    'dataTitle'    => __( 'נתונים מצטברים', 'boostbot' ),
                    'dataDesc'     => __( 'לידים, שיחות, פגישות, אנשי קשר, ניתוחים וסטטיסטיקות שימוש.', 'boostbot' ),
                    'exportConfig' => __( 'ייצוא הגדרות', 'boostbot' ),
                    'exportData'   => __( 'ייצוא נתונים', 'boostbot' ),
                    'skipDelete'   => __( 'דלג ומחק', 'boostbot' ),
                    'cancel'       => __( 'ביטול', 'boostbot' ),
                    'exporting'    => __( 'מייצא...', 'boostbot' ),
                    'downloaded'   => __( 'הורד!', 'boostbot' ),
                    'error'        => __( 'הייצוא נכשל. ניתן עדיין למחוק את התוסף.', 'boostbot' ),
                ),
            )
        );
    }

    /**
     * Check and run any pending migrations.
     *
     * @return void
     */
    public function maybe_run_migrations() {
        $migrations_dir = BOOSTBOT_PLUGIN_DIR . 'includes/database/migrations/';

        // Visual editor fields migration
        $migration_key = 'boostbot_migration_visual_editor_fields_v1';
        if ( ! get_option( $migration_key, false ) ) {
            $migration_file = $migrations_dir . 'class-migration-add-visual-editor-fields.php';
            if ( file_exists( $migration_file ) ) {
                require_once $migration_file;
                if ( class_exists( 'BoostBot_Migration_Add_Visual_Editor_Fields' ) ) {
                    $result = BoostBot_Migration_Add_Visual_Editor_Fields::run();
                    if ( $result ) {
                        update_option( $migration_key, true );
                        if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
                            BoostBot_Logger::info( 'Visual editor fields migration completed successfully' );
                        }
                    }
                }
            }
        }

        // Run bot appearance overrides migration
        $migration_key_2 = 'boostbot_migration_bot_appearance_overrides_v1';
        if ( ! get_option( $migration_key_2, false ) ) {
            $migration_file_2 = $migrations_dir . 'class-migration-add-bot-appearance-overrides.php';
            if ( file_exists( $migration_file_2 ) ) {
                require_once $migration_file_2;
                if ( class_exists( 'BoostBot_Migration_Add_Bot_Appearance_Overrides' ) ) {
                    $result = BoostBot_Migration_Add_Bot_Appearance_Overrides::up();
                    if ( $result ) {
                        update_option( $migration_key_2, true );
                    }
                }
            }
        }

        // Week 24 migration: add RAG columns (raw_content, service_category, is_service_page)
        $migration_key_3 = 'boostbot_migration_add_rag_columns_v1';
        if ( ! get_option( $migration_key_3, false ) ) {
            $migration_file_3 = $migrations_dir . 'class-migration-add-rag-columns.php';
            if ( file_exists( $migration_file_3 ) ) {
                require_once $migration_file_3;
                if ( class_exists( 'BoostBot_Migration_Add_RAG_Columns' ) ) {
                    $result = BoostBot_Migration_Add_RAG_Columns::run();
                    if ( $result ) {
                        update_option( $migration_key_3, true );
                    }
                }
            }
        }

        // Semantic indexing migration: create knowledge chunks table
        $migration_key_4 = 'boostbot_migration_create_knowledge_chunks_table_v1';
        if ( ! get_option( $migration_key_4, false ) ) {
            $migration_file_4 = $migrations_dir . 'class-migration-create-knowledge-chunks-table.php';
            if ( file_exists( $migration_file_4 ) ) {
                require_once $migration_file_4;
                if ( class_exists( 'BoostBot_Migration_Create_Knowledge_Chunks_Table' ) ) {
                    $result = BoostBot_Migration_Create_Knowledge_Chunks_Table::run();
                    if ( $result ) {
                        update_option( $migration_key_4, true );
                    }
                }
            }
        }

        // Week 30 migration: add is_enabled flag for content exclusion control
        $migration_key_5 = 'boostbot_migration_add_content_enabled_flag_v1';
        if ( ! get_option( $migration_key_5, false ) ) {
            $migration_file_5 = $migrations_dir . 'class-migration-add-content-enabled-flag.php';
            if ( file_exists( $migration_file_5 ) ) {
                require_once $migration_file_5;
                if ( class_exists( 'BoostBot_Migration_Add_Content_Enabled_Flag' ) ) {
                    $result = BoostBot_Migration_Add_Content_Enabled_Flag::run();
                    if ( $result ) {
                        update_option( $migration_key_5, true );
                    }
                }
            }
        }

        // Week 28+ migration: add FULLTEXT index for BM25 hybrid search
        $migration_key_6 = 'boostbot_migration_add_fulltext_index_v1';
        if ( ! get_option( $migration_key_6, false ) ) {
            $migration_file_6 = $migrations_dir . 'class-migration-add-fulltext-index.php';
            if ( file_exists( $migration_file_6 ) ) {
                require_once $migration_file_6;
                if ( class_exists( 'BoostBot_Migration_Add_Fulltext_Index' ) ) {
                    $result = BoostBot_Migration_Add_Fulltext_Index::run();
                    if ( $result ) {
                        update_option( $migration_key_6, true );
                    }
                }
            }
        }

        // Migration: add test_mode column to bots table (admin-only preview mode)
        $migration_key_7 = 'boostbot_migration_add_bot_test_mode_v1';
        if ( ! get_option( $migration_key_7, false ) ) {
            $migration_file_7 = $migrations_dir . 'class-migration-add-bot-test-mode.php';
            if ( file_exists( $migration_file_7 ) ) {
                require_once $migration_file_7;
                if ( class_exists( 'BoostBot_Migration_Add_Bot_Test_Mode' ) ) {
                    $result = BoostBot_Migration_Add_Bot_Test_Mode::run();
                    if ( $result ) {
                        update_option( $migration_key_7, true );
                    }
                }
            }
        }

        // Migration: add meeting_availability to contacts + assigned_contact_id to meetings
        $migration_key_8 = 'boostbot_migration_add_meeting_availability_v1';
        if ( ! get_option( $migration_key_8, false ) ) {
            $migration_file_8 = $migrations_dir . 'class-migration-add-meeting-availability.php';
            if ( file_exists( $migration_file_8 ) ) {
                require_once $migration_file_8;
                if ( class_exists( 'BoostBot_Migration_Add_Meeting_Availability' ) ) {
                    $result = BoostBot_Migration_Add_Meeting_Availability::run();
                    if ( $result ) {
                        update_option( $migration_key_8, true );
                    }
                }
            }
        }

        // Migration: add ai_summary to conversations table
        $migration_key_9 = 'boostbot_migration_add_conversation_summary_v1';
        if ( ! get_option( $migration_key_9, false ) ) {
            $migration_file_9 = $migrations_dir . 'class-migration-add-conversation-summary.php';
            if ( file_exists( $migration_file_9 ) ) {
                require_once $migration_file_9;
                if ( class_exists( 'BoostBot_Migration_Add_Conversation_Summary' ) ) {
                    $result = BoostBot_Migration_Add_Conversation_Summary::run();
                    if ( $result ) {
                        update_option( $migration_key_9, true );
                    }
                }
            }
        }

        // Migration: remove deprecated personality behavior fields and contact-card show_fields.
        $migration_key_10 = 'boostbot_migration_remove_deprecated_behavior_fields_v1';
        if ( ! get_option( $migration_key_10, false ) ) {
            $migration_file_10 = $migrations_dir . 'class-migration-remove-deprecated-behavior-fields.php';
            if ( file_exists( $migration_file_10 ) ) {
                require_once $migration_file_10;
                if ( class_exists( 'BoostBot_Migration_Remove_Deprecated_Behavior_Fields' ) ) {
                    $result = BoostBot_Migration_Remove_Deprecated_Behavior_Fields::run();
                    if ( $result ) {
                        update_option( $migration_key_10, true );
                    }
                }
            }
        }

        // Migration: appearance preset default-ref refactor (global → default/custom).
        // v2 re-runs to fix language detection and bot-preset mismatches from v1.3.0.
        $migration_file_11 = $migrations_dir . 'class-migration-appearance-default-ref.php';
        if ( '' === get_option( 'boostbot_migration_appearance_default_ref_v3_done', '' ) ) {
            if ( file_exists( $migration_file_11 ) ) {
                require_once $migration_file_11;
                if ( class_exists( 'BoostBot_Migration_Appearance_Default_Ref' ) ) {
                    try {
                        BoostBot_Migration_Appearance_Default_Ref::run();
                    } catch ( \Throwable $e ) {
                        BoostBot_Logger::error( '[Migration] Appearance_Default_Ref failed: ' . $e->getMessage() );
                    }
                }
            }
        }

        // Migration: scope global values per-language.
        $migration_file_12 = $migrations_dir . 'class-migration-global-values-per-language.php';
        if ( '' === get_option( 'boostbot_migration_global_values_per_language_done', '' ) ) {
            if ( file_exists( $migration_file_12 ) ) {
                require_once $migration_file_12;
                if ( class_exists( 'BoostBot_Migration_Global_Values_Per_Language' ) ) {
                    try {
                        BoostBot_Migration_Global_Values_Per_Language::run();
                    } catch ( \Throwable $e ) {
                        BoostBot_Logger::error( '[Migration] Global_Values_Per_Language failed: ' . $e->getMessage() );
                    }
                }
            }
        }

        // Migration: Create WooCommerce tables.
        $migration_file_13 = $migrations_dir . 'class-migration-create-woocommerce-tables.php';
        if ( ! get_option( 'boostbot_migration_create_woocommerce_tables_v1', false ) ) {
            if ( file_exists( $migration_file_13 ) ) {
                require_once $migration_file_13;
                if ( class_exists( 'BoostBot_Migration_Create_WooCommerce_Tables' ) ) {
                    try {
                        BoostBot_Migration_Create_WooCommerce_Tables::run();
                        update_option( 'boostbot_migration_create_woocommerce_tables_v1', true );
                    } catch ( \Throwable $e ) {
                        BoostBot_Logger::error( '[Migration] Create_WooCommerce_Tables failed: ' . $e->getMessage() );
                    }
                }
            }
        }

        // Migration: Add language column to bundles table.
        $migration_file_14 = $migrations_dir . 'class-migration-add-language-to-bundles.php';
        if ( ! get_option( 'boostbot_migration_add_language_to_bundles_v1', false ) ) {
            if ( file_exists( $migration_file_14 ) ) {
                require_once $migration_file_14;
                if ( class_exists( 'BoostBot_Migration_Add_Language_To_Bundles' ) ) {
                    try {
                        BoostBot_Migration_Add_Language_To_Bundles::run();
                        update_option( 'boostbot_migration_add_language_to_bundles_v1', true );
                    } catch ( \Throwable $e ) {
                        BoostBot_Logger::error( '[Migration] Add_Language_To_Bundles failed: ' . $e->getMessage() );
                    }
                }
            }
        }

        // Migration: Add woocommerce_preset_id column to bots table.
        $migration_file_15 = $migrations_dir . 'class-migration-add-woocommerce-preset-to-bots.php';
        if ( ! get_option( 'boostbot_migration_add_woocommerce_preset_to_bots_v1', false ) ) {
            if ( file_exists( $migration_file_15 ) ) {
                require_once $migration_file_15;
                if ( class_exists( 'BoostBot_Migration_Add_WooCommerce_Preset_To_Bots' ) ) {
                    try {
                        BoostBot_Migration_Add_WooCommerce_Preset_To_Bots::run();
                        update_option( 'boostbot_migration_add_woocommerce_preset_to_bots_v1', true );
                    } catch ( \Throwable $e ) {
                        BoostBot_Logger::error( '[Migration] Add_WooCommerce_Preset_To_Bots failed: ' . $e->getMessage() );
                    }
                }
            }
        }

        // Migration: Add is_system_managed column to widgets table.
        if ( ! get_option( 'boostbot_migration_add_widget_system_managed_v1', false ) ) {
            $migration_file_16 = $migrations_dir . 'class-migration-add-widget-system-managed.php';
            if ( file_exists( $migration_file_16 ) ) {
                require_once $migration_file_16;
                if ( class_exists( 'BoostBot_Migration_Add_Widget_System_Managed' ) ) {
                    try {
                        BoostBot_Migration_Add_Widget_System_Managed::run();
                        update_option( 'boostbot_migration_add_widget_system_managed_v1', true );
                    } catch ( \Throwable $e ) {
                        BoostBot_Logger::error( '[Migration] Add_Widget_System_Managed failed: ' . $e->getMessage() );
                    }
                }
            }
        }

        // Migration: Fix WooCommerce widget sharing + mark as system-managed.
        // Must run AFTER the is_system_managed column migration above.
        if ( ! get_option( 'boostbot_migration_woocommerce_widget_sharing_v1', false ) ) {
            $migration_file_17 = $migrations_dir . 'class-migration-woocommerce-widget-sharing.php';
            if ( file_exists( $migration_file_17 ) ) {
                require_once $migration_file_17;
                if ( class_exists( 'BoostBot_Migration_WooCommerce_Widget_Sharing' ) ) {
                    try {
                        BoostBot_Migration_WooCommerce_Widget_Sharing::run();
                        update_option( 'boostbot_migration_woocommerce_widget_sharing_v1', true );
                    } catch ( \Throwable $e ) {
                        BoostBot_Logger::error( '[Migration] WooCommerce_Widget_Sharing failed: ' . $e->getMessage() );
                    }
                }
            }
        }

        // Migration: Add product tier columns to presets (product_card_enabled, bundle product scope).
        if ( ! get_option( 'boostbot_migration_add_preset_product_tiers_v1', false ) ) {
            $migration_file_18 = $migrations_dir . 'class-migration-add-preset-product-tiers.php';
            if ( file_exists( $migration_file_18 ) ) {
                require_once $migration_file_18;
                if ( class_exists( 'BoostBot_Migration_Add_Preset_Product_Tiers' ) ) {
                    try {
                        BoostBot_Migration_Add_Preset_Product_Tiers::run();
                        update_option( 'boostbot_migration_add_preset_product_tiers_v1', true );
                    } catch ( \Throwable $e ) {
                        BoostBot_Logger::error( '[Migration] Add_Preset_Product_Tiers failed: ' . $e->getMessage() );
                    }
                }
            }
        }

        // Migration: separate WooCommerce coupon purposes for widget vs bundle usage.
        if ( ! get_option( 'boostbot_migration_add_coupon_purpose_columns_v1', false ) ) {
            $migration_file_19 = $migrations_dir . 'class-migration-add-coupon-purpose-columns.php';
            if ( file_exists( $migration_file_19 ) ) {
                require_once $migration_file_19;
                if ( class_exists( 'BoostBot_Migration_Add_Coupon_Purpose_Columns' ) ) {
                    try {
                        BoostBot_Migration_Add_Coupon_Purpose_Columns::run();
                        update_option( 'boostbot_migration_add_coupon_purpose_columns_v1', true );
                    } catch ( \Throwable $e ) {
                        BoostBot_Logger::error( '[Migration] Add_Coupon_Purpose_Columns failed: ' . $e->getMessage() );
                    }
                }
            }
        }

        // Migration: Create commerce events (telemetry) table.
        // Activator-only migration wired into the admin_init safety net so it
        // also runs on existing installs — the missing table otherwise breaks
        // commerce analytics inserts. Mirrors the WooCommerce-tables pattern above.
        if ( ! get_option( 'boostbot_migration_create_commerce_events_table_v1', false ) ) {
            $migration_file_20 = $migrations_dir . 'class-migration-create-commerce-events-table.php';
            if ( file_exists( $migration_file_20 ) ) {
                require_once $migration_file_20;
                if ( class_exists( 'BoostBot_Migration_Create_Commerce_Events_Table' ) ) {
                    try {
                        BoostBot_Migration_Create_Commerce_Events_Table::run();
                        update_option( 'boostbot_migration_create_commerce_events_table_v1', true );
                    } catch ( \Throwable $e ) {
                        BoostBot_Logger::error( '[Migration] Create_Commerce_Events_Table failed: ' . $e->getMessage() );
                    }
                }
            }
        }

        // Migration: Add FULLTEXT index to the knowledge_index table (BM25 hybrid search).
        // Activator-only migration wired into the safety net so existing installs get it.
        if ( ! get_option( 'boostbot_migration_add_fulltext_index_knowledge_index_v1', false ) ) {
            $migration_file_21 = $migrations_dir . 'class-migration-add-fulltext-index-knowledge-index.php';
            if ( file_exists( $migration_file_21 ) ) {
                require_once $migration_file_21;
                if ( class_exists( 'BoostBot_Migration_Add_Fulltext_Index_Knowledge_Index' ) ) {
                    try {
                        // run() returns false when the target table does not yet
                        // exist; only mark done on success so it retries later.
                        if ( BoostBot_Migration_Add_Fulltext_Index_Knowledge_Index::run() ) {
                            update_option( 'boostbot_migration_add_fulltext_index_knowledge_index_v1', true );
                        }
                    } catch ( \Throwable $e ) {
                        BoostBot_Logger::error( '[Migration] Add_Fulltext_Index_Knowledge_Index failed: ' . $e->getMessage() );
                    }
                }
            }
        }
    }

    /**
     * Run the plugin.
     *
     * Execute all hooks registered with the loader.
     *
     * @return void
     */
    public function run() {
        $this->loader->run();

        // Log that the plugin is running
        if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
            BoostBot_Logger::info( 'BoostBot plugin running', array(
                'hooks_registered' => true,
                'version'          => $this->version,
            ) );
        }
    }

    /**
     * Get the plugin version.
     *
     * @return string
     */
    public function get_version() {
        return $this->version;
    }

    /**
     * Get the loader instance.
     *
     * @return BoostBot_Loader
     */
    public function get_loader() {
        return $this->loader;
    }

    /**
     * Get the database manager instance.
     *
     * @return BoostBot_DB_Manager
     */
    public function get_db() {
        return $this->db;
    }

    /**
     * Get the API manager instance.
     *
     * @return BoostBot_API_Manager
     */
    public function get_api_manager() {
        return $this->api_manager;
    }

    /**
     * Get the license manager instance.
     *
     * @return BoostBot_License_Manager
     */
    public function get_license_manager() {
        return $this->license_manager;
    }

    /**
     * Get the i18n instance.
     *
     * @return BoostBot_i18n
     */
    public function get_i18n() {
        return $this->i18n;
    }

    /**
     * Restrict a SQL identifier to [A-Za-z0-9_] characters.
     *
     * @param string $identifier Raw identifier.
     * @return string Identifier with disallowed characters removed.
     */
    private static function sanitize_identifier( $identifier ) {
        return preg_replace( '/[^A-Za-z0-9_]/', '', (string) $identifier );
    }
}

