<?php

/**
 * The admin-specific functionality of the plugin.
 *
 * @package Inqyra
 */
// If this file is called directly, abort.
if ( !defined( 'WPINC' ) ) {
    die;
}
/**
 * Admin class.
 */
class Inqyra_Admin {
    /**
     * Plugin name.
     *
     * @var string
     */
    private $plugin_name;

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

    /**
     * Initialize the class.
     *
     * @param string $plugin_name The plugin name.
     * @param string $version     The plugin version.
     */
    public function __construct( $plugin_name, $version ) {
        $this->plugin_name = $plugin_name;
        $this->version = $version;
    }

    /**
     * Register admin menu.
     */
    public function add_admin_menu() {
        // Main menu
        add_menu_page(
            __( 'Inqyra', 'inqyra' ),
            __( 'Inqyra', 'inqyra' ),
            'manage_options',
            'inqyra',
            [$this, 'render_dashboard_page'],
            'dashicons-format-chat',
            30
        );
        // Dashboard (same as main)
        add_submenu_page(
            'inqyra',
            __( 'Dashboard', 'inqyra' ),
            __( 'Dashboard', 'inqyra' ),
            'manage_options',
            'inqyra',
            [$this, 'render_dashboard_page']
        );
        // Agent Configuration
        add_submenu_page(
            'inqyra',
            __( 'Configuration', 'inqyra' ),
            __( 'Configuration', 'inqyra' ),
            'manage_options',
            'inqyra-config',
            [$this, 'render_config_page']
        );
        // Knowledge Base
        add_submenu_page(
            'inqyra',
            __( 'Knowledge Base', 'inqyra' ),
            __( 'Knowledge Base', 'inqyra' ),
            'manage_options',
            'inqyra-knowledge',
            [$this, 'render_knowledge_page']
        );
        // Conversations
        add_submenu_page(
            'inqyra',
            __( 'Conversations', 'inqyra' ),
            __( 'Conversations', 'inqyra' ),
            'manage_options',
            'inqyra-conversations',
            [$this, 'render_conversations_page']
        );
        // Settings
        add_submenu_page(
            'inqyra',
            __( 'Settings', 'inqyra' ),
            __( 'Settings', 'inqyra' ),
            'manage_options',
            'inqyra-settings',
            [$this, 'render_settings_page']
        );
        // Free: show premium features as upsell items
        add_submenu_page(
            'inqyra',
            __( 'Documents', 'inqyra' ),
            __( 'Documents', 'inqyra' ) . ' <span class="inqyra-premium-badge">Premium</span>',
            'manage_options',
            'inqyra-documents',
            [$this, 'render_upgrade_page']
        );
        add_submenu_page(
            'inqyra',
            __( 'Synonyms', 'inqyra' ),
            __( 'Synonyms', 'inqyra' ) . ' <span class="inqyra-premium-badge">Premium</span>',
            'manage_options',
            'inqyra-synonyms',
            [$this, 'render_upgrade_page']
        );
        add_submenu_page(
            'inqyra',
            __( 'Webhooks', 'inqyra' ),
            __( 'Webhooks', 'inqyra' ) . ' <span class="inqyra-premium-badge">Premium</span>',
            'manage_options',
            'inqyra-webhooks',
            [$this, 'render_upgrade_page']
        );
        add_submenu_page(
            'inqyra',
            __( 'Visibility', 'inqyra' ),
            __( 'Visibility', 'inqyra' ) . ' <span class="inqyra-premium-badge">Premium</span>',
            'manage_options',
            'inqyra-page-targeting',
            [$this, 'render_upgrade_page']
        );
        add_submenu_page(
            'inqyra',
            __( 'Statistics', 'inqyra' ),
            __( 'Statistics', 'inqyra' ) . ' <span class="inqyra-premium-badge">Premium</span>',
            'manage_options',
            'inqyra-stats',
            [$this, 'render_upgrade_page']
        );
        // Setup Wizard (hidden from menu)
        add_submenu_page(
            null,
            __( 'Setup Wizard', 'inqyra' ),
            __( 'Setup Wizard', 'inqyra' ),
            'manage_options',
            'inqyra-wizard',
            [$this, 'render_wizard_page']
        );
    }

    /**
     * Enqueue admin styles.
     *
     * @param string $hook The current admin page.
     */
    public function enqueue_styles( $hook ) {
        // Only load on our plugin pages
        if ( strpos( $hook, 'inqyra' ) === false ) {
            return;
        }
        wp_enqueue_style(
            $this->plugin_name,
            INQYRA_URL . 'admin/css/inqyra-admin.css',
            [],
            $this->version
        );
        // Load widget CSS on config and wizard pages for live preview.
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Admin page routing only.
        $page = ( isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : '' );
        if ( $page === 'inqyra-config' || $page === 'inqyra-wizard' ) {
            wp_enqueue_style(
                $this->plugin_name . '-widget-preview',
                INQYRA_URL . 'public/css/inqyra-widget.css',
                [$this->plugin_name],
                $this->version
            );
        }
    }

    /**
     * Enqueue admin scripts.
     *
     * @param string $hook The current admin page.
     */
    public function enqueue_scripts( $hook ) {
        // Only load on our plugin pages
        if ( strpos( $hook, 'inqyra' ) === false ) {
            return;
        }
        wp_enqueue_script(
            $this->plugin_name,
            INQYRA_URL . 'admin/js/inqyra-admin.js',
            ['jquery'],
            $this->version,
            true
        );
        // Localize script
        wp_localize_script( $this->plugin_name, 'inqyraAdmin', [
            'ajaxUrl'            => admin_url( 'admin-ajax.php' ),
            'nonce'              => wp_create_nonce( 'inqyra_admin' ),
            'isFree'             => true,
            'isPremium'          => false,
            'indexingLabels'     => ( class_exists( 'Inqyra_Indexing_Labels' ) ? Inqyra_Indexing_Labels::get_all() : [] ),
            'agentLanguage'      => $this->get_agent_language(),
            'widgetPlaceholders' => Inqyra_Public::get_placeholder_translations(),
            'strings'            => [
                'saving'         => __( 'Saving...', 'inqyra' ),
                'saved'          => __( 'Saved!', 'inqyra' ),
                'error'          => __( 'Error saving settings.', 'inqyra' ),
                'confirm_delete' => __( 'Are you sure you want to delete this item?', 'inqyra' ),
                'crawling'       => __( 'Crawling...', 'inqyra' ),
                'crawl_complete' => __( 'Crawl complete!', 'inqyra' ),
                'selectIcon'     => __( 'Select Header Icon', 'inqyra' ),
                'useIcon'        => __( 'Use this icon', 'inqyra' ),
                'yes'            => __( 'Yes', 'inqyra' ),
                'no'             => __( 'No', 'inqyra' ),
                'edit'           => __( 'Edit', 'inqyra' ),
                'delete'         => __( 'Delete', 'inqyra' ),
                'add_synonym'    => __( 'Add Synonym', 'inqyra' ),
                'update_synonym' => __( 'Update Synonym', 'inqyra' ),
                'processing'     => __( 'Processing...', 'inqyra' ),
                'retry'          => __( 'Retry', 'inqyra' ),
                'search_pages'   => __( 'Search pages...', 'inqyra' ),
                'save_changes'   => __( 'Save Changes', 'inqyra' ),
            ],
        ] );
        // Load media library and Select2 on config page.
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Admin page routing only.
        $page = ( isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : '' );
        if ( $page === 'inqyra-config' ) {
            wp_enqueue_media();
            // Load Select2 for page selector
            wp_enqueue_style(
                'inqyra-select2',
                INQYRA_URL . 'admin/vendor/select2/select2.min.css',
                [],
                '4.1.0'
            );
            wp_enqueue_script(
                'inqyra-select2',
                INQYRA_URL . 'admin/vendor/select2/select2.min.js',
                ['jquery'],
                '4.1.0',
                true
            );
        }
        // Load Select2 on page targeting page
        if ( $page === 'inqyra-page-targeting' ) {
            wp_enqueue_style(
                'inqyra-select2',
                INQYRA_URL . 'admin/vendor/select2/select2.min.css',
                [],
                '4.1.0'
            );
            wp_enqueue_script(
                'inqyra-select2',
                INQYRA_URL . 'admin/vendor/select2/select2.min.js',
                ['jquery'],
                '4.1.0',
                true
            );
            wp_enqueue_script(
                'inqyra-page-targeting',
                INQYRA_URL . 'admin/js/inqyra-page-targeting.js',
                ['jquery', 'inqyra-select2', $this->plugin_name],
                INQYRA_VERSION,
                true
            );
        }
        // Check for pending embeddings from auto-recrawl
        $agent = $this->get_current_agent();
        $pending_from_recrawl = 0;
        if ( $agent ) {
            $pending_from_recrawl = (int) get_transient( 'inqyra_pending_embeddings_' . $agent->id );
            if ( $pending_from_recrawl > 0 ) {
                delete_transient( 'inqyra_pending_embeddings_' . $agent->id );
            }
        }
        // Load embeddings module on knowledge page, documents page, or any page with pending embeddings
        if ( strpos( $hook, 'inqyra-knowledge' ) !== false || strpos( $hook, 'inqyra-documents' ) !== false || $pending_from_recrawl > 0 ) {
            add_action( 'admin_footer', [$this, 'load_embeddings_module'] );
        }
        wp_localize_script( $this->plugin_name, 'inqyraAutoEmbed', [
            'pendingFromRecrawl' => $pending_from_recrawl,
            'agentId'            => ( $agent ? $agent->id : 0 ),
        ] );
        // Setup Wizard page assets
        if ( $page === 'inqyra-wizard' ) {
            wp_enqueue_media();
            // Select2 for page selector
            wp_enqueue_style(
                'inqyra-select2',
                INQYRA_URL . 'admin/vendor/select2/select2.min.css',
                [],
                '4.1.0'
            );
            wp_enqueue_script(
                'inqyra-select2',
                INQYRA_URL . 'admin/vendor/select2/select2.min.js',
                ['jquery'],
                '4.1.0',
                true
            );
            // Wizard CSS
            wp_enqueue_style(
                'inqyra-wizard',
                INQYRA_URL . 'admin/css/inqyra-wizard.css',
                [$this->plugin_name],
                $this->version
            );
            // Wizard JS
            wp_enqueue_script(
                'inqyra-wizard',
                INQYRA_URL . 'admin/js/inqyra-wizard.js',
                ['jquery'],
                $this->version,
                true
            );
            // Get provider models
            $models = [];
            foreach ( [
                'claude',
                'openai',
                'gemini',
                'mistral',
                'deepseek'
            ] as $provider_type ) {
                try {
                    $provider = Inqyra_AI_Provider_Factory::create( $provider_type );
                    $models[$provider_type] = $provider->get_models();
                } catch ( Exception $e ) {
                    $models[$provider_type] = [];
                }
            }
            // Handle restart parameter — clear wizard state but seed with existing agent data.
            $user_id = get_current_user_id();
            // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only flag for wizard restart.
            if ( isset( $_GET['restart'] ) && sanitize_text_field( wp_unslash( $_GET['restart'] ) ) === '1' ) {
                delete_option( 'inqyra_wizard_completed' );
                global $wpdb;
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Real-time check needed.
                $agent = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE active = 1 ORDER BY updated_at DESC LIMIT 1', $wpdb->prefix . 'inqyra_agents' ), ARRAY_A );
                if ( $agent ) {
                    $provider = $agent['provider'] ?? 'claude';
                    $has_api_key = !empty( Inqyra_AI_Provider_Factory::get_api_key( $provider ) );
                    // Count crawled chunks and posts for this agent
                    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix.
                    $crawl_chunks = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i WHERE agent_id = %d', $wpdb->prefix . 'inqyra_knowledge_chunks', $agent['id'] ) );
                    // Count embeddings
                    $embeddings_total = $crawl_chunks;
                    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix.
                    $embeddings_processed = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i WHERE agent_id = %d AND embedding IS NOT NULL', $wpdb->prefix . 'inqyra_knowledge_chunks', $agent['id'] ) );
                    $seeded_state = [
                        'agentId'             => (int) $agent['id'],
                        'apiKeyValid'         => $has_api_key,
                        'selectedProvider'    => $provider,
                        'selectedModel'       => $agent['model'] ?? '',
                        'crawlComplete'       => $crawl_chunks > 0,
                        'crawlChunks'         => $crawl_chunks,
                        'embeddingsComplete'  => $embeddings_processed > 0 && $embeddings_processed >= $embeddings_total,
                        'embeddingsProcessed' => $embeddings_processed,
                        'embeddingsTotal'     => $embeddings_total,
                        'skipEmbeddings'      => $embeddings_processed > 0 && $embeddings_processed < $embeddings_total,
                    ];
                    set_transient( 'inqyra_wizard_state_' . $user_id, wp_json_encode( $seeded_state ), DAY_IN_SECONDS );
                } else {
                    delete_transient( 'inqyra_wizard_state_' . $user_id );
                }
            }
            // Get saved wizard state
            $wizard_state = get_transient( 'inqyra_wizard_state_' . $user_id );
            // Localize wizard script
            wp_localize_script( 'inqyra-wizard', 'inqyraWizard', [
                'ajaxUrl'            => admin_url( 'admin-ajax.php' ),
                'nonce'              => wp_create_nonce( 'inqyra_admin' ),
                'restUrl'            => rest_url( 'inqyra/v1' ),
                'restNonce'          => wp_create_nonce( 'wp_rest' ),
                'savedState'         => ( $wizard_state ? $wizard_state : '' ),
                'siteName'           => get_bloginfo( 'name' ),
                'autoLanguage'       => $this->detect_language(),
                'models'             => $models,
                'dashboardUrl'       => admin_url( 'admin.php?page=inqyra' ),
                'widgetPlaceholders' => Inqyra_Public::get_placeholder_translations(),
                'defaultPrompts'     => Inqyra_Default_Prompts::get_all(),
                'indexingLabels'     => Inqyra_Indexing_Labels::get_all(),
                'isFree'             => true,
                'isPremium'          => false,
                'strings'            => [
                    'error'                 => __( 'An error occurred. Please try again.', 'inqyra' ),
                    'saving'                => __( 'Saving...', 'inqyra' ),
                    'saved'                 => __( 'Saved!', 'inqyra' ),
                    'crawling'              => __( 'Crawling your content...', 'inqyra' ),
                    'crawl_error'           => __( 'Error: ', 'inqyra' ),
                    'crawl_timeout'         => __( 'Crawl timed out. Please try again.', 'inqyra' ),
                    'chunks_created'        => __( 'Chunks Created', 'inqyra' ),
                    'validate_api_key'      => __( 'Please validate your API key first.', 'inqyra' ),
                    'enter_name'            => __( 'Please enter a chatbot name.', 'inqyra' ),
                    'run_crawl'             => __( 'Please run the crawl first.', 'inqyra' ),
                    'save_error'            => __( 'Failed to save. Please try again.', 'inqyra' ),
                    'selectIcon'            => __( 'Select Header Icon', 'inqyra' ),
                    'useIcon'               => __( 'Use this icon', 'inqyra' ),
                    'search_pages'          => __( 'Search pages...', 'inqyra' ),
                    'license_active'        => __( 'Active', 'inqyra' ),
                    'license_free'          => __( 'Free tier — limitations apply', 'inqyra' ),
                    'chunks'                => __( 'chunks', 'inqyra' ),
                    'vectors'               => __( 'vectors', 'inqyra' ),
                    'embeddings_background' => __( 'Processing in background', 'inqyra' ),
                    'embeddings_ready'      => __( 'Ready to process embeddings.', 'inqyra' ),
                    'loading_module'        => __( 'Loading embeddings module...', 'inqyra' ),
                    'module_error'          => __( 'Error: Embeddings module failed to load.', 'inqyra' ),
                    'already_processing'    => __( 'Already processing...', 'inqyra' ),
                    'starting'              => __( 'Starting...', 'inqyra' ),
                    'processed'             => __( 'Processed', 'inqyra' ),
                    'failed'                => __( 'Failed', 'inqyra' ),
                    'uploading'             => __( 'Uploading...', 'inqyra' ),
                    'uploaded'              => __( 'Uploaded!', 'inqyra' ),
                    'upload'                => __( 'Upload', 'inqyra' ),
                    'preview_user_msg'      => __( 'Hello!', 'inqyra' ),
                    'preview_placeholder'   => __( 'Type a message...', 'inqyra' ),
                    'pb_error'              => __( 'Failed to get a response. Please try again.', 'inqyra' ),
                    'pb_generating'         => __( 'Generating your prompt...', 'inqyra' ),
                    'pb_generated'          => __( 'Prompt generated and placed in the textarea below. Not satisfied? Describe what to change and click "Generate Prompt" again.', 'inqyra' ),
                    'pb_type_answer'        => __( 'Type your answer...', 'inqyra' ),
                ],
            ] );
            // Load embeddings module in footer for step 4b
            add_action( 'admin_footer', [$this, 'load_embeddings_module'] );
        }
    }

    /**
     * Load the Transformers.js embeddings module.
     */
    public function load_embeddings_module() {
        $transformers_url = ( defined( 'INQYRA_TRANSFORMERS_URL' ) ? INQYRA_TRANSFORMERS_URL : INQYRA_URL . 'assets/js/vendor/transformers.min.js' );
        wp_enqueue_script(
            'inqyra-embeddings-module',
            INQYRA_URL . 'admin/js/inqyra-embeddings-module.js',
            [],
            $this->version,
            true
        );
        // Pass the Transformers.js URL to the module (bundled locally to avoid remote CDN dependency).
        wp_localize_script( 'inqyra-embeddings-module', 'inqyraEmbeddingsConfig', [
            'transformersUrl' => $transformers_url,
        ] );
        add_filter(
            'script_loader_tag',
            static function ( $tag, $handle ) {
                if ( 'inqyra-embeddings-module' === $handle ) {
                    $tag = str_replace( ' src=', ' type="module" src=', $tag );
                }
                return $tag;
            },
            10,
            2
        );
    }

    /**
     * Enqueue auto-index scripts on post editor pages (Gutenberg).
     *
     * @param string $hook The current admin page.
     */
    public function enqueue_post_editor_scripts( $hook ) {
        if ( $hook !== 'post.php' ) {
            return;
        }
        // Only trigger after a save (WordPress adds ?message= after save).
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Detecting WP's post-save redirect signal.
        if ( empty( $_GET['message'] ) ) {
            return;
        }
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading post ID from WP admin URL.
        $post_id = ( isset( $_GET['post'] ) ? absint( wp_unslash( $_GET['post'] ) ) : 0 );
        if ( !$post_id ) {
            return;
        }
        $post = get_post( $post_id );
        if ( !$post || $post->post_status !== 'publish' ) {
            return;
        }
        // Check if there is an active agent
        global $wpdb;
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table query.
        $agent = $wpdb->get_row( $wpdb->prepare( 'SELECT id, crawl_post_types FROM %i WHERE active = 1 ORDER BY updated_at DESC LIMIT 1', $wpdb->prefix . 'inqyra_agents' ) );
        if ( !$agent ) {
            return;
        }
        // Check post type
        $crawl_post_types = ( json_decode( $agent->crawl_post_types ?? '[]', true ) ?: ['page', 'post'] );
        if ( !in_array( $post->post_type, $crawl_post_types, true ) ) {
            return;
        }
        // Load the auto-index script
        wp_enqueue_script(
            'inqyra-post-autoindex',
            INQYRA_URL . 'admin/js/inqyra-post-autoindex.js',
            ['jquery'],
            $this->version,
            true
        );
        wp_localize_script( 'inqyra-post-autoindex', 'inqyraPostAutoIndex', [
            'ajaxUrl' => admin_url( 'admin-ajax.php' ),
            'nonce'   => wp_create_nonce( 'inqyra_admin' ),
            'postId'  => $post_id,
        ] );
        // Load the embeddings module
        add_action( 'admin_footer', [$this, 'load_embeddings_module'] );
    }

    /**
     * Add settings link to plugins page.
     *
     * @param array $links Existing links.
     * @return array Modified links.
     */
    public function add_settings_link( $links ) {
        $settings_link = sprintf( '<a href="%s">%s</a>', admin_url( 'admin.php?page=inqyra-settings' ), __( 'Settings', 'inqyra' ) );
        array_unshift( $links, $settings_link );
        if ( !inqyra_fs()->is__premium_only() || !inqyra_fs()->is_paying() ) {
            $links[] = sprintf( '<a href="%s" style="color: #10b981; font-weight: bold;" target="_blank">%s</a>', 'https://inqyra.com/pricing', __( 'Upgrade to Premium', 'inqyra' ) );
        }
        return $links;
    }

    /**
     * Display admin notices.
     */
    public function admin_notices() {
        // Only show on our pages
        $screen = get_current_screen();
        if ( !$screen || strpos( $screen->id, 'inqyra' ) === false ) {
            return;
        }
        // Suppress notices on wizard page
        if ( $screen->id === 'admin_page_inqyra-wizard' ) {
            return;
        }
        // Check if API keys are configured
        $settings = get_option( 'inqyra_settings', [] );
        $api_keys = $settings['api_keys'] ?? [];
        $has_api_key = !empty( $api_keys['claude'] ) || !empty( $api_keys['openai'] ) || !empty( $api_keys['gemini'] ) || !empty( $api_keys['mistral'] ) || !empty( $api_keys['deepseek'] );
        if ( !$has_api_key ) {
            printf(
                '<div class="notice notice-warning"><p><strong>Inqyra:</strong> %s <a href="%s">%s</a></p></div>',
                esc_html__( 'No AI API key configured. The chatbot will not work without an API key.', 'inqyra' ),
                esc_url( admin_url( 'admin.php?page=inqyra-settings' ) ),
                esc_html__( 'Configure API Keys', 'inqyra' )
            );
        }
    }

    /**
     * Render dashboard page.
     */
    public function render_dashboard_page() {
        if ( function_exists( 'inqyra_is_license_expired' ) && inqyra_is_license_expired() ) {
            $this->render_license_expired_page();
            return;
        }
        require_once INQYRA_PATH . 'admin/partials/dashboard.php';
    }

    /**
     * Render configuration page.
     */
    public function render_config_page() {
        if ( function_exists( 'inqyra_is_license_expired' ) && inqyra_is_license_expired() ) {
            $this->render_license_expired_page();
            return;
        }
        require_once INQYRA_PATH . 'admin/partials/config.php';
    }

    /**
     * Render knowledge base page.
     */
    public function render_knowledge_page() {
        if ( function_exists( 'inqyra_is_license_expired' ) && inqyra_is_license_expired() ) {
            $this->render_license_expired_page();
            return;
        }
        require_once INQYRA_PATH . 'admin/partials/knowledge.php';
    }

    /**
     * Render documents page.
     */
    public function render_documents_page() {
        $this->render_upgrade_page();
        return;
        if ( function_exists( 'inqyra_is_license_expired' ) && inqyra_is_license_expired() ) {
            $this->render_license_expired_page();
            return;
        }
        require_once INQYRA_PATH . 'premium/admin/partials/documents.php';
    }

    /**
     * Render synonyms page.
     */
    public function render_synonyms_page() {
        $this->render_upgrade_page();
        return;
        if ( function_exists( 'inqyra_is_license_expired' ) && inqyra_is_license_expired() ) {
            $this->render_license_expired_page();
            return;
        }
        require_once INQYRA_PATH . 'premium/admin/partials/synonyms.php';
    }

    /**
     * Render webhooks page.
     */
    public function render_webhooks_page() {
        $this->render_upgrade_page();
        return;
        if ( function_exists( 'inqyra_is_license_expired' ) && inqyra_is_license_expired() ) {
            $this->render_license_expired_page();
            return;
        }
        require_once INQYRA_PATH . 'premium/admin/partials/webhooks.php';
    }

    /**
     * Render conversations page.
     */
    public function render_conversations_page() {
        if ( function_exists( 'inqyra_is_license_expired' ) && inqyra_is_license_expired() ) {
            $this->render_license_expired_page();
            return;
        }
        require_once INQYRA_PATH . 'admin/partials/conversations.php';
    }

    /**
     * Render statistics page.
     */
    public function render_stats_page() {
        $this->render_upgrade_page();
        return;
        if ( function_exists( 'inqyra_is_license_expired' ) && inqyra_is_license_expired() ) {
            $this->render_license_expired_page();
            return;
        }
        require_once INQYRA_PATH . 'premium/admin/partials/statistics.php';
    }

    /**
     * Render page targeting page.
     */
    public function render_page_targeting_page() {
        $this->render_upgrade_page();
        return;
        if ( function_exists( 'inqyra_is_license_expired' ) && inqyra_is_license_expired() ) {
            $this->render_license_expired_page();
            return;
        }
        require_once INQYRA_PATH . 'premium/admin/partials/page-targeting.php';
    }

    /**
     * Render settings page.
     */
    public function render_settings_page() {
        if ( function_exists( 'inqyra_is_license_expired' ) && inqyra_is_license_expired() ) {
            $this->render_license_expired_page();
            return;
        }
        require_once INQYRA_PATH . 'admin/partials/settings.php';
    }

    /**
     * Render setup wizard page.
     */
    public function render_wizard_page() {
        if ( function_exists( 'inqyra_is_license_expired' ) && inqyra_is_license_expired() ) {
            $this->render_license_expired_page();
            return;
        }
        // If wizard was already completed (e.g. Freemius first-path on a
        // returning connection), redirect to the dashboard instead.
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only flag check.
        if ( get_option( 'inqyra_wizard_completed' ) && !isset( $_GET['restart'] ) ) {
            wp_safe_redirect( admin_url( 'admin.php?page=inqyra' ) );
            exit;
        }
        require_once INQYRA_PATH . 'admin/partials/wizard.php';
    }

    /**
     * Render the upgrade prompt page for free users.
     */
    public function render_upgrade_page() {
        $upgrade_url = inqyra_fs()->get_upgrade_url();
        ?>
        <div class="wrap">
            <h1><?php 
        esc_html_e( 'Premium Feature', 'inqyra' );
        ?></h1>
            <div class="inqyra-upgrade-notice" style="max-width: 600px; margin-top: 20px; padding: 20px; background: #fff; border: 1px solid #c3c4c7; border-left: 4px solid #2271b1;">
                <h2 style="margin-top: 0;"><?php 
        esc_html_e( 'Upgrade to Inqyra Premium', 'inqyra' );
        ?></h2>
                <p><?php 
        esc_html_e( 'This feature is available in Inqyra Premium. Upgrade to unlock:', 'inqyra' );
        ?></p>
                <ul style="list-style: disc; margin-left: 20px;">
                    <li><?php 
        esc_html_e( 'Index posts, products & custom post types', 'inqyra' );
        ?></li>
                    <li><?php 
        esc_html_e( 'Document upload & processing (PDF, DOCX)', 'inqyra' );
        ?></li>
                    <li><?php 
        esc_html_e( 'Synonym management for better search', 'inqyra' );
        ?></li>
                    <li><?php 
        esc_html_e( 'Webhook integrations (Zapier, Make, CRM)', 'inqyra' );
        ?></li>
                    <li><?php 
        esc_html_e( 'Advanced statistics & analytics export', 'inqyra' );
        ?></li>
                    <li><?php 
        esc_html_e( 'Lead extraction (email, phone, company)', 'inqyra' );
        ?></li>
                    <li><?php 
        esc_html_e( 'Page targeting (show widget on specific pages)', 'inqyra' );
        ?></li>
                    <li><?php 
        esc_html_e( 'Extended conversation history & analytics', 'inqyra' );
        ?></li>
                </ul>
                <p>
                    <a href="<?php 
        echo esc_url( $upgrade_url );
        ?>" class="button button-primary button-hero">
                        <?php 
        esc_html_e( 'Upgrade to Premium', 'inqyra' );
        ?>
                    </a>
                </p>
            </div>
        </div>
        <?php 
    }

    /**
     * Render the license expired page.
     * Shown on all admin pages when the premium license has expired.
     */
    private function render_license_expired_page() {
        $account_url = inqyra_fs()->get_account_url();
        ?>
        <div class="wrap">
            <h1><?php 
        esc_html_e( 'License Expired', 'inqyra' );
        ?></h1>
            <div class="inqyra-upgrade-notice" style="max-width: 700px; margin-top: 20px; padding: 30px; background: #fff; border: 1px solid #c3c4c7; border-left: 4px solid #d63638;">
                <h2 style="margin-top: 0; color: #d63638;">
                    <?php 
        esc_html_e( 'Your Inqyra Premium license has expired', 'inqyra' );
        ?>
                </h2>
                <p>
                    <?php 
        esc_html_e( 'Your premium license is no longer active. All plugin functionality has been disabled, including the chat widget on your website.', 'inqyra' );
        ?>
                </p>
                <h3><?php 
        esc_html_e( 'How to restore your chatbot:', 'inqyra' );
        ?></h3>
                <ol style="margin-left: 20px;">
                    <li>
                        <strong><?php 
        esc_html_e( 'Renew your license', 'inqyra' );
        ?></strong> &mdash;
                        <?php 
        esc_html_e( 'Visit your account to renew or upgrade your subscription.', 'inqyra' );
        ?>
                    </li>
                    <li>
                        <strong><?php 
        esc_html_e( 'Or use the free tier', 'inqyra' );
        ?></strong> &mdash;
                        <?php 
        esc_html_e( 'Deactivate and delete this plugin, then install the free version from WordPress.org. Free version indexes pages only — no document upload or advanced features.', 'inqyra' );
        ?>
                    </li>
                </ol>
                <p style="margin-top: 20px;">
                    <a href="<?php 
        echo esc_url( $account_url );
        ?>" class="button button-primary button-hero">
                        <?php 
        esc_html_e( 'Renew License', 'inqyra' );
        ?>
                    </a>
                </p>
            </div>
        </div>
        <?php 
    }

    /**
     * Check if license is expired and bail from AJAX with an error.
     *
     * @return bool True if the request was blocked (caller should return).
     */
    private function check_license_expired_ajax() : bool {
        if ( function_exists( 'inqyra_is_license_expired' ) && inqyra_is_license_expired() ) {
            wp_send_json_error( [
                'message' => __( 'Your Inqyra Premium license has expired. Please renew to continue using the plugin.', 'inqyra' ),
            ] );
            return true;
        }
        return false;
    }

    /**
     * Redirect to wizard if needed (fresh install or activation).
     */
    public function maybe_redirect_to_wizard() {
        // Skip if doing AJAX
        if ( wp_doing_ajax() ) {
            return;
        }
        // Don't redirect to wizard if license expired
        if ( function_exists( 'inqyra_is_license_expired' ) && inqyra_is_license_expired() ) {
            return;
        }
        // Handle close_wizard — restore completed flag so wizard doesn't redirect again.
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Nonce is verified inline via wp_verify_nonce().
        if ( isset( $_GET['close_wizard'] ) && sanitize_text_field( wp_unslash( $_GET['close_wizard'] ) ) === '1' && wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ?? '' ) ), 'inqyra_close_wizard' ) ) {
            update_option( 'inqyra_wizard_completed', true );
        }
        // Skip if wizard already completed
        if ( get_option( 'inqyra_wizard_completed' ) ) {
            return;
        }
        // Immediate redirect after plugin activation — fires once regardless
        // of which admin page the user is on or Freemius registration state.
        if ( get_transient( 'inqyra_activation_redirect' ) ) {
            delete_transient( 'inqyra_activation_redirect' );
            wp_safe_redirect( admin_url( 'admin.php?page=inqyra-wizard' ) );
            exit;
        }
        // Let Freemius show its connect/opt-in page first so the user can
        // activate a license (or choose free) before the wizard runs.
        // After opt-in, Freemius redirects to the wizard via 'first-path'.
        if ( function_exists( 'inqyra_fs' ) && !inqyra_fs()->is_registered() && !inqyra_fs()->is_anonymous() ) {
            return;
        }
        // Only redirect when user navigates to an inqyra page.
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Admin page routing only.
        $page = ( isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : '' );
        if ( strpos( $page, 'inqyra' ) === false ) {
            return;
        }
        // Skip if already on wizard page
        if ( $page === 'inqyra-wizard' ) {
            return;
        }
        // Check if there's a reason to redirect: no agents or no API key
        global $wpdb;
        // Safety: skip if agents table doesn't exist yet
        $table_name = $wpdb->prefix . 'inqyra_agents';
        $table_like = $wpdb->esc_like( $table_name );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Checking if plugin table exists.
        if ( $wpdb->get_var( $wpdb->prepare( "SHOW TABLES LIKE %s", $table_like ) ) !== $table_name ) {
            return;
        }
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table query.
        $agent_count = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i', $wpdb->prefix . 'inqyra_agents' ) );
        $settings = get_option( 'inqyra_settings', [] );
        $api_keys = $settings['api_keys'] ?? [];
        $has_api_key = !empty( $api_keys['claude'] ) || !empty( $api_keys['openai'] ) || !empty( $api_keys['gemini'] ) || !empty( $api_keys['mistral'] ) || !empty( $api_keys['deepseek'] );
        if ( $agent_count === 0 || !$has_api_key ) {
            wp_safe_redirect( admin_url( 'admin.php?page=inqyra-wizard' ) );
            exit;
        }
    }

    /**
     * AJAX handler for saving settings.
     */
    public function ajax_save_settings() {
        $this->check_license_expired_ajax();
        check_ajax_referer( 'inqyra_admin', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( [
                'message' => __( 'Permission denied.', 'inqyra' ),
            ] );
        }
        $settings = get_option( 'inqyra_settings', [] );
        // Update API keys (with encryption)
        if ( isset( $_POST['api_keys'] ) ) {
            $existing_keys = ( isset( $settings['api_keys'] ) ? $settings['api_keys'] : [] );
            $api_keys = [];
            foreach ( [
                'claude',
                'openai',
                'gemini',
                'mistral',
                'deepseek'
            ] as $provider ) {
                if ( isset( $_POST['api_keys'][$provider] ) ) {
                    $new_key = sanitize_text_field( wp_unslash( $_POST['api_keys'][$provider] ) );
                    // Skip if empty or if it's a masked placeholder (contains asterisks)
                    if ( empty( $new_key ) || strpos( $new_key, '****' ) !== false ) {
                        // Keep existing encrypted key
                        if ( !empty( $existing_keys[$provider] ) ) {
                            $api_keys[$provider] = $existing_keys[$provider];
                        }
                    } else {
                        // New key provided - encrypt it
                        $encrypted = Inqyra_Encryption::encrypt( $new_key );
                        if ( $encrypted !== false ) {
                            $api_keys[$provider] = $encrypted;
                        }
                        // Clear cached models when API key changes
                        delete_transient( 'inqyra_api_models_' . $provider );
                    }
                } elseif ( !empty( $existing_keys[$provider] ) ) {
                    // Keep existing key if nothing posted
                    $api_keys[$provider] = $existing_keys[$provider];
                }
            }
            $settings['api_keys'] = $api_keys;
        }
        // Update other settings
        if ( isset( $_POST['embedding_batch_size'] ) ) {
            $settings['embedding_batch_size'] = absint( wp_unslash( $_POST['embedding_batch_size'] ) );
        }
        if ( isset( $_POST['debug_mode'] ) ) {
            $settings['debug_mode'] = (bool) sanitize_text_field( wp_unslash( $_POST['debug_mode'] ) );
        }
        update_option( 'inqyra_settings', $settings );
        wp_send_json_success( [
            'message' => __( 'Settings saved.', 'inqyra' ),
        ] );
    }

    /**
     * AJAX handler for saving agent configuration.
     */
    public function ajax_save_agent() {
        $this->check_license_expired_ajax();
        check_ajax_referer( 'inqyra_admin', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( [
                'message' => __( 'Permission denied.', 'inqyra' ),
            ] );
        }
        global $wpdb;
        $table = $wpdb->prefix . 'inqyra_agents';
        $agent_id = ( isset( $_POST['agent_id'] ) ? absint( wp_unslash( $_POST['agent_id'] ) ) : 0 );
        // Prepare data — only include fields explicitly present in POST.
        // This allows partial updates (e.g. wizard crawl step) without
        // resetting fields that weren't submitted.
        $data = [];
        // Text fields: only include if present in POST
        $text_fields = [
            'name'                   => '',
            'language'               => 'en',
            'provider'               => 'claude',
            'model'                  => '',
            'widget_position'        => 'bottom-right',
            'button_text'            => '',
            'header_title'           => 'Chat',
            'header_subtitle'        => '',
            'powered_by_text'        => '',
            'send_button_text'       => '',
            'input_placeholder'      => '',
            'document_search_mode'   => 'linked',
            'query_rewrite_provider' => 'claude',
            'query_rewrite_model'    => 'claude-3-haiku-20240307',
            'widget_display_mode'    => 'all',
        ];
        foreach ( $text_fields as $field => $default ) {
            if ( isset( $_POST[$field] ) ) {
                $data[$field] = sanitize_text_field( wp_unslash( $_POST[$field] ) );
            } elseif ( $agent_id === 0 ) {
                $data[$field] = $default;
            }
        }
        // powered_by_text is customizable by all users.
        // Textarea fields
        $textarea_fields = ['system_prompt', 'greeting_message'];
        foreach ( $textarea_fields as $field ) {
            if ( isset( $_POST[$field] ) ) {
                $data[$field] = sanitize_textarea_field( wp_unslash( $_POST[$field] ) );
            } elseif ( $agent_id === 0 ) {
                $data[$field] = '';
            }
        }
        // Integer fields
        $int_fields = [
            'max_tokens'           => 4096,
            'daily_token_limit'    => 1000000,
            'hourly_request_limit' => 500,
            'header_icon_id'       => 0,
            'send_button_icon_id'  => 0,
        ];
        foreach ( $int_fields as $field => $default ) {
            if ( isset( $_POST[$field] ) ) {
                $data[$field] = absint( wp_unslash( $_POST[$field] ) );
            } elseif ( $agent_id === 0 ) {
                $data[$field] = $default;
            }
        }
        // Float fields
        $float_fields = [
            'cost_limit_usd'        => 10.0,
            'document_score_weight' => 0.7,
        ];
        foreach ( $float_fields as $field => $default ) {
            if ( isset( $_POST[$field] ) ) {
                $data[$field] = floatval( sanitize_text_field( wp_unslash( $_POST[$field] ) ) );
            } elseif ( $agent_id === 0 ) {
                $data[$field] = $default;
            }
        }
        // Color fields
        $color_fields = [
            'primary_color' => '#0d6efd',
            'text_color'    => '#ffffff',
        ];
        foreach ( $color_fields as $field => $default ) {
            if ( isset( $_POST[$field] ) ) {
                $data[$field] = sanitize_hex_color( wp_unslash( $_POST[$field] ) );
            } elseif ( $agent_id === 0 ) {
                $data[$field] = $default;
            }
        }
        // Boolean/checkbox fields: use array_key_exists because JS explicitly
        // sends 0 for unchecked. If the field is absent entirely (partial save),
        // don't touch it.
        $bool_fields = [
            'show_icon_in_messages',
            'query_rewrite_enabled',
            'auto_recrawl_enabled',
            'widget_enabled',
            'widget_mobile_enabled',
            'conversation_persist',
            'hide_input_placeholder',
        ];
        foreach ( $bool_fields as $field ) {
            if ( array_key_exists( $field, $_POST ) ) {
                // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified at method start.
                $data[$field] = ( !empty( sanitize_text_field( wp_unslash( $_POST[$field] ) ) ) ? 1 : 0 );
            } elseif ( $agent_id === 0 ) {
                $data[$field] = 0;
            }
        }
        // JSON array fields
        if ( isset( $_POST['crawl_parts'] ) ) {
            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Validated via validate_crawl_parts() allowlist.
            $data['crawl_parts'] = wp_json_encode( $this->validate_crawl_parts( array_map( 'sanitize_text_field', wp_unslash( (array) $_POST['crawl_parts'] ) ) ) );
        } elseif ( $agent_id === 0 ) {
            $data['crawl_parts'] = '["p","h1","h2","h3","h4","h5","h6","li","td","th","blockquote","figcaption","div","span","article","section"]';
        }
        if ( isset( $_POST['crawl_post_types'] ) ) {
            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Validated via validate_post_types() allowlist.
            $data['crawl_post_types'] = wp_json_encode( $this->validate_post_types( array_map( 'sanitize_text_field', wp_unslash( (array) $_POST['crawl_post_types'] ) ) ) );
        } elseif ( $agent_id === 0 ) {
            $data['crawl_post_types'] = '["post","page"]';
        }
        if ( isset( $_POST['widget_pages'] ) ) {
            $data['widget_pages'] = wp_json_encode( array_map( 'absint', wp_unslash( (array) $_POST['widget_pages'] ) ) );
        } elseif ( $agent_id === 0 ) {
            $data['widget_pages'] = '[]';
        }
        // Active flag
        if ( isset( $_POST['active'] ) ) {
            $data['active'] = absint( wp_unslash( $_POST['active'] ) );
        } elseif ( $agent_id === 0 ) {
            $data['active'] = ( array_key_exists( 'widget_enabled', $_POST ) && !empty( sanitize_text_field( wp_unslash( $_POST['widget_enabled'] ) ) ) ? 1 : 0 );
        }
        $data['updated_at'] = current_time( 'mysql' );
        if ( $agent_id > 0 ) {
            // Update existing
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix.
            $result = $wpdb->update( $table, $data, [
                'id' => $agent_id,
            ] );
        } else {
            // Insert new
            $data['created_at'] = current_time( 'mysql' );
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix.
            $result = $wpdb->insert( $table, $data );
            $agent_id = $wpdb->insert_id;
        }
        if ( $result === false ) {
            wp_send_json_error( [
                'message' => __( 'Failed to save agent.', 'inqyra' ) . ' ' . esc_html( $wpdb->last_error ),
            ] );
        }
        // Clear agent config cache
        if ( class_exists( 'Inqyra_Chat_Service' ) ) {
            Inqyra_Chat_Service::clear_agent_cache( $agent_id );
        }
        wp_send_json_success( [
            'message'  => __( 'Agent saved.', 'inqyra' ),
            'agent_id' => $agent_id,
        ] );
    }

    /**
     * AJAX handler for searching pages (Select2).
     */
    public function ajax_search_pages() {
        // Select2 sends GET requests, but nonce can be in either.
        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce is verified via wp_verify_nonce() below.
        $nonce = ( isset( $_GET['nonce'] ) ? sanitize_text_field( wp_unslash( $_GET['nonce'] ) ) : (( isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '' )) );
        if ( !wp_verify_nonce( $nonce, 'inqyra_admin' ) ) {
            wp_send_json_error( [
                'message' => __( 'Security check failed.', 'inqyra' ),
            ] );
        }
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( [
                'message' => __( 'Permission denied.', 'inqyra' ),
            ] );
        }
        $search = ( isset( $_GET['search'] ) ? sanitize_text_field( wp_unslash( $_GET['search'] ) ) : '' );
        $page = ( isset( $_GET['page'] ) ? absint( wp_unslash( $_GET['page'] ) ) : 1 );
        $per_page = 20;
        // Get public post types including custom ones
        $post_types = get_post_types( [
            'public' => true,
        ], 'names' );
        unset($post_types['attachment']);
        $args = [
            'post_type'      => array_values( $post_types ),
            'post_status'    => 'publish',
            'posts_per_page' => $per_page,
            'paged'          => $page,
            'orderby'        => 'title',
            'order'          => 'ASC',
        ];
        if ( !empty( $search ) ) {
            $args['s'] = $search;
        }
        $query = new WP_Query($args);
        $results = [];
        foreach ( $query->posts as $post ) {
            $post_type_obj = get_post_type_object( $post->post_type );
            $results[] = [
                'id'   => $post->ID,
                'text' => $post->post_title . ' (' . (( $post_type_obj ? $post_type_obj->labels->singular_name : $post->post_type )) . ')',
            ];
        }
        wp_send_json_success( [
            'results' => $results,
            'more'    => $query->max_num_pages > $page,
        ] );
    }

    /**
     * AJAX handler for getting page titles by IDs (for Select2 pre-population).
     */
    public function ajax_get_page_titles() {
        check_ajax_referer( 'inqyra_admin', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( [
                'message' => __( 'Permission denied.', 'inqyra' ),
            ] );
        }
        $page_ids = ( isset( $_POST['page_ids'] ) ? array_map( 'absint', wp_unslash( (array) $_POST['page_ids'] ) ) : [] );
        if ( empty( $page_ids ) ) {
            wp_send_json_success( [] );
        }
        $results = [];
        foreach ( $page_ids as $id ) {
            $post = get_post( $id );
            if ( $post && $post->post_status === 'publish' ) {
                $post_type_obj = get_post_type_object( $post->post_type );
                $results[$id] = $post->post_title . ' (' . (( $post_type_obj ? $post_type_obj->labels->singular_name : $post->post_type )) . ')';
            }
        }
        wp_send_json_success( $results );
    }

    /**
     * AJAX handler for dismissing the upgrade recrawl notice.
     */
    public function ajax_dismiss_recrawl_notice() {
        check_ajax_referer( 'inqyra_admin', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( [
                'message' => __( 'Permission denied.', 'inqyra' ),
            ] );
        }
        delete_option( 'inqyra_upgrade_recrawl_needed' );
        wp_send_json_success();
    }

    /**
     * AJAX handler for triggering a crawl.
     */
    public function ajax_trigger_crawl() {
        $this->check_license_expired_ajax();
        check_ajax_referer( 'inqyra_admin', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( [
                'message' => __( 'Permission denied.', 'inqyra' ),
            ] );
        }
        $agent_id = ( isset( $_POST['agent_id'] ) ? absint( wp_unslash( $_POST['agent_id'] ) ) : 0 );
        if ( !$agent_id ) {
            wp_send_json_error( [
                'message' => __( 'Invalid agent ID.', 'inqyra' ),
            ] );
        }
        // Start a crawl run
        global $wpdb;
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix.
        $wpdb->insert( $wpdb->prefix . 'inqyra_crawl_runs', [
            'agent_id'   => $agent_id,
            'status'     => 'pending',
            'created_at' => current_time( 'mysql' ),
        ] );
        $run_id = $wpdb->insert_id;
        // Run the crawl directly (synchronously)
        // This ensures immediate execution without relying on WP-Cron
        $crawler = new Inqyra_Crawler();
        $crawl_result = $crawler->crawl_agent( $agent_id, $run_id );
        // Get the final status
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix.
        $run = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE id = %d', $wpdb->prefix . 'inqyra_crawl_runs', $run_id ) );
        // Clear upgrade recrawl notice after successful crawl
        if ( $run && $run->status === 'completed' ) {
            delete_option( 'inqyra_upgrade_recrawl_needed' );
        }
        wp_send_json_success( [
            'message'             => __( 'Crawl completed.', 'inqyra' ),
            'run_id'              => $run_id,
            'status'              => ( $run ? $run->status : 'unknown' ),
            'chunks_created'      => ( $run ? (int) $run->chunks_created : 0 ),
            'total_posts'         => ( $run ? (int) $run->total_posts : 0 ),
            'posts_processed'     => $crawl_result['posts_processed'] ?? 0,
            'documents_processed' => $crawl_result['documents_processed'] ?? 0,
            'debug_logs'          => $crawl_result['debug_logs'] ?? [],
        ] );
    }

    /**
     * AJAX handler for crawling a single post (triggered from post editor).
     */
    public function ajax_crawl_single_post() {
        check_ajax_referer( 'inqyra_admin', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( [
                'message' => __( 'Permission denied.', 'inqyra' ),
            ] );
        }
        $post_id = ( isset( $_POST['post_id'] ) ? absint( wp_unslash( $_POST['post_id'] ) ) : 0 );
        if ( !$post_id ) {
            wp_send_json_error( [
                'message' => __( 'Invalid post ID.', 'inqyra' ),
            ] );
        }
        $post = get_post( $post_id );
        if ( !$post || $post->post_status !== 'publish' ) {
            wp_send_json_success( [
                'chunks_created' => 0,
                'skipped'        => true,
            ] );
            return;
        }
        global $wpdb;
        // Get the active agent
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $agent = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE active = 1 ORDER BY updated_at DESC LIMIT 1', $wpdb->prefix . 'inqyra_agents' ) );
        if ( !$agent ) {
            wp_send_json_error( [
                'message' => __( 'No active agent found.', 'inqyra' ),
            ] );
        }
        // Check if the post type is in the agent's crawl_post_types
        $crawl_post_types = ( json_decode( $agent->crawl_post_types ?? '[]', true ) ?: ['page', 'post'] );
        if ( !in_array( $post->post_type, $crawl_post_types, true ) ) {
            wp_send_json_success( [
                'chunks_created' => 0,
                'skipped'        => true,
            ] );
            return;
        }
        $crawler = new Inqyra_Crawler();
        $result = $crawler->crawl_posts( $agent->id, [$post_id] );
        $chunks_created = $result['chunks_created'] ?? 0;
        wp_send_json_success( [
            'chunks_created'  => $chunks_created,
            'agent_id'        => $agent->id,
            'posts_processed' => $result['posts_processed'] ?? 0,
        ] );
    }

    /**
     * AJAX handler for getting crawl status.
     */
    public function ajax_get_crawl_status() {
        check_ajax_referer( 'inqyra_admin', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( [
                'message' => __( 'Permission denied.', 'inqyra' ),
            ] );
        }
        global $wpdb;
        $run_id = ( isset( $_POST['run_id'] ) ? absint( wp_unslash( $_POST['run_id'] ) ) : 0 );
        $agent_id = ( isset( $_POST['agent_id'] ) ? absint( wp_unslash( $_POST['agent_id'] ) ) : 0 );
        if ( $run_id ) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix.
            $run = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE id = %d', $wpdb->prefix . 'inqyra_crawl_runs', $run_id ) );
        } elseif ( $agent_id ) {
            // Get the latest crawl run for this agent
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix.
            $run = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i
                 WHERE agent_id = %d ORDER BY id DESC LIMIT 1', $wpdb->prefix . 'inqyra_crawl_runs', $agent_id ) );
        } else {
            wp_send_json_error( [
                'message' => __( 'Invalid run ID or agent ID.', 'inqyra' ),
            ] );
            return;
        }
        if ( !$run ) {
            wp_send_json_error( [
                'message' => __( 'Crawl run not found.', 'inqyra' ),
            ] );
            return;
        }
        wp_send_json_success( [
            'status'          => $run->status,
            'total_posts'     => (int) $run->total_posts,
            'processed_posts' => (int) $run->processed_posts,
            'chunks_created'  => (int) $run->chunks_created,
            'error_message'   => $run->error_message,
        ] );
    }

    /**
     * AJAX handler for uploading documents.
     */
    public function ajax_upload_document() {
        check_ajax_referer( 'inqyra_admin', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( [
                'message' => __( 'Permission denied.', 'inqyra' ),
            ] );
        }
        $agent_id = ( isset( $_POST['agent_id'] ) ? absint( wp_unslash( $_POST['agent_id'] ) ) : 0 );
        if ( !$agent_id ) {
            wp_send_json_error( [
                'message' => __( 'Invalid agent ID.', 'inqyra' ),
            ] );
        }
        if ( empty( $_FILES['document'] ) ) {
            wp_send_json_error( [
                'message' => __( 'No file uploaded.', 'inqyra' ),
            ] );
        }
        // Sanitize each file upload field individually before passing to the processor.
        $file = [
            'name'     => ( isset( $_FILES['document']['name'] ) ? sanitize_file_name( wp_unslash( $_FILES['document']['name'] ) ) : '' ),
            'type'     => ( isset( $_FILES['document']['type'] ) ? sanitize_mime_type( wp_unslash( $_FILES['document']['type'] ) ) : '' ),
            'tmp_name' => ( isset( $_FILES['document']['tmp_name'] ) ? sanitize_text_field( wp_unslash( $_FILES['document']['tmp_name'] ) ) : '' ),
            'size'     => ( isset( $_FILES['document']['size'] ) ? absint( wp_unslash( $_FILES['document']['size'] ) ) : 0 ),
            'error'    => ( isset( $_FILES['document']['error'] ) ? absint( wp_unslash( $_FILES['document']['error'] ) ) : 0 ),
        ];
        $processor = new Inqyra_Document_Processor();
        $result = $processor->upload( $agent_id, $file );
        if ( isset( $result['error'] ) ) {
            wp_send_json_error( [
                'message' => $result['error'],
            ] );
        }
        // Process the document immediately (extract text, create chunks)
        $process_result = $processor->process( $result['document_id'] );
        $chunks_created = $process_result['chunks_created'] ?? 0;
        wp_send_json_success( [
            'message'        => __( 'Document uploaded and processed.', 'inqyra' ),
            'document_id'    => $result['document_id'],
            'agent_id'       => $agent_id,
            'chunks_created' => $chunks_created,
        ] );
    }

    /**
     * AJAX handler for deleting documents.
     */
    public function ajax_delete_document() {
        check_ajax_referer( 'inqyra_admin', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( [
                'message' => __( 'Permission denied.', 'inqyra' ),
            ] );
        }
        $document_id = ( isset( $_POST['document_id'] ) ? absint( wp_unslash( $_POST['document_id'] ) ) : 0 );
        if ( !$document_id ) {
            wp_send_json_error( [
                'message' => __( 'Invalid document ID.', 'inqyra' ),
            ] );
        }
        $processor = new Inqyra_Document_Processor();
        $result = $processor->delete( $document_id );
        if ( $result ) {
            wp_send_json_success( [
                'message' => __( 'Document deleted.', 'inqyra' ),
            ] );
        } else {
            wp_send_json_error( [
                'message' => __( 'Failed to delete document.', 'inqyra' ),
            ] );
        }
    }

    /**
     * AJAX handler for getting document processing status.
     */
    public function ajax_get_document_status() {
        check_ajax_referer( 'inqyra_admin', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( [
                'message' => __( 'Permission denied.', 'inqyra' ),
            ] );
        }
        $document_id = ( isset( $_POST['document_id'] ) ? absint( wp_unslash( $_POST['document_id'] ) ) : 0 );
        if ( !$document_id ) {
            wp_send_json_error( [
                'message' => __( 'Invalid document ID.', 'inqyra' ),
            ] );
        }
        global $wpdb;
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix.
        $document = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE id = %d', $wpdb->prefix . 'inqyra_documents', $document_id ) );
        if ( !$document ) {
            wp_send_json_error( [
                'message' => __( 'Document not found.', 'inqyra' ),
            ] );
        }
        wp_send_json_success( [
            'status'        => $document->status,
            'chunk_count'   => $document->chunk_count,
            'error_message' => $document->error_message,
        ] );
    }

    /**
     * AJAX handler for getting embedding statistics.
     */
    public function ajax_get_embedding_stats() {
        check_ajax_referer( 'inqyra_admin', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( [
                'message' => __( 'Permission denied.', 'inqyra' ),
            ] );
        }
        $agent_id = ( isset( $_POST['agent_id'] ) && sanitize_text_field( wp_unslash( $_POST['agent_id'] ) ) !== '' ? absint( wp_unslash( $_POST['agent_id'] ) ) : null );
        if ( $agent_id === 0 ) {
            $agent_id = null;
        }
        $vector_store = new Inqyra_Vector_Store();
        $stats = $vector_store->get_statistics( $agent_id );
        $embedder = new Inqyra_Embedder();
        $stats['embedder_available'] = $embedder->is_available();
        $stats['embedder_model'] = $embedder->get_model();
        $stats['embedder_provider'] = $embedder->get_provider();
        $stats['vector_dim'] = $embedder->get_vector_dim();
        $stats['pending_count'] = $embedder->get_pending_count( $agent_id );
        wp_send_json_success( $stats );
    }

    /**
     * AJAX handler for getting chunks that need embeddings.
     * Used by browser-based Transformers.js embedding.
     */
    public function ajax_get_chunks_for_embedding() {
        check_ajax_referer( 'inqyra_admin', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( [
                'message' => __( 'Permission denied.', 'inqyra' ),
            ] );
        }
        $agent_id = ( isset( $_POST['agent_id'] ) ? absint( wp_unslash( $_POST['agent_id'] ) ) : null );
        $limit = ( isset( $_POST['limit'] ) ? absint( wp_unslash( $_POST['limit'] ) ) : 10 );
        $embedder = new Inqyra_Embedder();
        $chunks = $embedder->get_chunks_needing_embeddings( $limit, $agent_id );
        wp_send_json_success( [
            'chunks'     => $chunks,
            'model'      => $embedder->get_model(),
            'vector_dim' => $embedder->get_vector_dim(),
        ] );
    }

    /**
     * AJAX handler for saving an embedding from the browser.
     * Used by browser-based Transformers.js embedding.
     */
    public function ajax_save_embedding() {
        $this->check_license_expired_ajax();
        check_ajax_referer( 'inqyra_admin', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( [
                'message' => __( 'Permission denied.', 'inqyra' ),
            ] );
        }
        $chunk_id = ( isset( $_POST['chunk_id'] ) ? absint( wp_unslash( $_POST['chunk_id'] ) ) : 0 );
        $embedding = ( isset( $_POST['embedding'] ) ? array_map( 'floatval', wp_unslash( $_POST['embedding'] ) ) : null );
        if ( !$chunk_id ) {
            wp_send_json_error( [
                'message' => __( 'Invalid chunk ID.', 'inqyra' ),
            ] );
        }
        if ( !$embedding || !is_array( $embedding ) ) {
            wp_send_json_error( [
                'message' => __( 'Invalid embedding data.', 'inqyra' ),
            ] );
        }
        $embedder = new Inqyra_Embedder();
        $expected_dim = $embedder->get_vector_dim();
        if ( count( $embedding ) !== $expected_dim ) {
            wp_send_json_error( [
                'message' => sprintf( 
                    /* translators: %1$d: expected embedding dimension, %2$d: actual embedding dimension received */
                    __( 'Invalid embedding dimension. Expected %1$d, got %2$d.', 'inqyra' ),
                    $expected_dim,
                    count( $embedding )
                 ),
            ] );
        }
        $result = $embedder->save_embedding( $chunk_id, $embedding );
        if ( $result ) {
            wp_send_json_success( [
                'message' => __( 'Embedding saved.', 'inqyra' ),
            ] );
        } else {
            wp_send_json_error( [
                'message' => __( 'Failed to save embedding.', 'inqyra' ),
            ] );
        }
    }

    /**
     * AJAX handler for marking an embedding as failed.
     * Used by browser-based Transformers.js embedding.
     */
    public function ajax_mark_embedding_failed() {
        check_ajax_referer( 'inqyra_admin', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( [
                'message' => __( 'Permission denied.', 'inqyra' ),
            ] );
        }
        $chunk_id = ( isset( $_POST['chunk_id'] ) ? absint( wp_unslash( $_POST['chunk_id'] ) ) : 0 );
        if ( !$chunk_id ) {
            wp_send_json_error( [
                'message' => __( 'Invalid chunk ID.', 'inqyra' ),
            ] );
        }
        $embedder = new Inqyra_Embedder();
        $result = $embedder->mark_failed( $chunk_id );
        if ( $result ) {
            wp_send_json_success( [
                'message' => __( 'Chunk marked as failed.', 'inqyra' ),
            ] );
        } else {
            wp_send_json_error( [
                'message' => __( 'Failed to mark chunk.', 'inqyra' ),
            ] );
        }
    }

    /**
     * AJAX handler for adding/updating a synonym.
     */
    public function ajax_save_synonym() {
        check_ajax_referer( 'inqyra_admin', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( [
                'message' => __( 'Permission denied.', 'inqyra' ),
            ] );
        }
        // Ensure synonyms table exists
        $table_error = $this->ensure_synonyms_table();
        if ( $table_error ) {
            wp_send_json_error( [
                'message' => $table_error,
            ] );
        }
        $agent_id = ( isset( $_POST['agent_id'] ) ? absint( wp_unslash( $_POST['agent_id'] ) ) : 0 );
        $synonym_id = ( isset( $_POST['synonym_id'] ) ? absint( wp_unslash( $_POST['synonym_id'] ) ) : 0 );
        $term = ( isset( $_POST['term'] ) ? sanitize_text_field( wp_unslash( $_POST['term'] ) ) : '' );
        $synonyms = ( isset( $_POST['synonyms'] ) ? sanitize_text_field( wp_unslash( $_POST['synonyms'] ) ) : '' );
        $bidirectional = ( !empty( sanitize_text_field( wp_unslash( $_POST['bidirectional'] ?? '' ) ) ) ? 1 : 0 );
        $case_sensitive = ( !empty( sanitize_text_field( wp_unslash( $_POST['case_sensitive'] ?? '' ) ) ) ? 1 : 0 );
        if ( !$agent_id ) {
            wp_send_json_error( [
                'message' => __( 'Invalid agent ID.', 'inqyra' ),
            ] );
        }
        if ( empty( $term ) ) {
            wp_send_json_error( [
                'message' => __( 'Term is required.', 'inqyra' ),
            ] );
        }
        if ( empty( $synonyms ) ) {
            wp_send_json_error( [
                'message' => __( 'Synonyms are required.', 'inqyra' ),
            ] );
        }
        global $wpdb;
        $table = $wpdb->prefix . 'inqyra_synonyms';
        $data = [
            'agent_id'       => $agent_id,
            'term'           => $term,
            'synonyms'       => $synonyms,
            'bidirectional'  => $bidirectional,
            'case_sensitive' => $case_sensitive,
        ];
        $format = [
            '%d',
            '%s',
            '%s',
            '%d',
            '%d'
        ];
        if ( $synonym_id > 0 ) {
            // Update existing
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix.
            $result = $wpdb->update(
                $table,
                $data,
                [
                    'id' => $synonym_id,
                ],
                $format,
                ['%d']
            );
        } else {
            // Check if term already exists for this agent
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix.
            $existing = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$table} WHERE agent_id = %d AND term = %s", $agent_id, $term ) );
            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
            if ( $existing ) {
                wp_send_json_error( [
                    'message' => __( 'A synonym for this term already exists.', 'inqyra' ),
                ] );
            }
            // Insert new
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix.
            $result = $wpdb->insert( $table, $data, $format );
            $synonym_id = $wpdb->insert_id;
        }
        if ( $result === false ) {
            wp_send_json_error( [
                'message' => __( 'Failed to save synonym.', 'inqyra' ) . ' ' . esc_html( $wpdb->last_error ),
            ] );
        }
        wp_send_json_success( [
            'message'    => __( 'Synonym saved.', 'inqyra' ),
            'synonym_id' => $synonym_id,
            'synonym'    => [
                'id'             => $synonym_id,
                'term'           => $term,
                'synonyms'       => $synonyms,
                'bidirectional'  => $bidirectional,
                'case_sensitive' => $case_sensitive,
            ],
        ] );
    }

    /**
     * AJAX handler for deleting a synonym.
     */
    public function ajax_delete_synonym() {
        check_ajax_referer( 'inqyra_admin', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( [
                'message' => __( 'Permission denied.', 'inqyra' ),
            ] );
        }
        $synonym_id = ( isset( $_POST['synonym_id'] ) ? absint( wp_unslash( $_POST['synonym_id'] ) ) : 0 );
        if ( !$synonym_id ) {
            wp_send_json_error( [
                'message' => __( 'Invalid synonym ID.', 'inqyra' ),
            ] );
        }
        global $wpdb;
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix.
        $result = $wpdb->delete( $wpdb->prefix . 'inqyra_synonyms', [
            'id' => $synonym_id,
        ], ['%d'] );
        if ( $result ) {
            wp_send_json_success( [
                'message' => __( 'Synonym deleted.', 'inqyra' ),
            ] );
        } else {
            wp_send_json_error( [
                'message' => __( 'Failed to delete synonym.', 'inqyra' ),
            ] );
        }
    }

    /**
     * AJAX handler for creating a new agent.
     */
    public function ajax_create_agent() {
        $this->check_license_expired_ajax();
        check_ajax_referer( 'inqyra_admin', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( [
                'message' => __( 'Permission denied.', 'inqyra' ),
            ] );
        }
        global $wpdb;
        $table = $wpdb->prefix . 'inqyra_agents';
        // If an agent already exists, reuse it instead of creating a duplicate.
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix.
        $existing = $wpdb->get_row( $wpdb->prepare( 'SELECT id FROM %i ORDER BY id ASC LIMIT 1', $table ) );
        if ( $existing ) {
            wp_send_json_success( [
                'message'  => __( 'Agent found.', 'inqyra' ),
                'agent_id' => (int) $existing->id,
                'redirect' => admin_url( 'admin.php?page=inqyra-config&agent_id=' . $existing->id ),
            ] );
            return;
        }
        $name = __( 'New Agent 1', 'inqyra' );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix.
        $result = $wpdb->insert( $table, [
            'name'            => $name,
            'system_prompt'   => '',
            'language'        => 'en',
            'provider'        => 'claude',
            'model'           => 'claude-sonnet-4-20250514',
            'max_tokens'      => 4096,
            'primary_color'   => '#0d6efd',
            'text_color'      => '#ffffff',
            'widget_position' => 'bottom-right',
            'active'          => 0,
            'created_at'      => current_time( 'mysql' ),
            'updated_at'      => current_time( 'mysql' ),
        ] );
        if ( $result === false ) {
            wp_send_json_error( [
                'message' => __( 'Failed to create agent.', 'inqyra' ),
            ] );
        }
        $agent_id = $wpdb->insert_id;
        wp_send_json_success( [
            'message'  => __( 'Agent created.', 'inqyra' ),
            'agent_id' => $agent_id,
            'redirect' => admin_url( 'admin.php?page=inqyra-config&agent_id=' . $agent_id ),
        ] );
    }

    /**
     * AJAX handler for reprocessing a failed document.
     */
    public function ajax_reprocess_document() {
        check_ajax_referer( 'inqyra_admin', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( [
                'message' => __( 'Permission denied.', 'inqyra' ),
            ] );
        }
        $document_id = ( isset( $_POST['document_id'] ) ? absint( wp_unslash( $_POST['document_id'] ) ) : 0 );
        if ( !$document_id ) {
            wp_send_json_error( [
                'message' => __( 'Invalid document ID.', 'inqyra' ),
            ] );
        }
        global $wpdb;
        // Check document exists and is failed
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix.
        $document = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE id = %d', $wpdb->prefix . 'inqyra_documents', $document_id ) );
        if ( !$document ) {
            wp_send_json_error( [
                'message' => __( 'Document not found.', 'inqyra' ),
            ] );
        }
        // Reset status to pending
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix.
        $wpdb->update( $wpdb->prefix . 'inqyra_documents', [
            'status'        => 'pending',
            'error_message' => null,
            'chunk_count'   => 0,
        ], [
            'id' => $document_id,
        ] );
        // Delete existing chunks for this document
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix.
        $wpdb->delete( $wpdb->prefix . 'inqyra_knowledge_chunks', [
            'source_type' => 'document',
            'source_id'   => $document_id,
        ] );
        // Process the document
        $processor = new Inqyra_Document_Processor();
        $processor->process( $document_id );
        // Get updated status
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix.
        $document = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE id = %d', $wpdb->prefix . 'inqyra_documents', $document_id ) );
        wp_send_json_success( [
            'message'       => __( 'Document reprocessed.', 'inqyra' ),
            'status'        => $document->status,
            'chunk_count'   => (int) $document->chunk_count,
            'agent_id'      => (int) $document->agent_id,
            'error_message' => $document->error_message,
        ] );
    }

    /**
     * Ensure the synonyms table exists.
     *
     * @return string|null Error message if table creation failed, null on success.
     */
    private function ensure_synonyms_table() {
        global $wpdb;
        $table = $wpdb->prefix . 'inqyra_synonyms';
        // Validate table name format (security: prevent SQL injection via table names)
        if ( !preg_match( '/^[a-zA-Z0-9_]+$/', $table ) ) {
            return 'Invalid table name format.';
        }
        // Quick check if table exists - use prepare() with esc_like() for LIKE patterns
        $table_like = $wpdb->esc_like( $table );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Checking if plugin table exists.
        if ( $wpdb->get_var( $wpdb->prepare( "SHOW TABLES LIKE %s", $table_like ) ) === $table ) {
            return null;
        }
        // Create table
        // Note: term is varchar(191) to stay within 767 byte key limit for utf8mb4
        // Note: Table name is validated above, safe to use in DDL
        $charset_collate = $wpdb->get_charset_collate();
        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- DDL for plugin table, name validated above.
        $result = $wpdb->query( $wpdb->prepare( "CREATE TABLE IF NOT EXISTS %i (\n            id bigint(20) unsigned NOT NULL AUTO_INCREMENT,\n            agent_id bigint(20) unsigned NOT NULL,\n            term varchar(191) NOT NULL,\n            synonyms text NOT NULL,\n            bidirectional tinyint(1) DEFAULT 0,\n            case_sensitive tinyint(1) DEFAULT 0,\n            created_at timestamp DEFAULT CURRENT_TIMESTAMP,\n            PRIMARY KEY (id),\n            UNIQUE KEY unique_term (agent_id, term),\n            KEY idx_agent (agent_id)\n        ) {$charset_collate};", $table ) );
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        if ( $result === false ) {
            return 'Failed to create synonyms table: ' . $wpdb->last_error;
        }
        // Verify table was created
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Verifying table creation.
        if ( $wpdb->get_var( $wpdb->prepare( "SHOW TABLES LIKE %s", $table_like ) ) !== $table ) {
            return 'Table creation query ran but table does not exist. DB Error: ' . $wpdb->last_error;
        }
        return null;
    }

    /**
     * Detect language from WordPress locale.
     *
     * @return string Language code (nl, en, de, fr).
     */
    private function detect_language() {
        $locale = get_locale();
        // Extract primary language code (e.g. 'nl' from 'nl_NL')
        $primary = strtolower( substr( $locale, 0, 2 ) );
        $supported = [
            'nl',
            'en',
            'de',
            'fr',
            'es',
            'it',
            'pt',
            'pl',
            'sv',
            'da',
            'no',
            'fi',
            'ja',
            'ko',
            'zh',
            'ar',
            'tr',
            'ru',
            'uk',
            'cs',
            'ro',
            'hu',
            'el',
            'he',
            'th',
            'vi',
            'id',
            'ms',
            'hi'
        ];
        // Special case: Norwegian locales use 'nb'/'nn'
        if ( $primary === 'nb' || $primary === 'nn' ) {
            return 'no';
        }
        return ( in_array( $primary, $supported, true ) ? $primary : 'en' );
    }

    /**
     * AJAX handler for refreshing available models from provider API.
     */
    public function ajax_refresh_models() {
        check_ajax_referer( 'inqyra_admin', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( [
                'message' => __( 'Permission denied.', 'inqyra' ),
            ] );
        }
        $provider = ( isset( $_POST['provider'] ) ? sanitize_text_field( wp_unslash( $_POST['provider'] ) ) : '' );
        $valid_providers = [
            'claude',
            'openai',
            'gemini',
            'mistral',
            'deepseek'
        ];
        if ( !in_array( $provider, $valid_providers, true ) ) {
            wp_send_json_error( [
                'message' => __( 'Invalid provider.', 'inqyra' ),
            ] );
        }
        try {
            $instance = Inqyra_AI_Provider_Factory::create( $provider );
            if ( !$instance->is_configured() ) {
                wp_send_json_error( [
                    'message' => __( 'API key not configured for this provider.', 'inqyra' ),
                ] );
            }
            $api_models = $instance->fetch_available_models();
            if ( !empty( $api_models ) ) {
                set_transient( 'inqyra_api_models_' . $provider, $api_models, 86400 );
            }
            // Return the merged model list
            $models = $instance->get_models();
            wp_send_json_success( [
                'models'       => $models,
                'refreshed_at' => current_time( 'mysql' ),
            ] );
        } catch ( Exception $e ) {
            wp_send_json_error( [
                'message' => sprintf( 
                    /* translators: %s: error message from the API */
                    __( 'Failed to fetch models: %s', 'inqyra' ),
                    esc_html( $e->getMessage() )
                 ),
            ] );
        }
    }

    /**
     * AJAX handler for validating an API key.
     */
    public function ajax_validate_api_key() {
        check_ajax_referer( 'inqyra_admin', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( [
                'message' => __( 'Permission denied.', 'inqyra' ),
            ] );
        }
        $provider = ( isset( $_POST['provider'] ) ? sanitize_text_field( wp_unslash( $_POST['provider'] ) ) : '' );
        $api_key = ( isset( $_POST['api_key'] ) ? sanitize_text_field( wp_unslash( $_POST['api_key'] ) ) : '' );
        if ( empty( $provider ) || empty( $api_key ) ) {
            wp_send_json_error( [
                'message' => __( 'Provider and API key are required.', 'inqyra' ),
            ] );
        }
        $valid_providers = [
            'claude',
            'openai',
            'gemini',
            'mistral',
            'deepseek'
        ];
        if ( !in_array( $provider, $valid_providers, true ) ) {
            wp_send_json_error( [
                'message' => __( 'Invalid provider.', 'inqyra' ),
            ] );
        }
        $result = $this->validate_provider_api_key( $provider, $api_key );
        if ( $result['valid'] ) {
            wp_send_json_success( [
                'message' => __( 'API key is valid!', 'inqyra' ),
            ] );
        } else {
            wp_send_json_error( [
                'message' => $result['error'],
            ] );
        }
    }

    /**
     * AJAX handler for deleting an API key.
     */
    public function ajax_delete_api_key() {
        check_ajax_referer( 'inqyra_admin', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( [
                'message' => __( 'Permission denied.', 'inqyra' ),
            ] );
        }
        $provider = ( isset( $_POST['provider'] ) ? sanitize_text_field( wp_unslash( $_POST['provider'] ) ) : '' );
        $valid_providers = [
            'claude',
            'openai',
            'gemini',
            'mistral',
            'deepseek'
        ];
        if ( !in_array( $provider, $valid_providers, true ) ) {
            wp_send_json_error( [
                'message' => __( 'Invalid provider.', 'inqyra' ),
            ] );
        }
        $settings = get_option( 'inqyra_settings', [] );
        if ( isset( $settings['api_keys'][$provider] ) ) {
            unset($settings['api_keys'][$provider]);
            update_option( 'inqyra_settings', $settings );
            delete_transient( 'inqyra_api_models_' . $provider );
        }
        wp_send_json_success( [
            'message' => __( 'API key deleted.', 'inqyra' ),
        ] );
    }

    /**
     * Validate a provider API key by making a test request.
     *
     * @param string $provider Provider name.
     * @param string $api_key  API key to validate.
     * @return array           Result with 'valid' bool and optional 'error' string.
     */
    private function validate_provider_api_key( $provider, $api_key ) {
        $timeout = 15;
        switch ( $provider ) {
            case 'claude':
                $response = wp_remote_post( 'https://api.anthropic.com/v1/messages', [
                    'timeout' => $timeout,
                    'headers' => [
                        'x-api-key'         => $api_key,
                        'anthropic-version' => '2023-06-01',
                        'Content-Type'      => 'application/json',
                    ],
                    'body'    => wp_json_encode( [
                        'model'      => 'claude-3-haiku-20240307',
                        'max_tokens' => 1,
                        'messages'   => [[
                            'role'    => 'user',
                            'content' => 'Hi',
                        ]],
                    ] ),
                ] );
                break;
            case 'openai':
                $response = wp_remote_get( 'https://api.openai.com/v1/models', [
                    'timeout' => $timeout,
                    'headers' => [
                        'Authorization' => 'Bearer ' . $api_key,
                    ],
                ] );
                break;
            case 'gemini':
                $response = wp_remote_get( 'https://generativelanguage.googleapis.com/v1beta/models', [
                    'timeout' => $timeout,
                    'headers' => [
                        'x-goog-api-key' => $api_key,
                    ],
                ] );
                break;
            case 'mistral':
                $response = wp_remote_get( 'https://api.mistral.ai/v1/models', [
                    'timeout' => $timeout,
                    'headers' => [
                        'Authorization' => 'Bearer ' . $api_key,
                    ],
                ] );
                break;
            case 'deepseek':
                $response = wp_remote_get( 'https://api.deepseek.com/v1/models', [
                    'timeout' => $timeout,
                    'headers' => [
                        'Authorization' => 'Bearer ' . $api_key,
                    ],
                ] );
                break;
            default:
                return [
                    'valid' => false,
                    'error' => __( 'Unknown provider.', 'inqyra' ),
                ];
        }
        if ( is_wp_error( $response ) ) {
            return [
                'valid' => false,
                'error' => $response->get_error_message(),
            ];
        }
        $status_code = wp_remote_retrieve_response_code( $response );
        if ( $status_code >= 200 && $status_code < 300 ) {
            return [
                'valid' => true,
            ];
        }
        if ( $status_code === 401 || $status_code === 403 ) {
            return [
                'valid' => false,
                'error' => __( 'Invalid API key. Please check and try again.', 'inqyra' ),
            ];
        }
        /* translators: %d: HTTP status code returned by the API */
        $error_msg = sprintf( __( 'API returned status %d. Please check your key.', 'inqyra' ), $status_code );
        return [
            'valid' => false,
            'error' => $error_msg,
        ];
    }

    /**
     * AJAX handler for saving wizard state.
     */
    public function ajax_save_wizard_state() {
        check_ajax_referer( 'inqyra_admin', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( [
                'message' => __( 'Permission denied.', 'inqyra' ),
            ] );
        }
        $state_json = ( isset( $_POST['state'] ) ? sanitize_textarea_field( wp_unslash( $_POST['state'] ) ) : '' );
        if ( empty( $state_json ) ) {
            wp_send_json_error( [
                'message' => __( 'No state data provided.', 'inqyra' ),
            ] );
        }
        // Decode JSON, then sanitize each value individually via whitelist below.
        $state = json_decode( $state_json, true );
        if ( json_last_error() !== JSON_ERROR_NONE ) {
            wp_send_json_error( [
                'message' => __( 'Invalid state data.', 'inqyra' ),
            ] );
        }
        // Whitelist allowed keys
        $allowed_keys = [
            'currentStep',
            'completedSteps',
            'agentId',
            'apiKeyValid',
            'selectedProvider',
            'selectedModel',
            'crawlComplete',
            'crawlChunks',
            'crawlPosts',
            'embeddingsComplete',
            'embeddingsProcessed',
            'embeddingsFailed',
            'embeddingsTotal',
            'skipEmbeddings'
        ];
        $sanitized = [];
        foreach ( $allowed_keys as $key ) {
            if ( isset( $state[$key] ) ) {
                $value = $state[$key];
                if ( is_array( $value ) ) {
                    // completedSteps is an array of step numbers.
                    $sanitized[$key] = array_map( 'absint', $value );
                } elseif ( is_bool( $value ) ) {
                    $sanitized[$key] = (bool) $value;
                } elseif ( is_numeric( $value ) ) {
                    $sanitized[$key] = absint( $value );
                } else {
                    $sanitized[$key] = sanitize_text_field( $value );
                }
            }
        }
        $user_id = get_current_user_id();
        set_transient( 'inqyra_wizard_state_' . $user_id, wp_json_encode( $sanitized ), DAY_IN_SECONDS );
        wp_send_json_success( [
            'message' => __( 'State saved.', 'inqyra' ),
        ] );
    }

    /**
     * AJAX handler for completing the wizard.
     */
    public function ajax_complete_wizard() {
        check_ajax_referer( 'inqyra_admin', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( [
                'message' => __( 'Permission denied.', 'inqyra' ),
            ] );
        }
        $agent_id = ( isset( $_POST['agent_id'] ) ? absint( wp_unslash( $_POST['agent_id'] ) ) : 0 );
        if ( !$agent_id ) {
            wp_send_json_error( [
                'message' => __( 'Invalid agent ID.', 'inqyra' ),
            ] );
        }
        global $wpdb;
        // Deactivate all other agents first (single-agent mode)
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix.
        $wpdb->query( $wpdb->prepare( 'UPDATE %i SET active = 0 WHERE id != %d', $wpdb->prefix . 'inqyra_agents', $agent_id ) );
        // Enable the agent and widget
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix.
        $wpdb->update( $wpdb->prefix . 'inqyra_agents', [
            'widget_enabled' => 1,
            'active'         => 1,
            'updated_at'     => current_time( 'mysql' ),
        ], [
            'id' => $agent_id,
        ] );
        // Mark wizard as completed
        update_option( 'inqyra_wizard_completed', true );
        // Delete wizard state transient
        $user_id = get_current_user_id();
        delete_transient( 'inqyra_wizard_state_' . $user_id );
        // Clear agent cache
        if ( class_exists( 'Inqyra_Chat_Service' ) ) {
            Inqyra_Chat_Service::clear_agent_cache( $agent_id );
        }
        wp_send_json_success( [
            'message'  => __( 'Setup complete! Your chatbot is now active.', 'inqyra' ),
            'redirect' => admin_url( 'admin.php?page=inqyra' ),
        ] );
    }

    /**
     * Get current agent.
     *
     * @return object|null Current agent or null.
     */
    public function get_current_agent() {
        global $wpdb;
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin navigation parameter.
        $agent_id = ( isset( $_GET['agent_id'] ) ? absint( wp_unslash( $_GET['agent_id'] ) ) : 0 );
        if ( $agent_id ) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix.
            return $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE id = %d', $wpdb->prefix . 'inqyra_agents', $agent_id ) );
        }
        // Prefer the most recently updated active agent
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table query.
        $agent = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE active = 1 ORDER BY updated_at DESC LIMIT 1', $wpdb->prefix . 'inqyra_agents' ) );
        // Fallback to any agent if no active one exists
        if ( !$agent ) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table query.
            $agent = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i ORDER BY id ASC LIMIT 1', $wpdb->prefix . 'inqyra_agents' ) );
        }
        return $agent;
    }

    /**
     * Get the current agent's language setting.
     *
     * @return string Language code (e.g. 'en', 'nl').
     */
    private function get_agent_language() {
        $agent = $this->get_current_agent();
        return ( $agent ? $agent->language ?? 'en' : 'en' );
    }

    /**
     * Get all agents.
     *
     * @return array List of agents.
     */
    public function get_all_agents() {
        global $wpdb;
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table query.
        return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i ORDER BY name ASC', $wpdb->prefix . 'inqyra_agents' ) );
    }

    /**
     * AJAX handler for fetching agent data (used by wizard state restore).
     */
    public function ajax_get_agent() {
        check_ajax_referer( 'inqyra_admin', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( [
                'message' => __( 'Permission denied.', 'inqyra' ),
            ] );
        }
        $agent_id = ( isset( $_POST['agent_id'] ) ? absint( wp_unslash( $_POST['agent_id'] ) ) : 0 );
        if ( !$agent_id ) {
            wp_send_json_error( [
                'message' => __( 'Invalid agent ID.', 'inqyra' ),
            ] );
        }
        global $wpdb;
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix.
        $agent = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE id = %d', $wpdb->prefix . 'inqyra_agents', $agent_id ), ARRAY_A );
        if ( !$agent ) {
            wp_send_json_error( [
                'message' => __( 'Agent not found.', 'inqyra' ),
            ] );
        }
        // Get header icon URL if set
        $agent['header_icon_url'] = '';
        if ( !empty( $agent['header_icon_id'] ) ) {
            $agent['header_icon_url'] = ( wp_get_attachment_image_url( (int) $agent['header_icon_id'], 'thumbnail' ) ?: '' );
        }
        wp_send_json_success( $agent );
    }

    /**
     * AJAX handler for creating a webhook.
     */
    public function ajax_create_webhook() {
        check_ajax_referer( 'inqyra_admin', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( [
                'message' => __( 'Permission denied.', 'inqyra' ),
            ] );
        }
        $agent_id = ( isset( $_POST['agent_id'] ) ? absint( wp_unslash( $_POST['agent_id'] ) ) : 0 );
        $name = ( isset( $_POST['name'] ) ? sanitize_text_field( wp_unslash( $_POST['name'] ) ) : '' );
        $url = ( isset( $_POST['url'] ) ? esc_url_raw( wp_unslash( $_POST['url'] ) ) : '' );
        $events = ( isset( $_POST['events'] ) ? array_map( 'sanitize_text_field', wp_unslash( (array) $_POST['events'] ) ) : [] );
        if ( empty( $agent_id ) || empty( $name ) || empty( $url ) || empty( $events ) ) {
            wp_send_json_error( [
                'message' => __( 'Please fill in all required fields.', 'inqyra' ),
            ] );
        }
        // Validate URL
        if ( !filter_var( $url, FILTER_VALIDATE_URL ) ) {
            wp_send_json_error( [
                'message' => __( 'Please enter a valid URL.', 'inqyra' ),
            ] );
        }
        // Validate events
        $valid_events = [
            'conversation.started',
            'conversation.ended',
            'message.received',
            'lead.captured',
            'escalation.requested',
            'appointment.requested'
        ];
        $events = array_intersect( $events, $valid_events );
        if ( empty( $events ) ) {
            wp_send_json_error( [
                'message' => __( 'Please select at least one event.', 'inqyra' ),
            ] );
        }
        // Parse custom headers
        $headers = [];
        if ( !empty( $_POST['header_keys'] ) && !empty( $_POST['header_values'] ) ) {
            $keys = array_map( 'sanitize_text_field', wp_unslash( $_POST['header_keys'] ) );
            $values = array_map( 'sanitize_text_field', wp_unslash( $_POST['header_values'] ) );
            foreach ( $keys as $i => $key ) {
                $key = trim( $key );
                if ( !empty( $key ) && isset( $values[$i] ) ) {
                    $headers[$key] = trim( $values[$i] );
                }
            }
        }
        $webhook_service = new Inqyra_Webhook_Service();
        $webhook_id = $webhook_service->create_webhook(
            $agent_id,
            $name,
            $url,
            $events,
            $headers
        );
        if ( $webhook_id === false ) {
            wp_send_json_error( [
                'message' => __( 'Failed to create webhook.', 'inqyra' ),
            ] );
        }
        $webhook = $webhook_service->get_webhook( $webhook_id );
        wp_send_json_success( [
            'message' => __( 'Webhook created successfully.', 'inqyra' ),
            'webhook' => [
                'id'      => $webhook->id,
                'name'    => $webhook->name,
                'url'     => $webhook->url,
                'events'  => json_decode( $webhook->events, true ),
                'enabled' => (bool) $webhook->enabled,
                'secret'  => $webhook->secret,
            ],
        ] );
    }

    /**
     * AJAX handler for deleting a webhook.
     */
    public function ajax_delete_webhook() {
        check_ajax_referer( 'inqyra_admin', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( [
                'message' => __( 'Permission denied.', 'inqyra' ),
            ] );
        }
        $webhook_id = ( isset( $_POST['webhook_id'] ) ? absint( wp_unslash( $_POST['webhook_id'] ) ) : 0 );
        if ( empty( $webhook_id ) ) {
            wp_send_json_error( [
                'message' => __( 'Invalid webhook ID.', 'inqyra' ),
            ] );
        }
        $webhook_service = new Inqyra_Webhook_Service();
        $result = $webhook_service->delete_webhook( $webhook_id );
        if ( !$result ) {
            wp_send_json_error( [
                'message' => __( 'Failed to delete webhook.', 'inqyra' ),
            ] );
        }
        wp_send_json_success( [
            'message' => __( 'Webhook deleted successfully.', 'inqyra' ),
        ] );
    }

    /**
     * AJAX handler for toggling a webhook.
     */
    public function ajax_toggle_webhook() {
        check_ajax_referer( 'inqyra_admin', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( [
                'message' => __( 'Permission denied.', 'inqyra' ),
            ] );
        }
        $webhook_id = ( isset( $_POST['webhook_id'] ) ? absint( wp_unslash( $_POST['webhook_id'] ) ) : 0 );
        if ( empty( $webhook_id ) ) {
            wp_send_json_error( [
                'message' => __( 'Invalid webhook ID.', 'inqyra' ),
            ] );
        }
        $webhook_service = new Inqyra_Webhook_Service();
        $result = $webhook_service->toggle_webhook( $webhook_id );
        if ( !$result ) {
            wp_send_json_error( [
                'message' => __( 'Failed to toggle webhook.', 'inqyra' ),
            ] );
        }
        $webhook = $webhook_service->get_webhook( $webhook_id );
        wp_send_json_success( [
            'message' => ( $webhook->enabled ? __( 'Webhook enabled.', 'inqyra' ) : __( 'Webhook disabled.', 'inqyra' ) ),
            'enabled' => (bool) $webhook->enabled,
        ] );
    }

    /**
     * AJAX handler for testing a webhook.
     */
    public function ajax_test_webhook() {
        check_ajax_referer( 'inqyra_admin', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( [
                'message' => __( 'Permission denied.', 'inqyra' ),
            ] );
        }
        $webhook_id = ( isset( $_POST['webhook_id'] ) ? absint( wp_unslash( $_POST['webhook_id'] ) ) : 0 );
        if ( empty( $webhook_id ) ) {
            wp_send_json_error( [
                'message' => __( 'Invalid webhook ID.', 'inqyra' ),
            ] );
        }
        $webhook_service = new Inqyra_Webhook_Service();
        $result = $webhook_service->test_webhook( $webhook_id );
        if ( $result['success'] ) {
            wp_send_json_success( [
                'message' => $result['message'],
            ] );
        } else {
            wp_send_json_error( [
                'message' => $result['message'],
            ] );
        }
    }

    /**
     * AJAX handler for updating a webhook.
     */
    public function ajax_update_webhook() {
        check_ajax_referer( 'inqyra_admin', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( [
                'message' => __( 'Permission denied.', 'inqyra' ),
            ] );
        }
        $webhook_id = ( isset( $_POST['webhook_id'] ) ? absint( wp_unslash( $_POST['webhook_id'] ) ) : 0 );
        $name = ( isset( $_POST['name'] ) ? sanitize_text_field( wp_unslash( $_POST['name'] ) ) : '' );
        $url = ( isset( $_POST['url'] ) ? esc_url_raw( wp_unslash( $_POST['url'] ) ) : '' );
        $events = ( isset( $_POST['events'] ) ? array_map( 'sanitize_text_field', wp_unslash( (array) $_POST['events'] ) ) : [] );
        if ( empty( $webhook_id ) || empty( $name ) || empty( $url ) || empty( $events ) ) {
            wp_send_json_error( [
                'message' => __( 'Please fill in all required fields.', 'inqyra' ),
            ] );
        }
        if ( !filter_var( $url, FILTER_VALIDATE_URL ) ) {
            wp_send_json_error( [
                'message' => __( 'Please enter a valid URL.', 'inqyra' ),
            ] );
        }
        $valid_events = [
            'conversation.started',
            'conversation.ended',
            'message.received',
            'lead.captured',
            'escalation.requested',
            'appointment.requested'
        ];
        $events = array_values( array_intersect( $events, $valid_events ) );
        if ( empty( $events ) ) {
            wp_send_json_error( [
                'message' => __( 'Please select at least one event.', 'inqyra' ),
            ] );
        }
        // Parse custom headers
        $headers = [];
        if ( !empty( $_POST['header_keys'] ) && !empty( $_POST['header_values'] ) ) {
            $keys = array_map( 'sanitize_text_field', wp_unslash( $_POST['header_keys'] ) );
            $values = array_map( 'sanitize_text_field', wp_unslash( $_POST['header_values'] ) );
            foreach ( $keys as $i => $key ) {
                $key = trim( $key );
                if ( !empty( $key ) && isset( $values[$i] ) ) {
                    $headers[$key] = trim( $values[$i] );
                }
            }
        }
        $webhook_service = new Inqyra_Webhook_Service();
        $result = $webhook_service->update_webhook( $webhook_id, [
            'name'    => $name,
            'url'     => $url,
            'events'  => $events,
            'headers' => $headers,
        ] );
        if ( !$result ) {
            wp_send_json_error( [
                'message' => __( 'Failed to update webhook.', 'inqyra' ),
            ] );
        }
        wp_send_json_success( [
            'message' => __( 'Webhook updated successfully.', 'inqyra' ),
        ] );
    }

    /**
     * AJAX handler for regenerating a webhook secret.
     */
    public function ajax_regenerate_webhook_secret() {
        check_ajax_referer( 'inqyra_admin', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( [
                'message' => __( 'Permission denied.', 'inqyra' ),
            ] );
        }
        $webhook_id = ( isset( $_POST['webhook_id'] ) ? absint( wp_unslash( $_POST['webhook_id'] ) ) : 0 );
        if ( empty( $webhook_id ) ) {
            wp_send_json_error( [
                'message' => __( 'Invalid webhook ID.', 'inqyra' ),
            ] );
        }
        $webhook_service = new Inqyra_Webhook_Service();
        $secret = $webhook_service->regenerate_secret( $webhook_id );
        if ( $secret === false ) {
            wp_send_json_error( [
                'message' => __( 'Failed to regenerate secret.', 'inqyra' ),
            ] );
        }
        wp_send_json_success( [
            'message' => __( 'Secret regenerated successfully.', 'inqyra' ),
            'secret'  => $secret,
        ] );
    }

    /**
     * AJAX handler for getting webhook logs.
     */
    public function ajax_get_webhook_logs() {
        check_ajax_referer( 'inqyra_admin', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( [
                'message' => __( 'Permission denied.', 'inqyra' ),
            ] );
        }
        $agent_id = ( isset( $_POST['agent_id'] ) ? absint( wp_unslash( $_POST['agent_id'] ) ) : 0 );
        if ( empty( $agent_id ) ) {
            wp_send_json_error( [
                'message' => __( 'Invalid agent ID.', 'inqyra' ),
            ] );
        }
        $filters = [];
        if ( !empty( $_POST['event_type'] ) ) {
            $filters['event_type'] = sanitize_text_field( wp_unslash( $_POST['event_type'] ) );
        }
        if ( isset( $_POST['success'] ) && sanitize_text_field( wp_unslash( $_POST['success'] ) ) !== '' ) {
            $filters['success'] = absint( wp_unslash( $_POST['success'] ) );
        }
        $webhook_service = new Inqyra_Webhook_Service();
        $logs = $webhook_service->get_agent_logs( $agent_id, $filters );
        // Format for display
        $formatted = [];
        foreach ( $logs as $log ) {
            $formatted[] = [
                'id'              => (int) $log->id,
                'webhook_name'    => $log->webhook_name,
                'webhook_url'     => $log->webhook_url,
                'event_type'      => $log->event_type,
                'response_status' => (int) $log->response_status,
                'success'         => (bool) $log->success,
                'retry_count'     => (int) $log->retry_count,
                'error_message'   => $log->error_message,
                'payload'         => $log->payload,
                'response_body'   => $log->response_body,
                'sent_at'         => $log->sent_at,
                'completed_at'    => $log->completed_at,
            ];
        }
        wp_send_json_success( [
            'logs' => $formatted,
        ] );
    }

    /**
     * AJAX handler for exporting conversations.
     */
    public function ajax_export_conversations() {
        check_ajax_referer( 'inqyra_admin', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( [
                'message' => __( 'Permission denied.', 'inqyra' ),
            ] );
        }
        global $wpdb;
        $agent_id = ( isset( $_POST['agent_id'] ) ? absint( wp_unslash( $_POST['agent_id'] ) ) : 0 );
        $format = ( isset( $_POST['format'] ) ? sanitize_text_field( wp_unslash( $_POST['format'] ) ) : 'csv' );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix.
        $conversations = $wpdb->get_results( $wpdb->prepare(
            'SELECT c.*,
                    (SELECT COUNT(*) FROM %i WHERE conversation_id = c.id) as message_count
             FROM %i c
             WHERE c.agent_id = %d
             ORDER BY c.started_at DESC',
            $wpdb->prefix . 'inqyra_messages',
            $wpdb->prefix . 'inqyra_conversations',
            $agent_id
        ), ARRAY_A );
        if ( $format === 'csv' ) {
            $csv_data = $this->generate_conversations_csv( $conversations );
            wp_send_json_success( [
                'data'     => $csv_data,
                'filename' => 'conversations-' . gmdate( 'Y-m-d' ) . '.csv',
                'type'     => 'text/csv',
            ] );
        } else {
            wp_send_json_success( [
                'data'     => $conversations,
                'filename' => 'conversations-' . gmdate( 'Y-m-d' ) . '.json',
                'type'     => 'application/json',
            ] );
        }
    }

    /**
     * Generate CSV data for conversations.
     *
     * @param array $conversations Array of conversations.
     * @return string CSV data.
     */
    private function generate_conversations_csv( $conversations ) {
        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- php://temp is an in-memory stream, not a filesystem operation.
        $output = fopen( 'php://temp', 'r+' );
        // Header row
        fputcsv( $output, [
            'ID',
            'Session ID',
            'Started At',
            'Last Message At',
            'Message Count',
            'Total Tokens',
            'Total Cost (USD)',
            'Archived'
        ] );
        foreach ( $conversations as $conv ) {
            fputcsv( $output, [
                $conv['id'],
                $conv['session_id'],
                $conv['started_at'],
                $conv['last_message_at'],
                $conv['message_count'],
                $conv['total_tokens'],
                $conv['total_cost_usd'],
                ( $conv['archived'] ? 'Yes' : 'No' )
            ] );
        }
        rewind( $output );
        $csv = stream_get_contents( $output );
        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Closing in-memory stream.
        fclose( $output );
        return $csv;
    }

    /**
     * AJAX handler for exporting support diagnostics.
     */
    public function ajax_export_diagnostics() {
        check_ajax_referer( 'inqyra_admin', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array(
                'message' => __( 'Permission denied.', 'inqyra' ),
            ) );
        }
        $diagnostics = array(
            'export_version' => '1.0',
            'exported_at'    => gmdate( 'c' ),
            'environment'    => $this->get_diagnostics_environment(),
            'plugin_state'   => $this->get_diagnostics_plugin_state(),
            'agents'         => $this->get_diagnostics_agents(),
            'knowledge_base' => $this->get_diagnostics_knowledge_base(),
            'conversations'  => $this->get_diagnostics_conversations(),
            'recent_errors'  => $this->get_diagnostics_recent_errors(),
            'cron_status'    => $this->get_diagnostics_cron_status(),
            'browser'        => null,
        );
        wp_send_json_success( array(
            'data'     => $diagnostics,
            'filename' => 'inqyra-diagnostics-' . gmdate( 'Y-m-d' ) . '.json',
            'type'     => 'application/json',
        ) );
    }

    /**
     * Get environment diagnostics.
     *
     * @return array Environment info.
     */
    private function get_diagnostics_environment() {
        global $wpdb;
        $active_plugins = get_option( 'active_plugins', array() );
        $plugin_list = array();
        foreach ( $active_plugins as $plugin_file ) {
            $plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin_file, false, false );
            $plugin_list[] = $plugin_data['Name'] . ' ' . $plugin_data['Version'];
        }
        $theme = wp_get_theme();
        return array(
            'php_version'         => PHP_VERSION,
            'php_extensions'      => array(
                'curl'     => extension_loaded( 'curl' ),
                'mbstring' => extension_loaded( 'mbstring' ),
                'openssl'  => extension_loaded( 'openssl' ),
                'json'     => extension_loaded( 'json' ),
                'dom'      => extension_loaded( 'dom' ),
            ),
            'wordpress_version'   => get_bloginfo( 'version' ),
            'multisite'           => is_multisite(),
            'locale'              => get_locale(),
            'active_theme'        => $theme->get( 'Name' ) . ' ' . $theme->get( 'Version' ),
            'active_plugins'      => $plugin_list,
            'server_software'     => ( isset( $_SERVER['SERVER_SOFTWARE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : 'unknown' ),
            'memory_limit'        => ini_get( 'memory_limit' ),
            'max_execution_time'  => (int) ini_get( 'max_execution_time' ),
            'upload_max_filesize' => ini_get( 'upload_max_filesize' ),
            'mysql_version'       => $wpdb->db_version(),
            'wp_debug'            => defined( 'WP_DEBUG' ) && WP_DEBUG,
            'wp_debug_log'        => defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG,
            'ssl'                 => is_ssl(),
            'permalink_structure' => ( get_option( 'permalink_structure' ) ?: 'Plain' ),
        );
    }

    /**
     * Get plugin state diagnostics.
     *
     * @return array Plugin state info.
     */
    private function get_diagnostics_plugin_state() {
        global $wpdb;
        $settings = get_option( 'inqyra_settings', array() );
        $api_keys = $settings['api_keys'] ?? array();
        $providers_configured = array();
        foreach ( array(
            'claude',
            'openai',
            'gemini',
            'mistral',
            'deepseek'
        ) as $provider ) {
            $providers_configured[$provider] = !empty( $api_keys[$provider] );
        }
        return array(
            'inqyra_version'       => INQYRA_VERSION,
            'db_version'           => get_option( 'inqyra_db_version', 'unknown' ),
            'is_premium'           => inqyra_fs()->is__premium_only(),
            'is_paying'            => inqyra_fs()->is__premium_only() && inqyra_fs()->is_paying(),
            'plan'                 => ( inqyra_fs()->is__premium_only() && function_exists( 'inqyra_is_premium' ) && inqyra_is_premium() ? 'premium' : 'free' ),
            'wizard_completed'     => (bool) get_option( 'inqyra_wizard_completed', false ),
            'providers_configured' => $providers_configured,
            'debug_mode'           => !empty( $settings['debug_mode'] ),
            'embedding_batch_size' => (int) ($settings['embedding_batch_size'] ?? 5),
        );
    }

    /**
     * Get sanitized agent configurations for diagnostics.
     *
     * @return array Agent configs with system_prompt redacted.
     */
    private function get_diagnostics_agents() {
        global $wpdb;
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Diagnostics page, no caching context.
        $agents = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i ORDER BY id ASC', $wpdb->prefix . 'inqyra_agents' ) );
        $result = array();
        foreach ( $agents as $agent ) {
            $agent_data = (array) $agent;
            $agent_data['system_prompt_length'] = strlen( $agent->system_prompt ?? '' );
            unset($agent_data['system_prompt']);
            foreach ( array('crawl_parts', 'crawl_post_types', 'widget_pages') as $json_field ) {
                if ( isset( $agent_data[$json_field] ) && is_string( $agent_data[$json_field] ) ) {
                    $agent_data[$json_field] = json_decode( $agent_data[$json_field], true );
                }
            }
            $result[] = $agent_data;
        }
        return $result;
    }

    /**
     * Get knowledge base statistics per agent for diagnostics.
     *
     * @return array Knowledge base stats.
     */
    private function get_diagnostics_knowledge_base() {
        global $wpdb;
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Diagnostics page, no caching context.
        $agents = $wpdb->get_col( $wpdb->prepare( 'SELECT id FROM %i ORDER BY id ASC', $wpdb->prefix . 'inqyra_agents' ) );
        $result = array();
        foreach ( $agents as $agent_id ) {
            $agent_id = (int) $agent_id;
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix.
            $total = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i WHERE agent_id = %d', $wpdb->prefix . 'inqyra_knowledge_chunks', $agent_id ) );
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix.
            $with_embedding = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i WHERE agent_id = %d AND embedding IS NOT NULL', $wpdb->prefix . 'inqyra_knowledge_chunks', $agent_id ) );
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix.
            $pending = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i WHERE agent_id = %d AND needs_embedding = 1', $wpdb->prefix . 'inqyra_knowledge_chunks', $agent_id ) );
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix.
            $source_breakdown = $wpdb->get_results( $wpdb->prepare( 'SELECT source_type, COUNT(*) as count FROM %i WHERE agent_id = %d GROUP BY source_type', $wpdb->prefix . 'inqyra_knowledge_chunks', $agent_id ), ARRAY_A );
            $by_source = array();
            foreach ( $source_breakdown as $row ) {
                $by_source[$row['source_type']] = (int) $row['count'];
            }
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix.
            $last_crawl = $wpdb->get_row( $wpdb->prepare( 'SELECT status, started_at, completed_at, chunks_created, error_message FROM %i WHERE agent_id = %d ORDER BY created_at DESC LIMIT 1', $wpdb->prefix . 'inqyra_crawl_runs', $agent_id ) );
            $doc_count = null;
            $result[] = array(
                'agent_id'              => $agent_id,
                'total_chunks'          => $total,
                'with_embedding'        => $with_embedding,
                'pending_embedding'     => $pending,
                'completion_percentage' => ( $total > 0 ? round( $with_embedding / $total * 100, 1 ) : 0 ),
                'by_source_type'        => $by_source,
                'last_crawl'            => ( $last_crawl ? (array) $last_crawl : null ),
                'total_documents'       => $doc_count,
            );
        }
        return $result;
    }

    /**
     * Get conversation statistics per agent for diagnostics.
     *
     * @return array Conversation stats (no message content).
     */
    private function get_diagnostics_conversations() {
        global $wpdb;
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Diagnostics page, no caching context.
        $agents = $wpdb->get_col( $wpdb->prepare( 'SELECT id FROM %i ORDER BY id ASC', $wpdb->prefix . 'inqyra_agents' ) );
        $result = array();
        foreach ( $agents as $agent_id ) {
            $agent_id = (int) $agent_id;
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix.
            $stats = $wpdb->get_row( $wpdb->prepare( 'SELECT COUNT(*) as total_conversations,
                            COALESCE(SUM(message_count), 0) as total_messages,
                            MIN(started_at) as earliest,
                            MAX(started_at) as latest,
                            CASE WHEN COUNT(*) > 0 THEN ROUND(SUM(message_count) / COUNT(*), 1) ELSE 0 END as avg_messages
                     FROM %i
                     WHERE agent_id = %d', $wpdb->prefix . 'inqyra_conversations', $agent_id ) );
            $result[] = array(
                'agent_id'                      => $agent_id,
                'total_conversations'           => (int) $stats->total_conversations,
                'total_messages'                => (int) $stats->total_messages,
                'earliest_conversation'         => $stats->earliest,
                'latest_conversation'           => $stats->latest,
                'avg_messages_per_conversation' => (float) $stats->avg_messages,
            );
        }
        return $result;
    }

    /**
     * Get recent errors for diagnostics.
     *
     * @return array Recent error info.
     */
    private function get_diagnostics_recent_errors() {
        global $wpdb;
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Diagnostics page, no caching context.
        $failed_crawls = $wpdb->get_results( $wpdb->prepare( 'SELECT agent_id, status, error_message, started_at, completed_at FROM %i WHERE error_message IS NOT NULL AND error_message != %s ORDER BY created_at DESC LIMIT 5', $wpdb->prefix . 'inqyra_crawl_runs', '' ), ARRAY_A );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Diagnostics page, no caching context.
        $failed_embeddings = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i WHERE embedding_model = %s', $wpdb->prefix . 'inqyra_knowledge_chunks', 'failed' ) );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Diagnostics page, no caching context.
        $stale_embeddings = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i WHERE needs_embedding = 1 AND updated_at < DATE_SUB(NOW(), INTERVAL 24 HOUR)', $wpdb->prefix . 'inqyra_knowledge_chunks' ) );
        $debug_log_lines = null;
        $debug_log_path = WP_CONTENT_DIR . '/debug.log';
        if ( is_readable( $debug_log_path ) ) {
            $file = new SplFileObject($debug_log_path, 'r');
            $file->seek( PHP_INT_MAX );
            $total_lines = $file->key();
            $start = max( 0, $total_lines - 50 );
            $lines = array();
            $file->seek( $start );
            while ( !$file->eof() ) {
                $line = $file->fgets();
                if ( trim( $line ) !== '' ) {
                    $lines[] = rtrim( $line );
                }
            }
            $debug_log_lines = $lines;
        }
        return array(
            'failed_crawl_runs' => $failed_crawls,
            'failed_embeddings' => $failed_embeddings,
            'stale_embeddings'  => $stale_embeddings,
            'debug_log_tail'    => $debug_log_lines,
        );
    }

    /**
     * Get Inqyra cron event status for diagnostics.
     *
     * @return array Cron status for each Inqyra hook.
     */
    private function get_diagnostics_cron_status() {
        $cron_hooks = array('inqyra_process_embeddings', 'inqyra_archive_conversations');
        $result = array();
        foreach ( $cron_hooks as $hook ) {
            $next = wp_next_scheduled( $hook );
            $result[$hook] = array(
                'scheduled' => false !== $next,
                'next_run'  => ( $next ? gmdate( 'c', $next ) : null ),
                'overdue'   => $next && $next < time(),
            );
        }
        return $result;
    }

    /**
     * AJAX handler for prompt builder chat.
     * Proxies messages to the user's configured AI provider for interactive prompt building.
     */
    public function ajax_prompt_builder_chat() {
        check_ajax_referer( 'inqyra_admin', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( [
                'message' => __( 'Permission denied.', 'inqyra' ),
            ] );
        }
        $provider_name = ( isset( $_POST['provider'] ) ? sanitize_text_field( wp_unslash( $_POST['provider'] ) ) : '' );
        $language = ( isset( $_POST['language'] ) ? sanitize_text_field( wp_unslash( $_POST['language'] ) ) : 'en' );
        $site_name = ( isset( $_POST['site_name'] ) ? sanitize_text_field( wp_unslash( $_POST['site_name'] ) ) : get_bloginfo( 'name' ) );
        $mode = ( isset( $_POST['mode'] ) ? sanitize_text_field( wp_unslash( $_POST['mode'] ) ) : 'chat' );
        $messages_json = ( isset( $_POST['messages'] ) ? sanitize_textarea_field( wp_unslash( $_POST['messages'] ) ) : '[]' );
        // Validate provider
        $valid_providers = [
            'claude',
            'openai',
            'gemini',
            'mistral',
            'deepseek'
        ];
        if ( !in_array( $provider_name, $valid_providers, true ) ) {
            wp_send_json_error( [
                'message' => __( 'Invalid provider.', 'inqyra' ),
            ] );
        }
        // Validate mode
        if ( !in_array( $mode, ['chat', 'generate'], true ) ) {
            wp_send_json_error( [
                'message' => __( 'Invalid mode.', 'inqyra' ),
            ] );
        }
        // Decode and sanitize messages
        $messages = json_decode( $messages_json, true );
        if ( !is_array( $messages ) ) {
            $messages = [];
        }
        $sanitized_messages = [];
        foreach ( $messages as $msg ) {
            if ( !isset( $msg['role'], $msg['content'] ) ) {
                continue;
            }
            $role = sanitize_text_field( $msg['role'] );
            if ( !in_array( $role, ['user', 'assistant'], true ) ) {
                continue;
            }
            $sanitized_messages[] = [
                'role'    => $role,
                'content' => sanitize_textarea_field( $msg['content'] ),
            ];
        }
        // Language name mapping
        $language_names = [
            'nl' => 'Dutch',
            'en' => 'English',
            'de' => 'German',
            'fr' => 'French',
            'es' => 'Spanish',
            'it' => 'Italian',
            'pt' => 'Portuguese',
            'pl' => 'Polish',
            'sv' => 'Swedish',
            'da' => 'Danish',
            'no' => 'Norwegian',
            'fi' => 'Finnish',
            'ja' => 'Japanese',
            'ko' => 'Korean',
            'zh' => 'Chinese',
            'ar' => 'Arabic',
            'tr' => 'Turkish',
            'ru' => 'Russian',
            'uk' => 'Ukrainian',
            'cs' => 'Czech',
            'ro' => 'Romanian',
            'hu' => 'Hungarian',
            'el' => 'Greek',
            'he' => 'Hebrew',
            'th' => 'Thai',
            'vi' => 'Vietnamese',
            'id' => 'Indonesian',
            'ms' => 'Malay',
            'hi' => 'Hindi',
        ];
        $language_name = $language_names[$language] ?? 'English';
        // Build meta system prompt
        if ( $mode === 'generate' ) {
            $system_prompt = "Based on the conversation below between a user and a prompt builder assistant, generate a comprehensive system prompt for an AI website chatbot. " . "The system prompt MUST be written entirely in {$language_name}.\n\n" . "Structure the prompt using markdown with ## section headers. Include ALL of the following sections where the user provided relevant information:\n\n" . "1. **Opening line**: One sentence defining who the chatbot is and what company/site it represents\n" . "2. **## Role**: What the chatbot does, how it helps visitors\n" . "3. **## About [Company]**: Background, founding, team, experience, specializations — as bullet points\n" . "4. **## Core values**: What makes the company unique — as bullet points\n" . "5. **## Services/Products**: Organized by category with sub-bullets for specifics. Use ### sub-headers per category if multiple categories exist\n" . "6. **## Clients**: Notable clients or sectors served\n" . "7. **## Contact details**: Address, phone, email — only if mentioned by the user\n" . "8. **## Response guidelines**: Numbered list — what to base answers on, how long answers should be, what to do when information is unknown, tone rules, when to ask follow-up questions\n" . "9. **## Example answers**: 2-3 short examples of ideal answers in the right tone and length\n" . "10. **## Restrictions**: What the chatbot must NOT do (e.g. no pricing, no medical advice, no speculation)\n" . "11. **## Formatting**: Any output formatting rules mentioned\n\n" . "Important rules:\n" . "- Only include sections for which the user actually provided information — skip empty sections\n" . "- Use bullet points and sub-bullets liberally for scannability\n" . "- Always include a guideline to only answer based on provided context/knowledge chunks and to not make things up\n" . "- Always include a guideline to refer to the contact page when information is unknown\n" . "- Keep the tone instructions specific (not just 'friendly' but describe HOW: use je/jij, informal but professional, etc.)\n\n" . "Output ONLY the system prompt. No explanations, no wrapping quotes, no preamble.";
        } else {
            $system_prompt = "You are an expert at writing detailed system prompts for AI website chatbots. You are helping a user create a comprehensive system prompt for their chatbot named \"{$site_name}\".\n\n" . "Conduct this conversation entirely in {$language_name}.\n\n" . "Your goal is to gather enough detail to produce a rich, structured system prompt with sections about the company, services, tone, restrictions and response guidelines.\n\n" . "Ask the user questions ONE AT A TIME. Cover these topics in order:\n" . "1. What their business/website is about — what do they do, what makes them unique, how long have they existed?\n" . "2. Their main services or products — ask them to list specifics, categories, notable projects\n" . "3. Their target audience and notable clients or sectors they serve\n" . "4. What tone the chatbot should use — ask for specifics: formal/informal, je/u, short or elaborate answers, with conviction or cautious?\n" . "5. What topics to AVOID or redirect (e.g. no pricing, no medical advice) and where to redirect (contact page URL?)\n" . "6. Any specific answer rules: maximum answer length, always refer to contact page when unsure, formatting rules for URLs, etc.\n" . "7. Contact details to include (address, phone, email) — only if they want the chatbot to share these\n\n" . "After each answer, acknowledge briefly and ask the next question. After you have covered enough topics (typically 5-8 questions), tell the user you have enough information and they can click the \"Generate Prompt\" button. Do NOT generate the prompt yourself during the chat.";
        }
        // Build messages array with system prompt
        if ( $mode === 'generate' ) {
            // For generate mode, fold conversation into system prompt as context.
            // This avoids the issue where the first message is 'assistant' (which
            // Claude's API rejects — it requires the first message to be 'user').
            $conversation_text = '';
            foreach ( $sanitized_messages as $msg ) {
                $label = ( $msg['role'] === 'user' ? 'User' : 'Assistant' );
                $conversation_text .= $label . ': ' . $msg['content'] . "\n\n";
            }
            $api_messages = [[
                'role'    => 'system',
                'content' => $system_prompt,
            ], [
                'role'    => 'user',
                'content' => "Here is the conversation:\n\n" . $conversation_text . "Generate the system prompt now.",
            ]];
        } elseif ( empty( $sanitized_messages ) ) {
            // Initial chat request: add a trigger message so the AI asks the first question
            $api_messages = [[
                'role'    => 'system',
                'content' => $system_prompt,
            ], [
                'role'    => 'user',
                'content' => "Hi, I'd like to create a system prompt for my chatbot.",
            ]];
        } else {
            // Ongoing chat: ensure messages start with 'user' for API compatibility
            $api_messages = [[
                'role'    => 'system',
                'content' => $system_prompt,
            ]];
            // Prepend the initial trigger that was used to start the conversation
            $api_messages[] = [
                'role'    => 'user',
                'content' => "Hi, I'd like to create a system prompt for my chatbot.",
            ];
            $api_messages = array_merge( $api_messages, $sanitized_messages );
        }
        // Cheap model selection per provider
        $cheap_models = [
            'claude'   => 'claude-haiku-4-5-20251001',
            'openai'   => 'gpt-4o-mini',
            'gemini'   => 'gemini-2.0-flash-lite',
            'mistral'  => 'mistral-small-latest',
            'deepseek' => 'deepseek-chat',
        ];
        $model = $cheap_models[$provider_name] ?? null;
        try {
            $provider = Inqyra_AI_Provider_Factory::create( $provider_name );
            if ( !$provider->is_configured() ) {
                wp_send_json_error( [
                    'message' => __( 'API key not configured for this provider.', 'inqyra' ),
                ] );
            }
            $max_tokens = ( $mode === 'generate' ? 4096 : 1024 );
            $response = $provider->chat( $api_messages, [
                'model'      => $model,
                'max_tokens' => $max_tokens,
            ] );
            wp_send_json_success( [
                'content' => $response['content'],
                'tokens'  => $response['tokens'],
            ] );
        } catch ( Exception $e ) {
            wp_send_json_error( [
                'message' => esc_html( $e->getMessage() ),
            ] );
        }
    }

    /**
     * Validate and filter crawl parts (HTML tags).
     *
     * Only allows safe HTML tags that make sense for content extraction.
     *
     * @param array $parts Array of HTML tag names.
     * @return array Filtered array of valid tag names.
     */
    private function validate_crawl_parts( array $parts ) : array {
        $dangerous_tags = [
            'script',
            'style',
            'iframe',
            'object',
            'embed',
            'form',
            'input',
            'textarea',
            'select',
            'button',
            'link',
            'meta',
            'noscript',
            'svg',
            'math'
        ];
        $validated = [];
        foreach ( $parts as $tag ) {
            $tag = strtolower( sanitize_text_field( $tag ) );
            // Must be a valid HTML tag name (letters and digits only, 1-20 chars)
            if ( !preg_match( '/^[a-z][a-z0-9]{0,19}$/', $tag ) ) {
                continue;
            }
            if ( in_array( $tag, $dangerous_tags, true ) ) {
                continue;
            }
            $validated[] = $tag;
        }
        $validated = array_unique( $validated );
        // Return at least default tags if nothing valid
        return ( !empty( $validated ) ? array_values( $validated ) : [
            'p',
            'h1',
            'h2',
            'h3'
        ] );
    }

    /**
     * Validate and filter post types.
     *
     * Only allows registered post types.
     *
     * @param array $types Array of post type names.
     * @return array Filtered array of valid post type names.
     */
    private function validate_post_types( array $types ) : array {
        $registered_types = get_post_types( [
            'public' => true,
        ], 'names' );
        $validated = [];
        foreach ( $types as $type ) {
            $type = sanitize_text_field( $type );
            if ( isset( $registered_types[$type] ) ) {
                $validated[] = $type;
            }
        }
        // Return at least default types if nothing valid
        return ( !empty( $validated ) ? $validated : ['post', 'page'] );
    }

    /**
     * AJAX handler for exporting statistics.
     */
    public function ajax_export_statistics() {
        check_ajax_referer( 'inqyra_admin', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( [
                'message' => __( 'Permission denied.', 'inqyra' ),
            ] );
        }
        global $wpdb;
        $agent_id = ( isset( $_POST['agent_id'] ) ? absint( wp_unslash( $_POST['agent_id'] ) ) : 0 );
        $days = ( isset( $_POST['days'] ) ? absint( wp_unslash( $_POST['days'] ) ) : 30 );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix.
        $stats = $wpdb->get_results( $wpdb->prepare(
            'SELECT
                DATE(c.started_at) as date,
                COUNT(DISTINCT c.id) as conversations,
                COUNT(m.id) as messages,
                COALESCE(SUM(m.tokens_used), 0) as tokens,
                COALESCE(SUM(m.cost_usd), 0) as cost
             FROM %i c
             LEFT JOIN %i m ON m.conversation_id = c.id
             WHERE c.agent_id = %d
             AND c.started_at >= DATE_SUB(NOW(), INTERVAL %d DAY)
             GROUP BY DATE(c.started_at)
             ORDER BY date DESC',
            $wpdb->prefix . 'inqyra_conversations',
            $wpdb->prefix . 'inqyra_messages',
            $agent_id,
            $days
        ), ARRAY_A );
        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- php://temp is an in-memory stream, not a filesystem operation.
        $output = fopen( 'php://temp', 'r+' );
        // Header row
        fputcsv( $output, [
            'Date',
            'Conversations',
            'Messages',
            'Tokens',
            'Cost (USD)'
        ] );
        foreach ( $stats as $row ) {
            fputcsv( $output, [
                $row['date'],
                $row['conversations'],
                $row['messages'],
                $row['tokens'],
                number_format( (float) $row['cost'], 6 )
            ] );
        }
        rewind( $output );
        $csv = stream_get_contents( $output );
        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Closing in-memory stream.
        fclose( $output );
        wp_send_json_success( [
            'data'     => $csv,
            'filename' => 'statistics-' . gmdate( 'Y-m-d' ) . '.csv',
            'type'     => 'text/csv',
        ] );
    }

    /**
     * AJAX handler for archiving a conversation.
     */
    public function ajax_archive_conversation() {
        check_ajax_referer( 'inqyra_admin', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( [
                'message' => __( 'Permission denied.', 'inqyra' ),
            ] );
        }
        global $wpdb;
        $conversation_id = ( isset( $_POST['conversation_id'] ) ? absint( wp_unslash( $_POST['conversation_id'] ) ) : 0 );
        if ( empty( $conversation_id ) ) {
            wp_send_json_error( [
                'message' => __( 'Invalid conversation ID.', 'inqyra' ),
            ] );
        }
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix.
        $result = $wpdb->update(
            $wpdb->prefix . 'inqyra_conversations',
            [
                'archived' => 1,
            ],
            [
                'id' => $conversation_id,
            ],
            ['%d'],
            ['%d']
        );
        if ( $result === false ) {
            wp_send_json_error( [
                'message' => __( 'Failed to archive conversation.', 'inqyra' ),
            ] );
        }
        wp_send_json_success( [
            'message' => __( 'Conversation archived.', 'inqyra' ),
        ] );
    }

    /**
     * AJAX handler for deleting a conversation.
     */
    public function ajax_delete_conversation() {
        check_ajax_referer( 'inqyra_admin', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( [
                'message' => __( 'Permission denied.', 'inqyra' ),
            ] );
        }
        global $wpdb;
        $conversation_id = ( isset( $_POST['conversation_id'] ) ? absint( wp_unslash( $_POST['conversation_id'] ) ) : 0 );
        if ( empty( $conversation_id ) ) {
            wp_send_json_error( [
                'message' => __( 'Invalid conversation ID.', 'inqyra' ),
            ] );
        }
        // Delete messages first
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix.
        $wpdb->delete( $wpdb->prefix . 'inqyra_messages', [
            'conversation_id' => $conversation_id,
        ], ['%d'] );
        // Delete conversation
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix.
        $result = $wpdb->delete( $wpdb->prefix . 'inqyra_conversations', [
            'id' => $conversation_id,
        ], ['%d'] );
        if ( $result === false ) {
            wp_send_json_error( [
                'message' => __( 'Failed to delete conversation.', 'inqyra' ),
            ] );
        }
        wp_send_json_success( [
            'message' => __( 'Conversation deleted.', 'inqyra' ),
        ] );
    }

    /**
     * AJAX handler for exporting synonyms.
     */
    public function ajax_export_synonyms() {
        check_ajax_referer( 'inqyra_admin', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( [
                'message' => __( 'Permission denied.', 'inqyra' ),
            ] );
        }
        $agent_id = ( isset( $_POST['agent_id'] ) ? absint( wp_unslash( $_POST['agent_id'] ) ) : 0 );
        if ( empty( $agent_id ) ) {
            wp_send_json_error( [
                'message' => __( 'Invalid agent ID.', 'inqyra' ),
            ] );
        }
        $synonym_service = new Inqyra_Synonym_Service();
        $synonyms = $synonym_service->export_synonyms( $agent_id );
        wp_send_json_success( [
            'data'     => wp_json_encode( $synonyms, JSON_PRETTY_PRINT ),
            'filename' => 'synonyms-' . gmdate( 'Y-m-d' ) . '.json',
            'type'     => 'application/json',
        ] );
    }

    /**
     * AJAX handler for importing synonyms.
     */
    public function ajax_import_synonyms() {
        check_ajax_referer( 'inqyra_admin', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( [
                'message' => __( 'Permission denied.', 'inqyra' ),
            ] );
        }
        $agent_id = ( isset( $_POST['agent_id'] ) ? absint( wp_unslash( $_POST['agent_id'] ) ) : 0 );
        $data = ( isset( $_POST['data'] ) ? sanitize_textarea_field( wp_unslash( $_POST['data'] ) ) : '' );
        if ( empty( $agent_id ) || empty( $data ) ) {
            wp_send_json_error( [
                'message' => __( 'Invalid import data.', 'inqyra' ),
            ] );
        }
        // Decode JSON, then sanitize each entry individually below.
        $synonyms = json_decode( $data, true );
        if ( json_last_error() !== JSON_ERROR_NONE || !is_array( $synonyms ) ) {
            wp_send_json_error( [
                'message' => __( 'Invalid JSON format.', 'inqyra' ),
            ] );
        }
        // Sanitize each synonym entry.
        $sanitized_synonyms = [];
        foreach ( $synonyms as $entry ) {
            if ( !is_array( $entry ) ) {
                continue;
            }
            $sanitized_entry = [];
            if ( isset( $entry['term'] ) ) {
                $sanitized_entry['term'] = sanitize_text_field( $entry['term'] );
            }
            if ( isset( $entry['synonyms'] ) ) {
                if ( is_array( $entry['synonyms'] ) ) {
                    $sanitized_entry['synonyms'] = array_map( 'sanitize_text_field', $entry['synonyms'] );
                } else {
                    $sanitized_entry['synonyms'] = sanitize_text_field( $entry['synonyms'] );
                }
            }
            if ( isset( $entry['bidirectional'] ) ) {
                $sanitized_entry['bidirectional'] = (bool) $entry['bidirectional'];
            }
            if ( isset( $entry['case_sensitive'] ) ) {
                $sanitized_entry['case_sensitive'] = (bool) $entry['case_sensitive'];
            }
            $sanitized_synonyms[] = $sanitized_entry;
        }
        $synonym_service = new Inqyra_Synonym_Service();
        $imported = $synonym_service->import_synonyms( $agent_id, $sanitized_synonyms );
        /* translators: %d: number of synonyms imported */
        $import_msg = sprintf( __( '%d synonyms imported.', 'inqyra' ), $imported );
        wp_send_json_success( [
            'message' => $import_msg,
            'count'   => $imported,
        ] );
    }

}
