<?php

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

/**
 * Main Plugin Class — Facade.
 *
 * Domain logic is split into traits under includes/traits/:
 * - Niwa_AI_Admin_Dashboard  — Admin page rendering
 * - Niwa_AI_Ajax_Handlers    — Codex OAuth, index/embeddings, simple AJAX
 * - Niwa_AI_Frontend_Chat    — Frontend chat AJAX, session, caching
 * - Niwa_AI_Frontend_Queue   — Chat queue, worker, concurrency, locking
 *
 * This class owns properties, boot/init, and dependency loading.
 */
class Niwa_AI_Plugin {

    const WHATSAPP_RUNTIME_REFRESH_HOOK = 'niwa_ai_refresh_whatsapp_runtime_after_plugin_update';

    use Niwa_AI_Admin_Dashboard;
    use Niwa_AI_Admin_Chat_Queue;
    use Niwa_AI_Ajax_Handlers;
    use Niwa_AI_Frontend_Chat;
    use Niwa_AI_Frontend_Queue;

    /* ================================================================
     * Properties
     * ============================================================= */

    private $auth_manager;
    private $frontend_auth_manager;
    private $founder_auth_manager;
    private $model_catalog;
    private $model_router;
    private $index_sync;
    private $agent_profile_summary_service;
    private $latency_tracker;
    private $index_worker;
    private $content_store;
    private $store_overview;
    private $embedding_client;
    private $memory_embedder;
    private $memory_searcher;
    private $openai_client;
    private $frontend_openai_client;
    private $humanizer;
    private $rag_fallback;
    private $rlm_engine;
    private $admin_settings;
    private $conversation_store;
    private $chat_renderer;
    private $asset_loader;
    private $variation_gallery;
    private $contact_service;
    private $appointment_service;
    private $appointment_prospect_summarizer;
    private $appointment_notifier;
	private $calendar_integration_service;
    private $scheduler_ajax_controller;
    private $lead_whatsapp_outreach;
    private $frontend_question_log;
    private $attention_capture;
    private $analytics_dashboard;
    private $traffic_module;
    private $template_library_module;
    private $card_builder;
    private $conversion_busters;
    private $leave_stoper_settings;
    private $ui_settings;
    private $agent_mode_settings;
    private $performance_settings_ui;
    private $name_processor;
    private $email_validator;
    private $lead_capture_page;
	private $conversation_history_page;
    private $admin_autonomy_store;
    private $admin_autonomy_telegram;
    private $admin_autonomy_whatsapp;
    private $admin_autonomy_settings;
    private $admin_autonomy_tools;
    private $admin_product_management_service;
    private $admin_product_target_resolver;
    private $admin_autonomy_workspace;
    private $admin_autonomy_control_plane;
    private $admin_autonomy_email;
    private $admin_autonomy_scheduler;
    private $admin_autonomy_skills;
    private $admin_autonomy_db_intelligence;
    private $admin_autonomy_site_discovery;
    private $admin_autonomy_code_intelligence;
    private $admin_autonomy_companion_memory;
    private $admin_autonomy_session_memory_store;
    private $admin_autonomy_self_improvement;
    private $admin_autonomy_session_state;
    private $admin_autonomy_actor_runtime;
    private $admin_autonomy_turn_engine;
    private $admin_autonomy_subagent_manager;
    private $admin_autonomy_chat;
    private $admin_autonomy;

    /* ── Lead Form Generator ── */
    private $lead_form_session_store;
    private $lead_form_llm_router;
    private $lead_form_mailer;
    private $lead_form_runtime;
    private $lead_form_ajax_handler;

    /* ── Cart Recovery ── */
    private $cart_recovery_repository;
    private $cart_recovery_snapshot_builder;
    private $cart_recovery_service;
    private $cart_recovery_scheduler;
    private $cart_recovery_coupon_service;
    private $cart_recovery_coupon_listener;
    private $cart_recovery_transcript_digester;
    private $cart_recovery_template_wrapper;
    private $cart_recovery_tracking_controller;
    private $cart_recovery_mailer;
    private $cart_recovery_settings_ui;
    private $cart_recovery_privacy;
    private $cart_recovery_analytics_widget;

    /* ── MCP Server ── */
    private $mcp_auth;
    private $mcp_settings;

    /* ================================================================
     * Helpers
     * ============================================================= */

    /**
     * Guard flag — core services are created once then reused.
     */
    private $core_services_booted = false;

    /**
     * Whether the admin-autonomy subsystem should be loaded.
     * Only needed in admin, AJAX, or WP-Cron contexts — never on plain frontend page loads.
     */
    private function needs_admin_autonomy() {
        if ( $this->is_elementor_editor_request() ) {
            return false;
        }
        return is_admin()
            || wp_doing_cron()
            || ( defined( 'NIWA_AI_CLI_RUNNER' ) && NIWA_AI_CLI_RUNNER )
            || $this->is_admin_autonomy_rest_request();
    }

	private function is_admin_chat_read_request() {
		$action = isset( $_REQUEST['action'] ) ? sanitize_key( wp_unslash( $_REQUEST['action'] ) ) : '';
		return wp_doing_ajax() && in_array( $action, array( 'niwa_ai_admin_autonomy_status', 'niwa_ai_admin_autonomy_history' ), true );
	}

    private function is_admin_autonomy_rest_request() {
        $rest_route = isset( $_GET['rest_route'] ) ? (string) wp_unslash( $_GET['rest_route'] ) : '';
        if (
            '' !== $rest_route
            && (
                0 === strpos( trim( $rest_route, '/' ), 'niwa-ai/v1/telegram/webhook' )
                || 0 === strpos( trim( $rest_route, '/' ), 'niwa-ai/v1/whatsapp/webhook' )
            )
        ) {
            return true;
        }

        $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? (string) wp_unslash( $_SERVER['REQUEST_URI'] ) : '';
        return false !== strpos( $request_uri, '/wp-json/niwa-ai/v1/telegram/webhook' )
            || false !== strpos( $request_uri, '/index.php/wp-json/niwa-ai/v1/telegram/webhook' )
            || false !== strpos( $request_uri, '/wp-json/niwa-ai/v1/whatsapp/webhook' )
            || false !== strpos( $request_uri, '/index.php/wp-json/niwa-ai/v1/whatsapp/webhook' );
    }

    private function is_elementor_editor_request() {
        $action = isset( $_REQUEST['action'] ) ? sanitize_key( wp_unslash( $_REQUEST['action'] ) ) : '';
        if ( in_array( $action, array( 'elementor', 'elementor_ajax' ), true ) ) {
            return true;
        }
        return isset( $_GET['elementor-preview'] );
    }

    /**
     * Skip Niwa boot on core media/editor utility requests where the plugin
     * does not need to run and can interfere with WordPress/Elementor internals.
     *
     * @return bool
     */
    private function is_core_admin_utility_request() {
        if ( ! is_admin() ) {
            return false;
        }

        $script = isset( $_SERVER['SCRIPT_NAME'] ) ? wp_basename( wp_unslash( $_SERVER['SCRIPT_NAME'] ) ) : '';
        $action = isset( $_REQUEST['action'] ) ? sanitize_key( wp_unslash( $_REQUEST['action'] ) ) : '';

        if ( in_array( $script, array( 'async-upload.php', 'media-upload.php' ), true ) ) {
            return true;
        }

        $media_ajax_actions = array(
            'query-attachments',
            'upload-attachment',
            'save-attachment',
            'delete-post',
            'send-attachment-to-editor',
            'image-editor',
        );

        if ( in_array( $action, $media_ajax_actions, true ) ) {
            return true;
        }

        return false;
    }

    /**
     * Admin settings are only needed on real wp-admin requests.
     * Cron/frontend should not depend on that class being present.
     *
     * @return object
     */
    private function build_admin_settings() {
        if ( class_exists( 'Niwa_AI_Admin_Settings' ) ) {
            return new Niwa_AI_Admin_Settings();
        }

        $fallback_file = NIWA_AI_DIR . 'admin/class-admin-settings.php';
        if ( file_exists( $fallback_file ) ) {
            require_once $fallback_file;
        }

        if ( class_exists( 'Niwa_AI_Admin_Settings' ) ) {
            return new Niwa_AI_Admin_Settings();
        }

        error_log( '[Niwa AI] Niwa_AI_Admin_Settings missing; using no-op fallback registrar.' );

        return new class() {
            public function register() {
                // Keep wp-admin operational even if the settings file is missing in a partial deploy.
            }
        };
    }

    /* ================================================================
     * Boot
     * ============================================================= */

    public function boot() {
        if ( $this->is_core_admin_utility_request() ) {
            return;
        }

        $this->load_dependencies();

		if ( $this->is_admin_chat_read_request() ) {
			$this->boot_admin_chat_read_services();
			add_action( 'wp_ajax_niwa_ai_admin_autonomy_history', array( $this, 'ajax_admin_autonomy_history' ) );
			add_action( 'wp_ajax_niwa_ai_admin_autonomy_status', array( $this, 'ajax_admin_autonomy_status' ) );
			return;
		}

        if ( class_exists( 'Niwa_AI_Admin_Autonomy_Email' ) ) {
            Niwa_AI_Admin_Autonomy_Email::register_global_wp_mail_smtp();
        }

        /* ----- Tier 1: Lightweight objects for hook registration & rendering ----- */
        $this->content_store       = new Niwa_AI_Content_Store();
        $this->index_sync          = new Niwa_AI_Index_Sync();
        $this->index_worker        = new Niwa_AI_Index_Worker( $this->content_store );
        $this->agent_profile_summary_service = new Niwa_AI_Agent_Profile_Summary_Service();
        $this->contact_service     = new Niwa_AI_Contact_Service();
		$this->appointment_service = new Niwa_AI_Appointment_Service( $this->contact_service );
		$this->appointment_service->ensure_schema();
		$this->scheduler_ajax_controller = new Niwa_AI_Scheduler_Ajax_Controller( $this->appointment_service );
		$this->calendar_integration_service = new Niwa_AI_Calendar_Integration_Service( $this->appointment_service );
        $this->lead_whatsapp_outreach = new Niwa_AI_Lead_WhatsApp_Outreach_Service( $this->contact_service );
        $this->appointment_notifier = new Niwa_AI_Appointment_Notifier( $this->lead_whatsapp_outreach );
        $this->chat_renderer       = new Niwa_AI_Chat_Renderer( $this );
        $this->asset_loader        = new Niwa_AI_Asset_Loader( NIWA_AI_VERSION );
        $this->variation_gallery   = new Niwa_AI_Variation_Gallery();
        $this->attention_capture   = new Niwa_AI_Attention_Capture();
        $this->analytics_dashboard = new Niwa_AI_Analytics_Dashboard();
        $this->traffic_module      = new Niwa_AI_Traffic_Module();
        $this->template_library_module = new Niwa_AI_Template_Library_Module();
        $this->conversion_busters  = new Niwa_AI_Conversion_Busters();
        $this->leave_stoper_settings = new Niwa_AI_Leave_Stoper_Settings();
        $this->ui_settings         = new Niwa_AI_UI_Settings();
        $this->agent_mode_settings = new Niwa_AI_Agent_Mode_Settings();
        $this->performance_settings_ui = new Niwa_AI_Performance_Settings_UI();
        $this->lead_capture_page   = new Niwa_AI_Lead_Capture_Settings( $this->contact_service );
		$this->conversation_history_page = new Niwa_AI_Conversation_History_Page();

        /* ----- Cart Recovery (always-on — listens to Woo cart events everywhere) ----- */
        // Settings filter wiring MUST come first — scheduler's delay /
        // cooldown / inactivity filters fall back to hard-coded constants
        // if this isn't called before the first `tick()`, which would
        // silently ignore every admin UI change on those timing knobs.
        Niwa_AI_Cart_Recovery_Settings::register_hooks();

        $this->cart_recovery_repository       = new Niwa_AI_Cart_Recovery_Repository();
        if ( Niwa_AI_Constants::CART_RECOVERY_SCHEMA_STAMP !== (string) get_option( Niwa_AI_Constants::OPT_CART_RECOVERY_SCHEMA_READY, '' ) ) {
            if ( $this->cart_recovery_repository->ensure_table_schema() ) {
                update_option( Niwa_AI_Constants::OPT_CART_RECOVERY_SCHEMA_READY, Niwa_AI_Constants::CART_RECOVERY_SCHEMA_STAMP, true );
            }
        }
        $this->cart_recovery_snapshot_builder = new Niwa_AI_Cart_Recovery_Snapshot_Builder();
        $this->cart_recovery_coupon_service   = new Niwa_AI_Cart_Recovery_Coupon_Service(
            $this->cart_recovery_repository
        );
        $this->cart_recovery_coupon_listener  = new Niwa_AI_Cart_Recovery_Coupon_Listener(
            $this->cart_recovery_repository
        );
        $this->cart_recovery_scheduler        = new Niwa_AI_Cart_Recovery_Scheduler(
            $this->cart_recovery_repository,
            $this->cart_recovery_coupon_service
        );
        // Service needs the scheduler to queue event-driven per-row sends
        // right after `upsert_snapshot`, so construct it after the
        // scheduler is available.
        $this->cart_recovery_service          = new Niwa_AI_Cart_Recovery_Service(
            $this->cart_recovery_repository,
            $this->cart_recovery_snapshot_builder,
            $this->cart_recovery_scheduler
        );
        $this->cart_recovery_template_wrapper   = new Niwa_AI_Cart_Recovery_Template_Wrapper();
        $this->cart_recovery_tracking_controller = new Niwa_AI_Cart_Recovery_Tracking_Controller(
            $this->cart_recovery_repository
        );
        $this->cart_recovery_privacy = new Niwa_AI_Cart_Recovery_Privacy(
            $this->cart_recovery_repository
        );
        $this->cart_recovery_analytics_widget = new Niwa_AI_Cart_Recovery_Analytics_Widget(
            $this->cart_recovery_repository
        );

        /* ----- Tier 2: Core services + Admin autonomy (admin/ajax/cron only) ----- */
        if ( $this->needs_admin_autonomy() ) {
            $this->ensure_core_services();
			$this->boot_scheduler_ai_context();
            if ( $this->template_library_module && method_exists( $this->template_library_module, 'set_llm_dependencies' ) ) {
                $this->template_library_module->set_llm_dependencies( $this->openai_client, $this->founder_auth_manager, $this->model_router );
            }
            $this->boot_admin_autonomy();
            $this->boot_lead_form_generator();
            $this->boot_cart_recovery_mailer();
        }

        /* ----- MCP Server (REST API — always available for external agent access) ----- */
        $this->boot_mcp();

        /* ----- Core hook registrations ----- */
        $this->index_sync->register_hooks();
        $this->index_worker->register_hooks();
        $this->agent_profile_summary_service->register_hooks();
        $this->attention_capture->register_hooks();
        $this->variation_gallery->register_hooks();
        $this->analytics_dashboard->register_hooks();
        $this->traffic_module->register_hooks();
        $this->template_library_module->register_hooks();
        $this->conversion_busters->register_hooks();
        $this->leave_stoper_settings->register_hooks();
        $this->ui_settings->register_hooks();
        $this->agent_mode_settings->register_hooks();
        $this->performance_settings_ui->register_hooks();
        $this->lead_capture_page->register_hooks();
		$this->conversation_history_page->register_hooks();
		$this->scheduler_ajax_controller->register_hooks();
        $this->appointment_notifier->register_hooks();
		$this->calendar_integration_service->register_hooks();
        if ( $this->lead_form_ajax_handler ) {
            $this->lead_form_ajax_handler->register_hooks();
        }
        $this->cart_recovery_service->register_hooks();
        $this->cart_recovery_scheduler->register_hooks();
        $this->cart_recovery_coupon_listener->register_hooks();
        $this->cart_recovery_tracking_controller->register_hooks();
        $this->cart_recovery_privacy->register_hooks();
        $this->cart_recovery_analytics_widget->register_hooks();

        /* Blog asset rendering — emits Niwa blog CSS/JS/JSON-LD on wp_head/wp_footer
         * from post meta. Keeps `<style>` and `<script>` payloads out of post_content
         * so wp_kses_post() can't leak them as plain text on the frontend. */
        Niwa_AI_Blog_Asset_Renderer::register_hooks();

        /* Blog asset migrator — admin-post endpoints (scan + run) for cleaning up
         * legacy posts that already had CSS/JS leaked into post_content. */
        Niwa_AI_Blog_Migrator::register_hooks();

        add_action( 'init', array( $this, 'load_textdomain' ) );
        add_action( 'admin_menu', array( $this, 'register_admin_menu' ), 9 );
        add_action( 'admin_menu', array( $this, 'normalize_niwa_admin_submenu' ), 10000 );
        add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_niwa_admin_assets' ) );
        add_action( 'admin_footer', array( $this, 'render_admin_autonomy_launcher' ) );
        add_action( 'admin_post_niwa_ai_codex_oauth_start', array( $this, 'handle_codex_oauth_start' ) );
        add_action( 'admin_post_niwa_ai_codex_oauth_callback', array( $this, 'handle_codex_oauth_callback' ) );
        add_action( 'admin_post_niwa_ai_codex_oauth_disconnect', array( $this, 'handle_codex_oauth_disconnect' ) );
        add_action( 'admin_post_niwa_ai_run_full_index_and_embeddings', array( $this, 'handle_run_full_index_and_embeddings' ) );
        add_action( 'admin_post_niwa_ai_index_background_worker', array( $this, 'handle_index_background_worker' ) );
        add_action( 'admin_post_nopriv_niwa_ai_index_background_worker', array( $this, 'handle_index_background_worker' ) );
        add_action( 'niwa_ai_process_memory_embeddings', array( $this, 'handle_process_memory_embeddings' ) );
        add_action( 'niwa_ai_process_index_queue', array( $this, 'handle_process_index_queue' ) );
		add_action( 'niwa_ai_refresh_frontend_semantic_lexicon', array( $this, 'handle_refresh_frontend_semantic_lexicon' ) );
		add_action( 'niwa_ai_index_upsert', array( $this, 'schedule_frontend_semantic_lexicon_refresh' ), 99, 3 );
		add_action( 'niwa_ai_index_delete', array( $this, 'schedule_frontend_semantic_lexicon_refresh' ), 99, 2 );
		add_action( 'init', array( $this, 'ensure_frontend_semantic_lexicon_refresh' ), 40 );
        add_action( 'niwa_ai_process_frontend_chat_queue', array( $this, 'process_frontend_chat_queue' ) );
        add_action( 'niwa_ai_admin_autonomy_imap_idle_watchdog', array( $this, 'handle_admin_autonomy_imap_idle_watchdog' ) );
        add_action( 'niwa_ai_admin_autonomy_background_clock_watchdog', array( $this, 'handle_admin_autonomy_background_clock_watchdog' ) );
        add_action( self::WHATSAPP_RUNTIME_REFRESH_HOOK, array( $this, 'handle_whatsapp_runtime_refresh_after_plugin_update' ), 10, 1 );
        add_action( 'niwa_ai_admin_autonomy_wake_background', array( $this, 'handle_admin_autonomy_background_clock_wake' ), 5, 1 );
        add_action( 'wp_ajax_niwa_ai_codex_oauth_poll', array( $this, 'ajax_codex_oauth_poll' ) );
        add_action( 'wp_ajax_niwa_ai_embeddings_status', array( $this, 'ajax_embeddings_status' ) );
        add_action( 'wp_ajax_niwa_ai_admin_autonomy_chat', array( $this, 'ajax_admin_autonomy_chat' ) );
        add_action( 'wp_ajax_niwa_ai_admin_autonomy_history', array( $this, 'ajax_admin_autonomy_history' ) );
        add_action( 'wp_ajax_niwa_ai_admin_autonomy_status', array( $this, 'ajax_admin_autonomy_status' ) );
        add_action( 'admin_post_niwa_ai_admin_ui_chat_worker', array( $this, 'handle_admin_ui_chat_worker' ) );
        add_action( 'admin_post_nopriv_niwa_ai_admin_ui_chat_worker', array( $this, 'handle_admin_ui_chat_worker' ) );
        add_action( 'niwa_ai_process_admin_ui_chat_queue', array( $this, 'process_admin_ui_chat_queue' ) );
		add_filter( 'niwa_ai_admin_ui_chat_job_transition', array( $this, 'filter_admin_ui_chat_job_transition' ), 10, 2 );
		add_filter( 'niwa_ai_admin_ui_chat_background_drain', array( $this, 'drain_admin_ui_chat_background_queue' ), 10, 1 );
		add_action( 'niwa_ai_recover_admin_ui_chat_job', array( $this, 'recover_admin_ui_chat_job' ) );
        add_filter( 'cron_schedules', array( $this, 'register_cron_schedules' ) );

        if ( is_admin() ) {
            $this->admin_settings = $this->build_admin_settings();
            add_action( 'admin_init', array( $this->admin_settings, 'register' ) );
        }

        /* ----- Cron schedule registration for admin/autonomy runtime requests ----- */
        if ( ( is_admin() && ! wp_doing_ajax() ) || wp_doing_cron() || $this->is_admin_autonomy_rest_request() ) {
            $this->maybe_register_cron_events();
        }

        $this->maybe_start_admin_autonomy_background_clock( 'plugin-boot' );

        add_action( 'niwa_ai_process_lead_notifications', array( $this->contact_service, 'process_lead_notifications' ) );

        /* ----- Frontend hooks ----- */
        add_shortcode( 'niwa_ai_chat', array( $this->chat_renderer, 'render_shortcode' ) );
        add_shortcode( 'niwa_booking', array( $this->chat_renderer, 'render_booking_shortcode' ) );
        add_action( 'wp_enqueue_scripts', array( $this->asset_loader, 'enqueue' ), 100 );
        add_action( 'wp_footer', array( $this->chat_renderer, 'render_flyout' ) );
        add_action( 'wp_ajax_niwa_ai_chat', array( $this, 'ajax_chat' ) );
        add_action( 'wp_ajax_nopriv_niwa_ai_chat', array( $this, 'ajax_chat' ) );
        // Public frontend recovery endpoint: returns only a fresh chat nonce.
        add_action( 'wp_ajax_niwa_ai_refresh_chat_nonce', array( $this, 'ajax_refresh_chat_nonce' ) );
        add_action( 'wp_ajax_nopriv_niwa_ai_refresh_chat_nonce', array( $this, 'ajax_refresh_chat_nonce' ) );
        add_action( 'wp_ajax_nopriv_niwa_ai_refresh_nonce', array( $this, 'ajax_refresh_chat_nonce' ) );
        // Admin nonce heartbeat endpoint — returns a fresh chat/admin nonce for
        // logged-in users and preserves scope-based capability checks.
        add_action( 'wp_ajax_niwa_ai_refresh_nonce', array( $this, 'ajax_admin_refresh_nonce' ) );
        add_action( 'wp_ajax_niwa_ai_admin_refresh_nonce', array( $this, 'ajax_admin_refresh_nonce' ) );
        add_action( 'wp_ajax_niwa_ai_chat_queue_status', array( $this, 'ajax_chat_queue_status' ) );
        add_action( 'wp_ajax_nopriv_niwa_ai_chat_queue_status', array( $this, 'ajax_chat_queue_status' ) );
        add_action( 'wp_ajax_niwa_ai_save_contact', array( $this, 'ajax_save_contact' ) );
        add_action( 'wp_ajax_nopriv_niwa_ai_save_contact', array( $this, 'ajax_save_contact' ) );
        add_action( 'wp_ajax_niwa_ai_handle_name', array( $this, 'ajax_handle_name' ) );
        add_action( 'wp_ajax_nopriv_niwa_ai_handle_name', array( $this, 'ajax_handle_name' ) );
        add_action( 'wp_ajax_niwa_ai_add_to_cart', array( $this, 'ajax_add_to_cart' ) );
        add_action( 'wp_ajax_nopriv_niwa_ai_add_to_cart', array( $this, 'ajax_add_to_cart' ) );
        add_action( 'wp_ajax_niwa_ai_get_product_details', array( $this, 'ajax_get_product_details' ) );
        add_action( 'wp_ajax_nopriv_niwa_ai_get_product_details', array( $this, 'ajax_get_product_details' ) );
        add_action( 'wp_ajax_niwa_ai_clear_cleanup_flag', array( $this, 'ajax_clear_cleanup_flag' ) );
        add_action( 'wp_ajax_nopriv_niwa_ai_clear_cleanup_flag', array( $this, 'ajax_clear_cleanup_flag' ) );
        add_action( 'admin_post_niwa_ai_frontend_chat_worker', array( $this, 'handle_frontend_chat_worker' ) );
        add_action( 'admin_post_nopriv_niwa_ai_frontend_chat_worker', array( $this, 'handle_frontend_chat_worker' ) );
    }

    /**
     * Lazily create the core service objects (auth, AI clients, search, chat engine).
     *
     * On plain frontend page loads these are never needed — the only work done
     * is asset enqueue and chat HTML rendering. The heavy services are created
     * here on the first admin, AJAX, or cron request. Subsequent calls are no-ops.
     */
    public function ensure_core_services() {
        if ( $this->core_services_booted ) {
            return;
        }
        $this->core_services_booted = true;

        /* ----- Auth & Model routing ----- */
        $this->auth_manager            = new Niwa_AI_Auth_Manager( 'default' );
        $this->frontend_auth_manager   = new Niwa_AI_Auth_Manager( 'frontend' );
        $this->founder_auth_manager    = new Niwa_AI_Auth_Manager( 'founder' );
        $this->model_catalog           = new Niwa_AI_Model_Catalog();
        $this->model_router            = new Niwa_AI_Model_Router( $this->model_catalog );
        $this->latency_tracker         = new Niwa_AI_Latency_Tracker();

        /* ----- AI clients ----- */
        $this->openai_client           = new Niwa_AI_OpenAI_Client( 'founder_deep' );
        $this->frontend_openai_client  = new Niwa_AI_OpenAI_Client( 'frontend_fast' );

        /* ----- Search & Retrieval ----- */
        $this->embedding_client  = new Niwa_AI_Embedding_Client();
        $this->memory_searcher   = new Niwa_AI_Memory_Searcher( $this->content_store, $this->embedding_client );
        $this->memory_embedder   = new Niwa_AI_Memory_Embedder( $this->content_store, $this->embedding_client );
        $this->store_overview    = new Niwa_AI_Store_Overview( $this->content_store );
        $this->humanizer         = new Niwa_AI_Humanizer();

        /* ----- Chat engine ----- */
        $woo_provider            = new Niwa_AI_Woo_RAG_Provider();
        $this->rag_fallback      = new Niwa_AI_RAG_Fallback( array( $woo_provider ) );
        $this->card_builder      = new Niwa_AI_Card_Builder();
        $this->rlm_engine        = new Niwa_AI_RLM_Engine(
            $this->frontend_auth_manager,
            $this->model_router,
            $this->frontend_openai_client,
            $this->humanizer,
            $this->rag_fallback,
            $this->card_builder,
            $this->content_store,
            $this->store_overview,
            $this->memory_searcher
        );
		$this->rlm_engine->set_latency_tracker( $this->latency_tracker );

        $spark_adapter = new Niwa_AI_Spark_Adapter(
            $this->frontend_openai_client,
            $this->frontend_auth_manager,
            $this->memory_searcher,
            $this->humanizer,
            $this->content_store
        );
        $this->rlm_engine->set_spark_adapter( $spark_adapter );

        /* ----- Remaining on-demand services ----- */
        $this->conversation_store    = new Niwa_AI_Conversation_Store();
        $this->frontend_question_log = new Niwa_AI_Frontend_Question_Log();
        $this->name_processor        = new Niwa_AI_Name_Processor( $this->frontend_openai_client, $this->frontend_auth_manager );
        $this->email_validator       = new Niwa_AI_Email_Validator();
    }

	/** Recover a missing or outdated catalog relation lexicon outside the chat request path. */
	public function ensure_frontend_semantic_lexicon_refresh() {
		$lexicon = get_option( Niwa_AI_Frontend_Semantic_Lexicon::OPTION, array() );
		$source_hash = $this->content_store && method_exists( $this->content_store, 'get_product_semantic_lexicon_hash' )
			? (string) $this->content_store->get_product_semantic_lexicon_hash()
			: '';
		if (
			is_array( $lexicon )
			&& Niwa_AI_Frontend_Semantic_Lexicon::VERSION === intval( $lexicon['version'] ?? 0 )
			&& ( '' === $source_hash || hash_equals( (string) ( $lexicon['source_hash'] ?? '' ), $source_hash ) )
		) {
			return;
		}
		if ( ! wp_next_scheduled( 'niwa_ai_refresh_frontend_semantic_lexicon' ) ) {
			wp_schedule_single_event( time() + 30, 'niwa_ai_refresh_frontend_semantic_lexicon' );
		}
	}

	/** Debounce one default-model lexicon rebuild after indexed products change. */
	public function schedule_frontend_semantic_lexicon_refresh( $post_id = 0, $post_type = '', $update = false ) {
		unset( $post_id, $update );
		if ( 'product' !== (string) $post_type || ! function_exists( 'wp_schedule_single_event' ) ) {
			return;
		}
		if ( ! wp_next_scheduled( 'niwa_ai_refresh_frontend_semantic_lexicon' ) ) {
			wp_schedule_single_event( time() + 120, 'niwa_ai_refresh_frontend_semantic_lexicon' );
		}
	}

	/** Cron entrypoint for the catalog semantic warmup. */
	public function handle_refresh_frontend_semantic_lexicon() {
		$result = $this->refresh_frontend_semantic_lexicon_now();
		if ( empty( $result['success'] ) && function_exists( 'wp_schedule_single_event' ) && ! wp_next_scheduled( 'niwa_ai_refresh_frontend_semantic_lexicon' ) ) {
			wp_schedule_single_event( time() + 600, 'niwa_ai_refresh_frontend_semantic_lexicon' );
		}
		return $result;
	}

	/** Public manual warmup used by WP-CLI after deploy and by cron. */
	public function refresh_frontend_semantic_lexicon_now() {
		$this->ensure_core_services();
		if ( ! $this->rlm_engine || ! method_exists( $this->rlm_engine, 'refresh_frontend_semantic_lexicon' ) ) {
			return array( 'success' => false, 'error' => 'Frontend semantic lexicon service is unavailable.' );
		}
		$result = $this->rlm_engine->refresh_frontend_semantic_lexicon();
		if ( ! empty( $result['success'] ) && function_exists( 'wp_clear_scheduled_hook' ) ) {
			wp_clear_scheduled_hook( 'niwa_ai_refresh_frontend_semantic_lexicon' );
		}
		return $result;
	}

	private function boot_admin_chat_read_services() {
		$this->conversation_store            = new Niwa_AI_Conversation_Store();
		$this->admin_autonomy_telegram       = new Niwa_AI_Admin_Autonomy_Telegram();
		$this->admin_autonomy_workspace      = new Niwa_AI_Admin_Autonomy_Workspace();
		$this->conversation_store->set_workspace( $this->admin_autonomy_workspace );
		$this->admin_autonomy = new Niwa_AI_Admin_Autonomy(
			null,
			null,
			$this->admin_autonomy_telegram,
			null,
			$this->conversation_store,
			null,
			null
		);
	}

    /**
     * Boot the Lead Form Generator runtime + AJAX handler.
     *
     * Requires core services (frontend auth, model router, AI client) to be
     * up — always called from within the needs_admin_autonomy() branch.
     *
     * @return void
     */
    private function boot_lead_form_generator() {
        $this->lead_form_session_store = new Niwa_AI_Lead_Form_Session_Store();

        $this->lead_form_llm_router = new Niwa_AI_Lead_Form_LLM_Router(
            $this->model_router,
            $this->frontend_auth_manager,
            $this->frontend_openai_client
        );

        $this->lead_form_mailer = new Niwa_AI_Lead_Form_Mailer( $this->lead_form_session_store );

        $this->lead_form_runtime = new Niwa_AI_Lead_Form_Runtime(
            $this->lead_form_session_store,
            $this->lead_form_llm_router,
            $this->contact_service,
            $this->lead_form_mailer
        );

        $this->lead_form_ajax_handler = new Niwa_AI_Lead_Form_Ajax_Handler( $this->lead_form_runtime );
    }

	/**
	 * Bind verified frontend sessions to factual AI prospect summaries used by
	 * the Scheduler contact card. This hook is registered only in admin/AJAX/
	 * cron contexts where frontend model dependencies are already available.
	 */
	private function boot_scheduler_ai_context() {
		if ( $this->appointment_prospect_summarizer ) {
			return;
		}
		$this->appointment_prospect_summarizer = new Niwa_AI_Appointment_Prospect_Summarizer(
			$this->model_router,
			$this->frontend_auth_manager,
			$this->frontend_openai_client,
			$this->conversation_store
		);
		add_filter(
			'niwa_ai_scheduler_verified_conversation_summary',
			array( $this, 'summarize_scheduler_prospect' ),
			10,
			3
		);
	}

	/**
	 * @param string $summary     Summary supplied by an earlier filter.
	 * @param string $session_id Verified frontend session id.
	 * @param array  $appointment Saved appointment row.
	 * @return string
	 */
	public function summarize_scheduler_prospect( $summary, $session_id, $appointment ) {
		unset( $appointment );
		if ( '' !== trim( (string) $summary ) || ! $this->appointment_prospect_summarizer ) {
			return (string) $summary;
		}
		return $this->appointment_prospect_summarizer->summarize( (string) $session_id );
	}

    /**
     * Boot the Cart Recovery mailer — Tier 2 because it needs the AI
     * composer (which in turn needs the frontend model router + auth
     * manager + AI client from ensure_core_services()) and the
     * conversation store for transcript digests. Tier 1 already wired
     * the repository, template wrapper, and tracking controller.
     *
     * Mailer hooks into `ACTION_CART_RECOVERY_SEND_MAIL1/2` fired from
     * the scheduler's tick — which only runs via WP-Cron, i.e. always
     * inside needs_admin_autonomy() === true, so the hook is always
     * registered before the action fires.
     */
    private function boot_cart_recovery_mailer() {
        $composer = new Niwa_AI_Cart_Recovery_Composer(
            $this->model_router,
            $this->frontend_auth_manager,
            $this->frontend_openai_client
        );

        $this->cart_recovery_transcript_digester = new Niwa_AI_Cart_Recovery_Transcript_Digester(
            $this->conversation_store
        );

        $this->cart_recovery_mailer = new Niwa_AI_Cart_Recovery_Mailer(
            $this->cart_recovery_repository,
            $this->cart_recovery_template_wrapper,
            $composer,
            $this->cart_recovery_coupon_service,
            $this->cart_recovery_transcript_digester,
            $this->lead_whatsapp_outreach
        );

        $this->cart_recovery_mailer->register_hooks();

        // Admin UI — needs mailer for preview + scheduler for run-tick-now.
        // Only useful in admin context which is always needs_admin_autonomy() true.
        if ( is_admin() ) {
            $this->cart_recovery_settings_ui = new Niwa_AI_Cart_Recovery_Settings_UI(
                $this->cart_recovery_repository,
                $this->cart_recovery_mailer,
                $this->cart_recovery_scheduler
            );
            $this->cart_recovery_settings_ui->register_hooks();
        }
    }

    /**
     * Boot the MCP server — always available for external agent access via REST API.
     */
    private function boot_mcp() {
        $this->mcp_auth     = new Niwa_AI_MCP_JWT_Auth();
        $this->mcp_settings = new Niwa_AI_Admin_MCP_Settings();
        $this->mcp_settings->register_hooks();

        // Niwa Bloging settings page.
        new Niwa_AI_Admin_Bloging_Settings();

        // Token management routes — lightweight, no dependencies on admin autonomy.
        add_action( 'rest_api_init', array( $this, 'register_mcp_token_routes' ) );

        // MCP protocol route — needs tools bridge, lazily boots admin autonomy.
        add_action( 'rest_api_init', array( $this, 'register_mcp_protocol_route' ) );
    }

    /**
     * Register token management REST endpoints (generate, revoke, list).
     * These are lightweight and don't need the admin autonomy subsystem.
     */
    public function register_mcp_token_routes() {
        $auth = $this->mcp_auth;

	    register_rest_route( 'niwa-ai/v1', '/mcp/token', array(
	        'methods'             => 'POST',
	        'callback'            => function ( WP_REST_Request $request ) use ( $auth ) {
	            $params   = $request->get_json_params();
	            $duration = isset( $params['duration'] ) ? (string) $params['duration'] : '1_day';
	            $scopes   = isset( $params['scopes'] ) && is_array( $params['scopes'] ) ? $params['scopes'] : array();
	            $allowed  = array( '1_day', '1_month', '1_year', 'never' );

	            if ( ! in_array( $duration, $allowed, true ) ) {
	                return new WP_REST_Response( array( 'success' => false, 'error' => 'Invalid duration.' ), 400 );
	            }

	            try {
	                $result = $auth->generate_token( get_current_user_id(), $duration, $scopes );
	            } catch ( \Throwable $e ) {
	                error_log( '[Niwa MCP] Token generation EXCEPTION: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine() );
	                return new WP_REST_Response( array( 'success' => false, 'error' => 'Token generation failed: ' . $e->getMessage() ), 500 );
	            }
	            if ( is_wp_error( $result ) ) {
	                return new WP_REST_Response( array( 'success' => false, 'error' => $result->get_error_message() ), 503 );
	            }

	            $response_data = array( 'success' => true, 'data' => $result );

	            return rest_ensure_response( $response_data );
	        },
	        'permission_callback' => function () {
	            return current_user_can( 'manage_options' );
	        },
	    ) );

        register_rest_route( 'niwa-ai/v1', '/mcp/revoke', array(
            'methods'             => 'POST',
            'callback'            => function ( WP_REST_Request $request ) use ( $auth ) {
                $params = $request->get_json_params();
                $jti    = isset( $params['jti'] ) ? (string) $params['jti'] : '';

                if ( '' === $jti ) {
                    return new WP_REST_Response( array( 'success' => false, 'error' => 'Token ID (jti) is required.' ), 400 );
                }

                $revoked = $auth->revoke_token( $jti );

                return rest_ensure_response( array( 'success' => $revoked, 'message' => $revoked ? 'Token revoked.' : 'Token not found.' ) );
            },
            'permission_callback' => function () {
                return current_user_can( 'manage_options' );
            },
        ) );

        register_rest_route( 'niwa-ai/v1', '/mcp/tokens', array(
            'methods'             => 'GET',
            'callback'            => function () use ( $auth ) {
                return rest_ensure_response( array( 'success' => true, 'data' => $auth->list_tokens() ) );
            },
            'permission_callback' => function () {
                return current_user_can( 'manage_options' );
            },
        ) );
    }

    /**
     * Register MCP protocol endpoint — lazily boots admin autonomy for tools bridge.
     */
    public function register_mcp_protocol_route() {
        $plugin = $this;

	    register_rest_route( 'niwa-ai/v1', '/mcp', array(
	        'methods'             => 'POST',
	        'callback'            => function ( WP_REST_Request $request ) use ( $plugin ) {
	            // Lazy-boot admin autonomy only when MCP protocol is actually called.
	            try {
	                $plugin->ensure_core_services();
	            } catch ( \Throwable $e ) {
                    error_log( '[Niwa MCP] ensure_core_services FAILED: ' . $e->getMessage() );
                }

	            if ( ! $plugin->admin_autonomy_tools || ! $plugin->admin_autonomy_chat ) {
	                $plugin->boot_admin_autonomy_for_mcp();

	                if ( ! $plugin->admin_autonomy_tools || ! $plugin->admin_autonomy_chat ) {
	                    error_log( '[Niwa MCP] admin autonomy MCP dependencies after boot: tools=' . ( $plugin->admin_autonomy_tools ? 'OK' : 'NULL' ) . ', chat=' . ( $plugin->admin_autonomy_chat ? 'OK' : 'NULL' ) );
	                }
	            }

                $bridge = new Niwa_AI_MCP_Tools_Bridge( $plugin->admin_autonomy_tools, $plugin->admin_autonomy_chat );
                $server = new Niwa_AI_MCP_Server( $plugin->mcp_auth, $bridge );

                return $server->handle_mcp_request( $request );
            },
            'permission_callback' => function ( WP_REST_Request $request ) use ( $plugin ) {
                $token = $request->get_header( 'X-Niwa-MCP-Token' );
                if ( empty( $token ) ) {
                    return new WP_Error( 'niwa_mcp_no_token', 'MCP token required.', array( 'status' => 401 ) );
                }

                $result = $plugin->mcp_auth->verify_token( $token );
                if ( is_wp_error( $result ) ) {
                    return new WP_Error( $result->get_error_code(), $result->get_error_message(), array( 'status' => 403 ) );
                }

                wp_set_current_user( intval( $result['user_id'] ) );

                return true;
            },
        ) );
    }

    /**
	 * Boot admin autonomy for MCP context — safe wrapper with error handling.
	 */
	public function boot_admin_autonomy_for_mcp() {
	    try {
	        $this->boot_admin_autonomy();
	    } catch ( \Throwable $e ) {
	        error_log( '[Niwa MCP] boot_admin_autonomy() EXCEPTION: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine() );
	        error_log( '[Niwa MCP] Stack trace: ' . $e->getTraceAsString() );
        }
    }

    /**
     * Boot the admin-autonomy subsystem — only called in admin/cron/ajax contexts.
     */
    private function boot_admin_autonomy() {
        $this->admin_autonomy_store    = new Niwa_AI_Admin_Autonomy_Store();
        $this->admin_autonomy_telegram = new Niwa_AI_Admin_Autonomy_Telegram();
        $this->admin_autonomy_whatsapp = new Niwa_AI_Admin_Autonomy_WhatsApp();
        $this->admin_autonomy_workspace = new Niwa_AI_Admin_Autonomy_Workspace();
        if ( $this->conversation_store && method_exists( $this->conversation_store, 'set_workspace' ) ) {
            $this->conversation_store->set_workspace( $this->admin_autonomy_workspace );
        }
        if ( $this->memory_searcher && method_exists( $this->memory_searcher, 'set_admin_autonomy_workspace' ) ) {
            $this->memory_searcher->set_admin_autonomy_workspace( $this->admin_autonomy_workspace );
        }

        // Only ensure workspace directories on non-AJAX, non-cron, non-REST admin requests.
        if ( ! wp_doing_ajax() && ! wp_doing_cron() && ! ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
            $this->admin_autonomy_workspace->ensure_workspace();
        }

        $this->admin_autonomy_email    = new Niwa_AI_Admin_Autonomy_Email( $this->contact_service, $this->admin_autonomy_workspace );
        $this->admin_autonomy_settings = new Niwa_AI_Admin_Autonomy_Settings( $this->admin_autonomy_store, $this->admin_autonomy_telegram, $this->admin_autonomy_whatsapp );
        $this->admin_autonomy_settings->set_email_service( $this->admin_autonomy_email );
        $this->admin_autonomy_settings->set_model_catalog( $this->model_catalog );
        $this->admin_autonomy_settings->set_performance_settings_ui( $this->performance_settings_ui );
        $this->admin_autonomy_tools    = new Niwa_AI_Admin_Autonomy_Tools();
        $this->admin_autonomy_tools->set_email_service( $this->admin_autonomy_email );
        $this->admin_product_management_service = new Niwa_AI_Admin_Product_Management_Service();
        $this->admin_product_target_resolver    = new Niwa_AI_Admin_Product_Target_Resolver( $this->memory_searcher );
        $this->admin_autonomy_tools->set_product_management_service( $this->admin_product_management_service );
        $this->admin_autonomy_tools->set_product_target_resolver( $this->admin_product_target_resolver );
        $this->admin_autonomy_tools->set_autonomy_store( $this->admin_autonomy_store );
        $this->admin_autonomy_scheduler = new Niwa_AI_Admin_Autonomy_Scheduler(
            $this->admin_autonomy_tools,
            $this->admin_autonomy_workspace,
            $this->admin_autonomy_telegram,
            $this->admin_autonomy_store,
            $this->conversation_store
        );
        $this->admin_autonomy_skills   = new Niwa_AI_Admin_Autonomy_Skills( $this->admin_autonomy_workspace );
        $this->admin_autonomy_tools->set_skills_manager( $this->admin_autonomy_skills );
        $this->admin_autonomy_tools->set_llm_dependencies( $this->openai_client, $this->founder_auth_manager, $this->model_router );
        $this->rlm_engine->set_skills_manager( $this->admin_autonomy_skills );
        $this->admin_autonomy_db_intelligence = new Niwa_AI_Admin_Autonomy_DB_Intelligence( $this->admin_autonomy_workspace );
        $this->admin_autonomy_site_discovery  = new Niwa_AI_Admin_Autonomy_Site_Discovery( $this->admin_autonomy_db_intelligence, $this->admin_autonomy_workspace );
        $this->admin_autonomy_code_intelligence = new Niwa_AI_Admin_Autonomy_Code_Intelligence( $this->admin_autonomy_site_discovery, $this->admin_autonomy_workspace );
        $this->admin_autonomy_self_improvement = new Niwa_AI_Admin_Autonomy_Self_Improvement_Manager(
            $this->founder_auth_manager,
            $this->openai_client,
            $this->model_router,
            $this->admin_autonomy_workspace
        );
        $this->admin_autonomy_session_state = new Niwa_AI_Admin_Autonomy_Session_State(
            $this->founder_auth_manager,
            $this->openai_client,
            $this->model_router,
            $this->admin_autonomy_workspace,
            $this->admin_autonomy_self_improvement
        );
        $this->admin_autonomy_control_plane = new Niwa_AI_Admin_Autonomy_Control_Plane(
            $this->admin_autonomy_workspace
        );
        $this->admin_autonomy_turn_engine = new Niwa_AI_Admin_Autonomy_Turn_Engine(
            $this->openai_client,
			$this->admin_autonomy_session_state,
			$this->admin_autonomy_control_plane
        );
        $this->admin_autonomy_companion_memory = new Niwa_AI_Companion_Memory_Manager(
            $this->admin_autonomy_workspace,
            $this->admin_autonomy_session_state,
            $this->founder_auth_manager,
            $this->openai_client,
            $this->model_router
        );
        $this->admin_autonomy_session_memory_store = new Niwa_AI_Session_Memory_Store(
            $this->embedding_client,
            $this->admin_autonomy_workspace
        );
        if ( $this->admin_autonomy_self_improvement && method_exists( $this->admin_autonomy_self_improvement, 'set_companion_memory_manager' ) ) {
            $this->admin_autonomy_self_improvement->set_companion_memory_manager( $this->admin_autonomy_companion_memory );
        }
        $this->admin_autonomy_actor_runtime = new Niwa_AI_Admin_Autonomy_Actor_Runtime(
            $this->admin_autonomy_control_plane,
            $this->admin_autonomy_telegram,
            array( $this, 'resolve_admin_autonomy_session_id' ),
            array( $this, 'run_admin_autonomy_actor_message' ),
            Niwa_AI_Admin_Autonomy::get_turn_worker_time_limit(),
            array(
                'request_runner_wake' => function ( $context = array() ) {
                    if ( $this->admin_autonomy && method_exists( $this->admin_autonomy, 'wake_actor_continuation_runtime' ) ) {
                        return $this->admin_autonomy->wake_actor_continuation_runtime( $context );
                    }

                    return array(
                        'success' => false,
                        'error'   => 'admin_autonomy_unavailable',
                    );
                },
                'record_scheduled_actor_result' => function ( $session_id, $entry, $result ) {
                    return $this->admin_autonomy_scheduler && method_exists( $this->admin_autonomy_scheduler, 'record_scheduled_actor_result' )
                        ? $this->admin_autonomy_scheduler->record_scheduled_actor_result( $session_id, $entry, $result )
                        : array(
                            'success' => false,
                            'error'   => 'scheduler_unavailable',
                        );
                },
                'validate_scheduled_actor_entry' => function ( $session_id, $entry ) {
                    return $this->admin_autonomy_scheduler && method_exists( $this->admin_autonomy_scheduler, 'validate_scheduled_actor_entry' )
                        ? $this->admin_autonomy_scheduler->validate_scheduled_actor_entry( $session_id, $entry )
                        : array(
                            'success' => false,
                            'error'   => 'scheduler_unavailable',
                        );
                },
				'fail_admin_ui_actor_job' => function ( $session_id, $entry, $error ) {
					return $this->admin_autonomy && method_exists( $this->admin_autonomy, 'fail_admin_ui_actor_job' )
						? $this->admin_autonomy->fail_admin_ui_actor_job( $session_id, $entry, $error )
						: false;
				},
            )
        );

        $this->admin_autonomy_subagent_manager = new Niwa_AI_Admin_Autonomy_Subagent_Manager(
            $this->admin_autonomy_turn_engine,
            $this->admin_autonomy_session_state,
            $this->admin_autonomy_telegram,
            $this->conversation_store,
            $this->admin_autonomy_workspace
        );
        $this->admin_autonomy_chat     = new Niwa_AI_Admin_Autonomy_Chat(
            $this->founder_auth_manager,
            $this->openai_client,
            $this->model_router,
            $this->store_overview,
            $this->memory_searcher,
            $this->content_store,
            $this->admin_autonomy_telegram,
            $this->admin_autonomy_store,
            $this->admin_autonomy_site_discovery,
            $this->admin_autonomy_db_intelligence,
            $this->admin_autonomy_code_intelligence,
            $this->admin_autonomy_workspace,
            $this->admin_autonomy_tools,
            $this->admin_autonomy_scheduler,
            $this->admin_autonomy_turn_engine,
            $this->admin_autonomy_session_state,
            $this->admin_autonomy_subagent_manager,
            $this->admin_autonomy_skills,
            $this->admin_autonomy_whatsapp
        );
        $this->admin_autonomy_chat->set_email_service( $this->admin_autonomy_email );
        $this->admin_autonomy_chat->set_conversation_store( $this->conversation_store );
        if ( method_exists( $this->admin_autonomy_chat, 'set_companion_memory_manager' ) ) {
            $this->admin_autonomy_chat->set_companion_memory_manager( $this->admin_autonomy_companion_memory );
        }
        if ( method_exists( $this->admin_autonomy_chat, 'set_session_memory_store' ) ) {
            $this->admin_autonomy_chat->set_session_memory_store( $this->admin_autonomy_session_memory_store );
        }
        $this->admin_autonomy          = new Niwa_AI_Admin_Autonomy(
            $this->admin_autonomy_store,
            new Niwa_AI_Admin_Autonomy_Executor( $this->admin_autonomy_tools ),
            $this->admin_autonomy_telegram,
            $this->admin_autonomy_chat,
            $this->conversation_store,
            $this->admin_autonomy_scheduler,
            $this->admin_autonomy_session_state,
            $this->admin_autonomy_actor_runtime,
            $this->admin_autonomy_control_plane,
            $this->admin_autonomy_whatsapp
        );
        if ( $this->admin_autonomy_subagent_manager && method_exists( $this->admin_autonomy_subagent_manager, 'set_callbacks' ) ) {
            $this->admin_autonomy_subagent_manager->set_callbacks(
                array(
                    'notify_parent_result' => function ( $job, $result ) {
                        return $this->admin_autonomy && method_exists( $this->admin_autonomy, 'enqueue_subagent_result' )
                            ? $this->admin_autonomy->enqueue_subagent_result( $job, $result )
                            : false;
                    },
                    'record_scheduled_result' => function ( $job, $result ) {
                        if ( ! $this->admin_autonomy_scheduler || ! method_exists( $this->admin_autonomy_scheduler, 'record_scheduled_actor_result' ) ) {
                            return array( 'success' => false, 'error' => 'scheduler_unavailable' );
                        }
                        $entry       = isset( $job['scheduled_actor_entry'] ) && is_array( $job['scheduled_actor_entry'] ) ? $job['scheduled_actor_entry'] : array();
                        $entry['id'] = 'subagent-' . (string) ( $job['id'] ?? '' );
                        return $this->admin_autonomy_scheduler->record_scheduled_actor_result(
                            (string) ( $job['parent_session_id'] ?? '' ),
                            $entry,
                            is_array( $result ) ? $result : array()
                        );
                    },
                )
            );
        }
        if ( $this->admin_autonomy_scheduler && method_exists( $this->admin_autonomy_scheduler, 'set_callbacks' ) ) {
            $this->admin_autonomy_scheduler->set_callbacks(
                array(
                    'enqueue_scheduled_self_wake' => function ( $message, $job = array() ) {
                        return $this->admin_autonomy && method_exists( $this->admin_autonomy, 'enqueue_scheduled_self_wake' )
                            ? $this->admin_autonomy->enqueue_scheduled_self_wake( $message, $job )
                            : array(
                                'success' => false,
                                'error'   => 'admin_autonomy_unavailable',
                            );
                    },
					'cancel_scheduled_subagents' => function ( $job_id, $job_uuid, $reason = '' ) {
						return $this->admin_autonomy_subagent_manager && method_exists( $this->admin_autonomy_subagent_manager, 'cancel_jobs_for_scheduled_job' )
							? $this->admin_autonomy_subagent_manager->cancel_jobs_for_scheduled_job( $job_id, $job_uuid, $reason )
							: array( 'success' => true, 'cancelled' => 0, 'signaled' => 0 );
					},
                    'find_scheduled_actor_message' => function ( $session_id, $job_id, $job_uuid, $attempt_token = '' ) {
                        if ( $this->admin_autonomy_subagent_manager && method_exists( $this->admin_autonomy_subagent_manager, 'find_scheduled_job' ) ) {
                            $subagent = (array) $this->admin_autonomy_subagent_manager->find_scheduled_job( $job_id, $job_uuid, $attempt_token );
                            if ( ! empty( $subagent['found'] ) ) {
                                return $subagent;
                            }
                        }
                        return $this->admin_autonomy_control_plane && method_exists( $this->admin_autonomy_control_plane, 'find_actor_message_by_scheduled_job' )
                            ? $this->admin_autonomy_control_plane->find_actor_message_by_scheduled_job( $session_id, $job_id, $job_uuid, $attempt_token )
                            : array(
                                'found' => false,
                                'error' => 'control_plane_unavailable',
                            );
                    },
                    'recover_orphaned_actor_messages' => function ( $reason = '', $limit = 50, $max_retries = 1, $recover_live_leases = false ) {
                        return $this->admin_autonomy_control_plane && method_exists( $this->admin_autonomy_control_plane, 'recover_orphaned_active_actor_messages' )
                            ? $this->admin_autonomy_control_plane->recover_orphaned_active_actor_messages( $reason, $limit, $max_retries, $recover_live_leases )
                            : array(
                                'requeued'     => 0,
                                'cancelled'    => 0,
                                'skipped_live' => 0,
                                'sessions'     => array(),
                            );
                    },
                    'wake_scheduled_actor_runtime' => function ( $job = array(), $queued = array() ) {
                        $context = array(
                            'reason'     => 'scheduled_actor_pending',
                            'session_id' => is_array( $job ) && ! empty( $job['session_id'] ) ? (string) $job['session_id'] : '',
                            'actor_id'   => is_array( $queued ) && ! empty( $queued['actor_id'] ) ? (string) $queued['actor_id'] : '',
                        );

                        return $this->admin_autonomy && method_exists( $this->admin_autonomy, 'wake_actor_continuation_runtime' )
                            ? $this->admin_autonomy->wake_actor_continuation_runtime( $context )
                            : array(
                                'success' => false,
                                'error'   => 'admin_autonomy_unavailable',
                            );
                    },
                )
            );
        }
        if ( method_exists( $this->admin_autonomy_chat, 'set_daily_report_dispatcher' ) ) {
            $this->admin_autonomy_chat->set_daily_report_dispatcher(
                array( $this->admin_autonomy, 'enqueue_daily_report_actor_message' )
            );
        }
        $admin_autonomy_runtime_version = get_option( Niwa_AI_Constants::OPT_AUTONOMY_RUNTIME_VERSION, '' );
        if ( NIWA_AI_VERSION !== (string) $admin_autonomy_runtime_version && method_exists( $this->admin_autonomy, 'cleanup_legacy_turn_runtime' ) ) {
            $this->admin_autonomy->cleanup_legacy_turn_runtime();
        }

        // DB schema check: use a version flag to avoid SHOW TABLES on every request.
        $this->maybe_ensure_autonomy_tables();

        $this->admin_autonomy_settings->register_hooks();
        $this->admin_autonomy_db_intelligence->register_hooks();
        $this->admin_autonomy_site_discovery->register_hooks();
        $this->admin_autonomy_code_intelligence->register_hooks();
        $this->admin_autonomy->register_hooks();
        $this->maybe_start_admin_autonomy_background_clock( 'admin-autonomy-boot' );
        $this->maybe_restart_admin_autonomy_runtime_after_plugin_update( $admin_autonomy_runtime_version );
    }

    /**
     * Restart Telegram/actor loopback once after a deployed plugin version changes.
     *
     * Code updates via git/FTP do not fire the plugin activation hook, so the
     * old loopback runner can look alive in stored state while no fresh request
     * is actually polling Telegram. This keeps the founder agent responsive
     * after deploys without requiring a manual "Poll now" click.
     *
     * @param string $previous_version Last version that restarted the runtime.
     * @return void
     */
    private function maybe_restart_admin_autonomy_runtime_after_plugin_update( $previous_version ) {
        $previous_version = trim( (string) $previous_version );
        if ( NIWA_AI_VERSION !== $previous_version ) {
            update_option( Niwa_AI_Constants::OPT_AUTONOMY_RUNTIME_VERSION, NIWA_AI_VERSION, false );

            if ( function_exists( 'niwa_ai_queue_admin_autonomy_poll_restart' ) ) {
                niwa_ai_queue_admin_autonomy_poll_restart( 'plugin-version-change' );
            }

            $this->maybe_start_admin_autonomy_background_clock( 'plugin-version-change' );
        }

        $whatsapp_version = get_option( Niwa_AI_Constants::OPT_WHATSAPP_RUNTIME_VERSION, '' );
        if ( NIWA_AI_VERSION !== (string) $whatsapp_version ) {
            $this->refresh_whatsapp_runtime_after_plugin_update( NIWA_AI_VERSION );
        }
    }

    public function handle_whatsapp_runtime_refresh_after_plugin_update( $plugin_version = '' ) {
        $plugin_version = '' !== trim( (string) $plugin_version ) ? trim( (string) $plugin_version ) : NIWA_AI_VERSION;
        if ( ! $this->admin_autonomy_whatsapp ) {
            $this->ensure_core_services();
            if ( ! $this->admin_autonomy ) {
                $this->boot_admin_autonomy();
            }
        }
        return $this->refresh_whatsapp_runtime_after_plugin_update( $plugin_version );
    }

    private function refresh_whatsapp_runtime_after_plugin_update( $plugin_version ) {
        if ( ! $this->admin_autonomy_whatsapp || ! method_exists( $this->admin_autonomy_whatsapp, 'refresh_runtime_after_plugin_update' ) ) {
            return array( 'success' => true, 'skipped' => 'whatsapp_runtime_unavailable' );
        }

        $result = (array) $this->admin_autonomy_whatsapp->refresh_runtime_after_plugin_update( $plugin_version );
        if ( ! empty( $result['success'] ) ) {
            update_option( Niwa_AI_Constants::OPT_WHATSAPP_RUNTIME_VERSION, (string) $plugin_version, false );
        }
        if ( empty( $result['success'] ) && ! empty( $result['retry'] ) ) {
            $args = array( (string) $plugin_version );
            if (
                function_exists( 'wp_schedule_single_event' )
                && ( ! function_exists( 'wp_next_scheduled' ) || ! wp_next_scheduled( self::WHATSAPP_RUNTIME_REFRESH_HOOK, $args ) )
            ) {
                wp_schedule_single_event( time() + MINUTE_IN_SECONDS, self::WHATSAPP_RUNTIME_REFRESH_HOOK, $args );
            }
        }
        return $result;
    }

    /**
     * Check DB table schema using a version flag instead of SHOW TABLES on every request.
     *
     * Runs three independent gates:
     *   1. Version-bump gate — on plugin version change, runs all schema ensurers
     *      (autonomy + scheduler + lead form + cart recovery) then records the
     *      new version.
     *   2. Dedicated lead-form gate — safety net that guarantees the lead-form
     *      tables exist even if the version option is already == plugin version
     *      (e.g. site was on 0.12.x before the tables were introduced in-branch,
     *      or the option drifted). dbDelta is idempotent; tracked via a separate
     *      `OPT_LEAD_FORM_SCHEMA_READY` flag stamped with the version that last
     *      provisioned them.
     *   3. Dedicated cart-recovery gate — same safety-net pattern as #2 for the
     *      two cart-recovery tables, tracked via `OPT_CART_RECOVERY_SCHEMA_READY`.
     */
    private function maybe_ensure_autonomy_tables() {
        $current = get_option( Niwa_AI_Constants::OPT_DB_SCHEMA_VERSION, '' );

        if ( NIWA_AI_VERSION !== $current ) {
			$content_schema_ready = false;
			// Content-index schema participates in the same version gate. This
			// must run before the shared schema stamp is advanced so existing
			// sites receive the lossless binary embedding columns on upgrade.
			if ( $this->content_store ) {
				$content_schema_ready = true === $this->content_store->ensure_tables();
			}

            $this->admin_autonomy_store->maybe_ensure_table_schema();
            $this->admin_autonomy_scheduler->maybe_ensure_table_schema();
			if ( $this->contact_service && method_exists( $this->contact_service, 'ensure_table_schema' ) ) {
				$this->contact_service->ensure_table_schema( true );
			}
			if ( $this->appointment_service && method_exists( $this->appointment_service, 'ensure_schema' ) ) {
				$this->appointment_service->ensure_schema( true );
			}
            if ( $this->admin_autonomy_session_memory_store && method_exists( $this->admin_autonomy_session_memory_store, 'ensure_table_schema' ) ) {
                $this->admin_autonomy_session_memory_store->ensure_table_schema();
                update_option( Niwa_AI_Constants::OPT_SESSION_MEMORY_SCHEMA_READY, Niwa_AI_Constants::SESSION_MEMORY_SCHEMA_STAMP, true );
            }

            // Lead Form Generator tables (0.12.0+).
            $lead_form_store = new Niwa_AI_Lead_Form_Session_Store();
            $lead_form_store->ensure_table_schema();
            update_option( Niwa_AI_Constants::OPT_LEAD_FORM_SCHEMA_READY, NIWA_AI_VERSION, true );

            // Cart Recovery tables (0.12.0+). Use SCHEMA_STAMP (not plugin
            // version) so mid-feature schema additions (like Phase 7's
            // send_attempts column) trigger a re-run on existing installs
            // without forcing a full plugin-version bump.
            $cart_recovery_repo = new Niwa_AI_Cart_Recovery_Repository();
            if ( $cart_recovery_repo->ensure_table_schema() ) {
                update_option( Niwa_AI_Constants::OPT_CART_RECOVERY_SCHEMA_READY, Niwa_AI_Constants::CART_RECOVERY_SCHEMA_STAMP, true );
            }

            $this->maybe_backfill_session_memory_store();
			if ( $this->index_worker && method_exists( $this->index_worker, 'maybe_schedule_embedding_binary_backfill' ) ) {
				$this->index_worker->maybe_schedule_embedding_binary_backfill();
			}

            // Never stamp a successful plugin schema version on dbDelta's
            // intent alone. A failed or partial content-index ALTER keeps the
            // previous stamp so the next admin/cron boot retries the upgrade.
            if ( $content_schema_ready ) {
                update_option( Niwa_AI_Constants::OPT_DB_SCHEMA_VERSION, NIWA_AI_VERSION, true );
            } elseif ( function_exists( 'do_action' ) ) {
                do_action( 'niwa_ai_content_schema_upgrade_failed', NIWA_AI_VERSION );
            }
            return;
        }

        // Safety net: ensure lead-form tables exist even when the version gate
        // didn't fire. Covers the case where a site carried a stamped
        // OPT_DB_SCHEMA_VERSION from before the lead-form tables were added to
        // the version-bump branch. dbDelta is idempotent.
        $lf_ready = get_option( Niwa_AI_Constants::OPT_LEAD_FORM_SCHEMA_READY, '' );
        if ( NIWA_AI_VERSION !== $lf_ready ) {
            $lead_form_store = new Niwa_AI_Lead_Form_Session_Store();
            $lead_form_store->ensure_table_schema();
            update_option( Niwa_AI_Constants::OPT_LEAD_FORM_SCHEMA_READY, NIWA_AI_VERSION, true );
        }

        // Same safety-net pattern for cart-recovery tables — compares
        // against SCHEMA_STAMP so mid-branch column additions take effect.
        $cr_ready = get_option( Niwa_AI_Constants::OPT_CART_RECOVERY_SCHEMA_READY, '' );
        if ( Niwa_AI_Constants::CART_RECOVERY_SCHEMA_STAMP !== $cr_ready ) {
            $cart_recovery_repo = new Niwa_AI_Cart_Recovery_Repository();
            if ( $cart_recovery_repo->ensure_table_schema() ) {
                update_option( Niwa_AI_Constants::OPT_CART_RECOVERY_SCHEMA_READY, Niwa_AI_Constants::CART_RECOVERY_SCHEMA_STAMP, true );
            }
        }

        $sm_ready = get_option( Niwa_AI_Constants::OPT_SESSION_MEMORY_SCHEMA_READY, '' );
        if ( Niwa_AI_Constants::SESSION_MEMORY_SCHEMA_STAMP !== $sm_ready && $this->admin_autonomy_session_memory_store && method_exists( $this->admin_autonomy_session_memory_store, 'ensure_table_schema' ) ) {
            $this->admin_autonomy_session_memory_store->ensure_table_schema();
            update_option( Niwa_AI_Constants::OPT_SESSION_MEMORY_SCHEMA_READY, Niwa_AI_Constants::SESSION_MEMORY_SCHEMA_STAMP, true );
        }

        $this->maybe_backfill_session_memory_store();
    }

    private function maybe_backfill_session_memory_store() {
        if ( ! $this->admin_autonomy_session_memory_store || ! $this->admin_autonomy_workspace ) {
            return;
        }

        $ready = get_option( Niwa_AI_Constants::OPT_SESSION_MEMORY_BACKFILL_READY, '' );
        if ( Niwa_AI_Constants::SESSION_MEMORY_SCHEMA_STAMP === $ready ) {
            return;
        }

        if ( method_exists( $this->admin_autonomy_session_memory_store, 'backfill_from_workspace' ) ) {
            $this->admin_autonomy_session_memory_store->backfill_from_workspace( $this->admin_autonomy_workspace, 800 );
        }

        update_option( Niwa_AI_Constants::OPT_SESSION_MEMORY_BACKFILL_READY, Niwa_AI_Constants::SESSION_MEMORY_SCHEMA_STAMP, false );
    }

    public function resolve_admin_autonomy_session_id( $message ) {
        if ( ! $this->admin_autonomy || ! method_exists( $this->admin_autonomy, 'resolve_actor_session_id' ) ) {
            return '';
        }

        return (string) $this->admin_autonomy->resolve_actor_session_id( $message );
    }

    public function run_admin_autonomy_actor_message( $session_id, $entry ) {
        if ( ! $this->admin_autonomy || ! method_exists( $this->admin_autonomy, 'run_session_actor_message' ) ) {
            return array(
                'success' => false,
                'error'   => 'Admin autonomy runtime is not available.',
            );
        }

        return (array) $this->admin_autonomy->run_session_actor_message( $session_id, $entry );
    }

    public function run_admin_autonomy_cli_runner( $reason = 'cli', $runner_id = '' ) {
        if ( ! $this->admin_autonomy || ! method_exists( $this->admin_autonomy, 'run_cli_runner' ) ) {
            return array(
                'success' => false,
                'error'   => 'Admin autonomy runtime is not available.',
            );
        }

        return (array) $this->admin_autonomy->run_cli_runner( $reason, $runner_id );
    }

    public function run_admin_autonomy_subagent_worker( $job_id = '' ) {
        if ( ! $this->admin_autonomy_subagent_manager || ! $this->admin_autonomy_chat || ! method_exists( $this->admin_autonomy_subagent_manager, 'process_job' ) ) {
            return array(
                'success' => false,
                'error'   => 'Subagent runtime is not available.',
            );
        }

        return (array) $this->admin_autonomy_subagent_manager->process_job( $this->admin_autonomy_chat, $job_id );
    }

    public function run_admin_autonomy_cli_typing_runner( $chat_id = '', $token = '' ) {
        if ( ! $this->admin_autonomy || ! method_exists( $this->admin_autonomy, 'run_cli_typing_runner' ) ) {
            return array(
                'success' => false,
                'error'   => 'Admin autonomy runtime is not available.',
            );
        }

        return (array) $this->admin_autonomy->run_cli_typing_runner( $chat_id, $token );
    }

    public function handle_admin_autonomy_imap_idle_watchdog() {
        if ( ! class_exists( 'Niwa_AI_Admin_Autonomy_IMAP_IDLE_Listener' ) ) {
            return array(
                'success' => false,
                'error'   => 'IMAP IDLE listener class is unavailable.',
            );
        }

        if ( ! $this->admin_autonomy_email ) {
            try {
                $this->ensure_core_services();
                if ( ! $this->admin_autonomy ) {
                    $this->boot_admin_autonomy();
                }
            } catch ( \Throwable $e ) {
                return array(
                    'success' => false,
                    'error'   => 'Admin autonomy boot failed before IMAP watchdog: ' . $e->getMessage(),
                );
            }
        }

        $listener = new Niwa_AI_Admin_Autonomy_IMAP_IDLE_Listener(
            $this->admin_autonomy_email,
            $this->admin_autonomy_telegram
        );

        return $listener->maybe_start_supervisor();
    }

    public function handle_admin_autonomy_background_clock_watchdog() {
        return $this->maybe_start_admin_autonomy_background_clock( 'watchdog' );
    }

    public function handle_admin_autonomy_background_clock_wake( $reason = 'wake-background' ) {
        $reason = sanitize_key( (string) $reason );
        if ( '' === $reason ) {
            $reason = 'wake-background';
        }

        return $this->maybe_start_admin_autonomy_background_clock( $reason );
    }

    private function maybe_start_admin_autonomy_background_clock( $reason = 'boot' ) {
        if ( defined( 'NIWA_AI_BACKGROUND_RUNNER' ) && NIWA_AI_BACKGROUND_RUNNER ) {
            return array(
                'success' => true,
                'status'  => 'current_background_runner',
            );
        }

        if ( ! class_exists( 'Niwa_AI_Admin_Autonomy_Background_Clock' ) ) {
            return array(
                'success' => false,
                'error'   => 'background_clock_unavailable',
            );
        }

        $clock = new Niwa_AI_Admin_Autonomy_Background_Clock();
        return $clock->maybe_start( $reason );
    }

    /**
     * Register WP-Cron events. Only called on admin page loads (not AJAX, not frontend).
     */
    private function maybe_register_cron_events() {
        if ( class_exists( 'Niwa_AI_Cron_Maintenance' ) ) {
            Niwa_AI_Cron_Maintenance::dedupe_managed_hooks();
        }

        $slug = class_exists( 'Niwa_AI_Performance_Settings' )
            ? Niwa_AI_Performance_Settings::resolve_schedule_for_hook( 'niwa_ai_process_memory_embeddings', 'niwa_ai_five_minutes' )
            : 'niwa_ai_five_minutes';
        $this->ensure_recurring_cron_event( 'niwa_ai_process_memory_embeddings', $slug, 120 );

        $this->ensure_recurring_cron_event( 'niwa_ai_process_lead_notifications', 'hourly', 300 );

        $slug = class_exists( 'Niwa_AI_Performance_Settings' )
            ? Niwa_AI_Performance_Settings::resolve_schedule_for_hook( 'niwa_ai_process_frontend_chat_queue', 'niwa_ai_one_minute' )
            : 'niwa_ai_one_minute';
        $this->ensure_recurring_cron_event( 'niwa_ai_process_frontend_chat_queue', $slug, 45 );

        $this->ensure_recurring_cron_event( 'niwa_ai_admin_autonomy_background_clock_watchdog', 'niwa_ai_one_minute', 30 );

        if ( class_exists( 'Niwa_AI_Admin_Autonomy' ) ) {
            $this->ensure_recurring_cron_event( Niwa_AI_Admin_Autonomy::VENDOR_INTEGRITY_HOOK, 'daily', 300 );
        }

        $imap_watchdog_hook = 'niwa_ai_admin_autonomy_imap_idle_watchdog';
        $imap_watchdog_next = wp_next_scheduled( $imap_watchdog_hook );
        $imap_watchdog_was_stale = $imap_watchdog_next && $imap_watchdog_next < ( time() - 15 * MINUTE_IN_SECONDS );
        if ( $imap_watchdog_was_stale ) {
            wp_clear_scheduled_hook( $imap_watchdog_hook );
            $imap_watchdog_next = false;
        }

        if ( ! $imap_watchdog_next ) {
            $slug = class_exists( 'Niwa_AI_Performance_Settings' )
                ? Niwa_AI_Performance_Settings::resolve_schedule_for_hook( $imap_watchdog_hook, 'niwa_ai_five_minutes' )
                : 'niwa_ai_five_minutes';
            $this->ensure_recurring_cron_event( $imap_watchdog_hook, $slug, 90 );
        }

        if ( $imap_watchdog_was_stale ) {
            $this->handle_admin_autonomy_imap_idle_watchdog();
        }

        if ( class_exists( 'Niwa_AI_Admin_Autonomy' ) ) {
            $whatsapp_watchdog_hook = Niwa_AI_Admin_Autonomy::WHATSAPP_GATEWAY_WATCHDOG_HOOK;
            if ( $this->admin_autonomy_whatsapp && $this->admin_autonomy_whatsapp->is_enabled() ) {
                $this->ensure_recurring_cron_event( $whatsapp_watchdog_hook, 'niwa_ai_one_minute', 60 );
            } else {
                $next_whatsapp_watchdog = wp_next_scheduled( $whatsapp_watchdog_hook );
                while ( $next_whatsapp_watchdog ) {
                    if ( false === wp_unschedule_event( $next_whatsapp_watchdog, $whatsapp_watchdog_hook ) ) {
                        break;
                    }
                    $next_whatsapp_watchdog = wp_next_scheduled( $whatsapp_watchdog_hook );
                }
            }
        }

        // Cart Recovery: every-5-minutes state-machine tick.
        // Deferred to `init` — Action Scheduler's data store initializes on
        // `init`, so calling `as_next_scheduled_action()` earlier (e.g. from
        // `plugins_loaded`, where this method currently runs) triggers a
        // `_doing_it_wrong` notice in wp-admin logs.
        if ( $this->cart_recovery_scheduler ) {
            $scheduler = $this->cart_recovery_scheduler;
            if ( did_action( 'init' ) ) {
                $scheduler->schedule_tick();
            } else {
                add_action(
                    'init',
                    static function () use ( $scheduler ) {
                        $scheduler->schedule_tick();
                    },
                    20
                );
            }
        }

        if ( $this->admin_autonomy_telegram && $this->admin_autonomy_telegram->is_enabled() ) {
            $slug = class_exists( 'Niwa_AI_Performance_Settings' )
                ? Niwa_AI_Performance_Settings::resolve_schedule_for_hook( 'niwa_ai_admin_autonomy_poll_telegram', 'niwa_ai_thirty_seconds' )
                : 'niwa_ai_thirty_seconds';
            $this->ensure_recurring_cron_event( 'niwa_ai_admin_autonomy_poll_telegram', $slug, 30 );
        } else {
            $next_poll = wp_next_scheduled( 'niwa_ai_admin_autonomy_poll_telegram' );
            while ( $next_poll ) {
                if ( false === wp_unschedule_event( $next_poll, 'niwa_ai_admin_autonomy_poll_telegram' ) ) {
                    break;
                }
                $next_poll = wp_next_scheduled( 'niwa_ai_admin_autonomy_poll_telegram' );
            }
        }

        // Background tick: never forcibly unschedule based on slug match —
        // that was the regression that silently overrode Performance Settings
        // every boot. We only schedule if no event exists; otherwise we
        // leave whatever the user (or Performance Settings save) set in place.
        $slug = class_exists( 'Niwa_AI_Performance_Settings' )
            ? Niwa_AI_Performance_Settings::resolve_schedule_for_hook( Niwa_AI_Admin_Autonomy::BACKGROUND_TICK_HOOK, 'niwa_ai_one_minute' )
            : 'niwa_ai_one_minute';
        $this->ensure_recurring_cron_event( Niwa_AI_Admin_Autonomy::BACKGROUND_TICK_HOOK, $slug, 60 );

        $slug = class_exists( 'Niwa_AI_Performance_Settings' )
            ? Niwa_AI_Performance_Settings::resolve_schedule_for_hook( 'niwa_ai_admin_autonomy_process_scheduled_tasks', 'niwa_ai_thirty_seconds' )
            : 'niwa_ai_thirty_seconds';
        $this->ensure_recurring_cron_event( 'niwa_ai_admin_autonomy_process_scheduled_tasks', $slug, 30 );

    }

    private function ensure_recurring_cron_event( $hook, $schedule, $offset = 60, $args = array() ) {
        if ( class_exists( 'Niwa_AI_Cron_Maintenance' ) ) {
            return Niwa_AI_Cron_Maintenance::ensure_recurring_event( $hook, $schedule, $offset, $args );
        }

        if ( ! wp_next_scheduled( $hook, $args ) ) {
            return false !== wp_schedule_event( time() + max( 1, intval( $offset ) ), $schedule, $hook, $args );
        }

        return true;
    }

    /* ================================================================
     * Dependencies
     * ============================================================= */

    /**
     * Load dependencies.
     *
     * All classes, traits, and interfaces are resolved via Niwa_AI_Autoloader
     * (registered in niwa-ai.php). Only vendor libraries that don't follow the
     * Niwa_AI_* naming convention need manual loading here.
     */
    private function load_dependencies() {
        // Vendor libraries are now handled by Composer autoloader (loaded in niwa-ai.php).
    }

    public function load_textdomain() {
        load_plugin_textdomain( 'niwa-ai', false, dirname( plugin_basename( NIWA_AI_FILE ) ) . '/languages' );
    }
}
