<?php

/**
 * The core plugin class.
 *
 * @package Inqyra
 */
// If this file is called directly, abort.
if ( !defined( 'WPINC' ) ) {
    die;
}
/**
 * Main plugin class.
 */
class Inqyra {
    /**
     * The loader responsible for maintaining and registering all hooks.
     *
     * @var Inqyra_Loader
     */
    protected $loader;

    /**
     * The unique identifier of this plugin.
     *
     * @var string
     */
    protected $plugin_name;

    /**
     * The current version of the plugin.
     *
     * @var string
     */
    protected $version;

    /**
     * Initialize the class and set its properties.
     */
    public function __construct() {
        $this->version = INQYRA_VERSION;
        $this->plugin_name = 'inqyra';
        $this->load_dependencies();
        $this->set_locale();
        $this->define_admin_hooks();
        $this->define_public_hooks();
        $this->define_cron_hooks();
    }

    /**
     * Check and run database upgrades if needed.
     */
    public function maybe_upgrade_database() {
        global $wpdb;
        $table = $wpdb->prefix . 'inqyra_agents';
        // Check if any required column is missing, if so run upgrade
        $required_columns = [
            'header_icon_id',
            'show_icon_in_messages',
            'send_button_icon_id',
            'send_button_text',
            'text_color'
        ];
        $needs_upgrade = false;
        foreach ( $required_columns as $column ) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- One-time upgrade check, no caching needed.
            $column_exists = $wpdb->get_var( $wpdb->prepare(
                "SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND COLUMN_NAME = %s",
                DB_NAME,
                $table,
                $column
            ) );
            if ( !$column_exists ) {
                $needs_upgrade = true;
                break;
            }
        }
        if ( $needs_upgrade ) {
            $this->upgrade_to_1_0_1();
        }
        // Ensure all tables exist (for upgrades from older versions)
        $this->ensure_tables_exist();
        // Run activator's upgrade checks for index optimizations
        require_once INQYRA_PATH . 'includes/class-inqyra-activator.php';
        Inqyra_Activator::maybe_upgrade();
    }

    /**
     * Ensure all required tables exist.
     * Creates missing tables for upgrades from older versions.
     */
    private function ensure_tables_exist() {
        return;
        global $wpdb;
        $charset_collate = $wpdb->get_charset_collate();
        // Documents table (premium)
        $table_documents = $wpdb->prefix . 'inqyra_documents';
        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix, $charset_collate from get_charset_collate().
        $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            filename varchar(255) NOT NULL,\n            original_filename varchar(255) NOT NULL,\n            file_path varchar(500) NOT NULL,\n            file_type enum('pdf','docx','doc') NOT NULL,\n            file_size bigint(20) unsigned,\n            status enum('pending','processing','completed','failed') DEFAULT 'pending',\n            error_message text,\n            chunk_count int(11) DEFAULT 0,\n            uploaded_by bigint(20) unsigned,\n            created_at timestamp DEFAULT CURRENT_TIMESTAMP,\n            updated_at timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n            PRIMARY KEY (id),\n            KEY idx_agent_status (agent_id, status)\n        ) {$charset_collate};", $table_documents ) );
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        // Synonyms table (premium)
        // Note: term is varchar(191) to stay within 767 byte key limit for utf8mb4
        $table_synonyms = $wpdb->prefix . 'inqyra_synonyms';
        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix, $charset_collate from get_charset_collate().
        $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_synonyms ) );
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        // Webhooks table (premium)
        $table_webhooks = $wpdb->prefix . 'inqyra_webhooks';
        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix, $charset_collate from get_charset_collate().
        $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            name varchar(255) NOT NULL,\n            url varchar(2048) NOT NULL,\n            secret varchar(64) NOT NULL,\n            events longtext NOT NULL,\n            headers longtext,\n            enabled tinyint(1) DEFAULT 1,\n            created_at timestamp DEFAULT CURRENT_TIMESTAMP,\n            updated_at timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n            PRIMARY KEY (id),\n            KEY idx_agent_enabled (agent_id, enabled)\n        ) {$charset_collate};", $table_webhooks ) );
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        // Webhook logs table (premium)
        $table_webhook_logs = $wpdb->prefix . 'inqyra_webhook_logs';
        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix, $charset_collate from get_charset_collate().
        $wpdb->query( $wpdb->prepare( "CREATE TABLE IF NOT EXISTS %i (\n            id bigint(20) unsigned NOT NULL AUTO_INCREMENT,\n            webhook_id bigint(20) unsigned NOT NULL,\n            event_type varchar(50) NOT NULL,\n            payload longtext,\n            response_status int(11),\n            response_body text,\n            success tinyint(1) DEFAULT 0,\n            retry_count int(11) DEFAULT 0,\n            error_message text,\n            sent_at timestamp DEFAULT CURRENT_TIMESTAMP,\n            completed_at timestamp NULL,\n            PRIMARY KEY (id),\n            KEY idx_webhook (webhook_id),\n            KEY idx_sent_at (sent_at)\n        ) {$charset_collate};", $table_webhook_logs ) );
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
    }

    /**
     * Database upgrade to version 1.0.1.
     * Adds header_icon_id column and removes old icon columns.
     */
    private function upgrade_to_1_0_1() {
        global $wpdb;
        $table = $wpdb->prefix . 'inqyra_agents';
        // Security: Validate table name format
        if ( !preg_match( '/^[a-zA-Z0-9_]+$/', $table ) ) {
            return;
        }
        // Columns to add with their definitions (column names are hardcoded, safe)
        $columns_to_add = [
            'text_color'            => "varchar(7) DEFAULT '#ffffff' AFTER primary_color",
            'header_icon_id'        => 'bigint(20) unsigned DEFAULT 0 AFTER powered_by_text',
            'show_icon_in_messages' => 'tinyint(1) DEFAULT 0 AFTER header_icon_id',
            'send_button_icon_id'   => 'bigint(20) unsigned DEFAULT 0 AFTER show_icon_in_messages',
            'send_button_text'      => "varchar(100) DEFAULT '' AFTER send_button_icon_id",
        ];
        foreach ( $columns_to_add as $column => $definition ) {
            // Column names are hardcoded above, but validate anyway
            if ( !preg_match( '/^[a-zA-Z_][a-zA-Z0-9_]*$/', $column ) ) {
                continue;
            }
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix, validated with regex.
            $column_exists = $wpdb->get_results( $wpdb->prepare( 'SHOW COLUMNS FROM %i LIKE %s', $table, $column ) );
            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
            if ( empty( $column_exists ) ) {
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Column definition contains SQL keywords (DEFAULT, AFTER) that cannot be parameterized; values are hardcoded above.
                $wpdb->query( $wpdb->prepare( 'ALTER TABLE %i ADD COLUMN %i ' . $definition, $table, $column ) );
            }
        }
        // Remove old icon columns if they exist (hardcoded column names)
        $old_columns = ['header_icon_type', 'header_icon_library', 'header_icon_url'];
        foreach ( $old_columns as $col ) {
            // Validate column name contains only safe characters
            if ( !preg_match( '/^[a-zA-Z_][a-zA-Z0-9_]*$/', $col ) ) {
                continue;
            }
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix, column name validated with regex.
            $col_exists = $wpdb->get_results( $wpdb->prepare( 'SHOW COLUMNS FROM %i LIKE %s', $table, $col ) );
            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
            if ( !empty( $col_exists ) ) {
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Hardcoded column name, table name from $wpdb->prefix.
                $wpdb->query( $wpdb->prepare( 'ALTER TABLE %i DROP COLUMN %i', $table, $col ) );
            }
        }
    }

    /**
     * Load required dependencies.
     */
    private function load_dependencies() {
        // Loader class
        require_once INQYRA_PATH . 'includes/class-inqyra-loader.php';
        // i18n class
        require_once INQYRA_PATH . 'includes/class-inqyra-i18n.php';
        // Encryption utility
        require_once INQYRA_PATH . 'includes/class-inqyra-encryption.php';
        // Default prompts and indexing labels
        require_once INQYRA_PATH . 'includes/class-inqyra-default-prompts.php';
        require_once INQYRA_PATH . 'includes/class-inqyra-indexing-labels.php';
        // Services (free)
        require_once INQYRA_PATH . 'services/class-inqyra-debug-logger.php';
        require_once INQYRA_PATH . 'services/class-inqyra-crawler.php';
        require_once INQYRA_PATH . 'services/class-inqyra-embedder.php';
        require_once INQYRA_PATH . 'services/class-inqyra-vector-store.php';
        require_once INQYRA_PATH . 'services/class-inqyra-rag-engine.php';
        require_once INQYRA_PATH . 'services/class-inqyra-query-rewriter.php';
        // AI Providers (load before chat service)
        $this->load_file_if_exists( 'services/providers/interface-inqyra-ai-provider.php' );
        $this->load_file_if_exists( 'services/providers/class-inqyra-ai-provider-abstract.php' );
        $this->load_file_if_exists( 'services/providers/class-inqyra-claude-provider.php' );
        $this->load_file_if_exists( 'services/providers/class-inqyra-openai-provider.php' );
        $this->load_file_if_exists( 'services/providers/class-inqyra-gemini-provider.php' );
        $this->load_file_if_exists( 'services/providers/class-inqyra-mistral-provider.php' );
        $this->load_file_if_exists( 'services/providers/class-inqyra-deepseek-provider.php' );
        $this->load_file_if_exists( 'services/providers/class-inqyra-ai-provider-factory.php' );
        // Chat service (depends on providers)
        $this->load_file_if_exists( 'services/class-inqyra-chat-service.php' );
        // Admin class
        require_once INQYRA_PATH . 'admin/class-inqyra-admin.php';
        // Public class
        $this->load_file_if_exists( 'public/class-inqyra-public.php' );
        // REST API
        $this->load_file_if_exists( 'api/class-inqyra-rest-api.php' );
        $this->loader = new Inqyra_Loader();
    }

    /**
     * Load a file if it exists, log error if not.
     *
     * @param string $relative_path Path relative to plugin directory.
     */
    private function load_file_if_exists( string $relative_path ) : void {
        $full_path = INQYRA_PATH . $relative_path;
        if ( file_exists( $full_path ) ) {
            require_once $full_path;
        } else {
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Critical: missing required plugin file.
            error_log( 'Inqyra: Missing required file: ' . $relative_path );
        }
    }

    /**
     * Define the locale for internationalization.
     *
     * Since WordPress 4.6, text domains are automatically loaded from
     * wp-content/languages/plugins/ so manual loading is no longer needed.
     */
    private function set_locale() {
        // No-op: WordPress 4.6+ auto-loads the text domain.
    }

    /**
     * Register admin hooks.
     */
    private function define_admin_hooks() {
        // Database upgrades
        $this->loader->add_action( 'admin_init', $this, 'maybe_upgrade_database' );
        $plugin_admin = new Inqyra_Admin($this->get_plugin_name(), $this->get_version());
        // Admin menu
        $this->loader->add_action( 'admin_menu', $plugin_admin, 'add_admin_menu' );
        // Admin scripts and styles
        $this->loader->add_action( 'admin_enqueue_scripts', $plugin_admin, 'enqueue_styles' );
        $this->loader->add_action( 'admin_enqueue_scripts', $plugin_admin, 'enqueue_scripts' );
        $this->loader->add_action( 'admin_enqueue_scripts', $plugin_admin, 'enqueue_post_editor_scripts' );
        // Plugin settings link
        $this->loader->add_filter( 'plugin_action_links_' . INQYRA_BASENAME, $plugin_admin, 'add_settings_link' );
        // AJAX handlers (free)
        $this->loader->add_action( 'wp_ajax_inqyra_save_settings', $plugin_admin, 'ajax_save_settings' );
        $this->loader->add_action( 'wp_ajax_inqyra_save_agent', $plugin_admin, 'ajax_save_agent' );
        $this->loader->add_action( 'wp_ajax_inqyra_trigger_crawl', $plugin_admin, 'ajax_trigger_crawl' );
        $this->loader->add_action( 'wp_ajax_inqyra_get_crawl_status', $plugin_admin, 'ajax_get_crawl_status' );
        $this->loader->add_action( 'wp_ajax_inqyra_get_embedding_stats', $plugin_admin, 'ajax_get_embedding_stats' );
        $this->loader->add_action( 'wp_ajax_inqyra_get_chunks_for_embedding', $plugin_admin, 'ajax_get_chunks_for_embedding' );
        $this->loader->add_action( 'wp_ajax_inqyra_save_embedding', $plugin_admin, 'ajax_save_embedding' );
        $this->loader->add_action( 'wp_ajax_inqyra_mark_embedding_failed', $plugin_admin, 'ajax_mark_embedding_failed' );
        $this->loader->add_action( 'wp_ajax_inqyra_create_agent', $plugin_admin, 'ajax_create_agent' );
        $this->loader->add_action( 'wp_ajax_inqyra_search_pages', $plugin_admin, 'ajax_search_pages' );
        $this->loader->add_action( 'wp_ajax_inqyra_get_page_titles', $plugin_admin, 'ajax_get_page_titles' );
        $this->loader->add_action( 'wp_ajax_inqyra_dismiss_recrawl_notice', $plugin_admin, 'ajax_dismiss_recrawl_notice' );
        $this->loader->add_action( 'wp_ajax_inqyra_crawl_single_post', $plugin_admin, 'ajax_crawl_single_post' );
        $this->loader->add_action( 'wp_ajax_inqyra_export_conversations', $plugin_admin, 'ajax_export_conversations' );
        $this->loader->add_action( 'wp_ajax_inqyra_export_diagnostics', $plugin_admin, 'ajax_export_diagnostics' );
        // Conversation management AJAX handlers
        $this->loader->add_action( 'wp_ajax_inqyra_archive_conversation', $plugin_admin, 'ajax_archive_conversation' );
        $this->loader->add_action( 'wp_ajax_inqyra_delete_conversation', $plugin_admin, 'ajax_delete_conversation' );
        // Model refresh AJAX handler
        $this->loader->add_action( 'wp_ajax_inqyra_refresh_models', $plugin_admin, 'ajax_refresh_models' );
        // Setup Wizard AJAX handlers
        $this->loader->add_action( 'wp_ajax_inqyra_validate_api_key', $plugin_admin, 'ajax_validate_api_key' );
        $this->loader->add_action( 'wp_ajax_inqyra_delete_api_key', $plugin_admin, 'ajax_delete_api_key' );
        $this->loader->add_action( 'wp_ajax_inqyra_save_wizard_state', $plugin_admin, 'ajax_save_wizard_state' );
        $this->loader->add_action( 'wp_ajax_inqyra_get_agent', $plugin_admin, 'ajax_get_agent' );
        $this->loader->add_action( 'wp_ajax_inqyra_complete_wizard', $plugin_admin, 'ajax_complete_wizard' );
        $this->loader->add_action( 'wp_ajax_inqyra_prompt_builder_chat', $plugin_admin, 'ajax_prompt_builder_chat' );
        // Setup Wizard redirect
        $this->loader->add_action( 'admin_init', $plugin_admin, 'maybe_redirect_to_wizard' );
        // Admin notices
        $this->loader->add_action( 'admin_notices', $plugin_admin, 'admin_notices' );
    }

    /**
     * Register public-facing hooks.
     */
    private function define_public_hooks() {
        // Register REST API routes if class exists
        if ( class_exists( 'Inqyra_REST_API' ) ) {
            $rest_api = new Inqyra_REST_API();
            $this->loader->add_action( 'rest_api_init', $rest_api, 'register_routes' );
        }
        // Only load widget hooks if the public class exists
        if ( !class_exists( 'Inqyra_Public' ) ) {
            return;
        }
        $plugin_public = new Inqyra_Public($this->get_plugin_name(), $this->get_version());
        // Enqueue scripts and styles
        $this->loader->add_action( 'wp_enqueue_scripts', $plugin_public, 'enqueue_styles' );
        $this->loader->add_action( 'wp_enqueue_scripts', $plugin_public, 'enqueue_scripts' );
        // Render widget
        $this->loader->add_action( 'wp_footer', $plugin_public, 'render_widget' );
    }

    /**
     * Register cron hooks.
     */
    private function define_cron_hooks() {
        // Custom cron schedules
        $this->loader->add_filter( 'cron_schedules', $this, 'add_cron_schedules' );
        // Free cron hooks
        $this->loader->add_action( 'inqyra_process_embeddings', $this, 'process_embeddings' );
        $this->loader->add_action( 'inqyra_archive_conversations', $this, 'archive_conversations' );
        $this->loader->add_action( 'inqyra_run_crawl', $this, 'run_crawl' );
        $this->loader->add_action( 'inqyra_auto_recrawl', $this, 'auto_recrawl' );
        // Content change detection for auto re-crawl
        $this->loader->add_action(
            'save_post',
            $this,
            'on_content_changed',
            10,
            2
        );
    }

    /**
     * Add custom cron schedules.
     *
     * @param array $schedules Existing schedules.
     * @return array Modified schedules.
     */
    public function add_cron_schedules( $schedules ) {
        $schedules['every_minute'] = [
            'interval' => 60,
            'display'  => __( 'Every Minute', 'inqyra' ),
        ];
        $schedules['every_fifteen_minutes'] = [
            'interval' => 900,
            'display'  => __( 'Every 15 Minutes', 'inqyra' ),
        ];
        return $schedules;
    }

    /**
     * Process pending embeddings.
     */
    public function process_embeddings() {
        // Embeddings are created in the browser using Transformers.js.
        // This cron hook is kept as a placeholder for future server-side
        // embedding support (e.g. via TransformersPHP or an API).
        // Currently, embeddings must be processed from the admin dashboard.
    }

    /**
     * Handle content changes for auto re-index.
     *
     * Crawls the changed post immediately (the admin is online).
     * Sets a transient so that embeddings are auto-processed on next admin page load.
     *
     * @param int      $post_id Post ID.
     * @param \WP_Post $post    Post object.
     */
    public function on_content_changed( $post_id, $post ) {
        // Skip revisions and autosaves
        if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) {
            return;
        }
        global $wpdb;
        // Get the active agent
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Real-time check needed on every post save.
        $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 || empty( $agent->auto_recrawl_enabled ) ) {
            return;
        }
        // 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 ) ) {
            return;
        }
        // Queue the post for re-crawl via cron. We cannot crawl synchronously
        // during save_post because the crawler fetches the front-end HTML via
        // wp_remote_get, which would deadlock when only one PHP worker is available.
        $option_key = 'inqyra_recrawl_posts_' . $agent->id;
        $pending_ids = get_option( $option_key, [] );
        if ( !in_array( $post_id, $pending_ids, true ) ) {
            $pending_ids[] = $post_id;
            update_option( $option_key, $pending_ids, false );
        }
        // Schedule re-crawl shortly after save completes
        if ( !wp_next_scheduled( 'inqyra_auto_recrawl' ) ) {
            wp_schedule_single_event( time() + 10, 'inqyra_auto_recrawl' );
        }
    }

    /**
     * Auto re-crawl cron handler. Processes queued post IDs from on_content_changed.
     */
    public function auto_recrawl() {
        global $wpdb;
        // Find agents with auto-recrawl enabled
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Cron job, no caching context.
        $agents = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE active = 1 AND auto_recrawl_enabled = 1', $wpdb->prefix . 'inqyra_agents' ) );
        foreach ( $agents as $agent ) {
            $option_key = 'inqyra_recrawl_posts_' . $agent->id;
            $post_ids = get_option( $option_key, [] );
            if ( empty( $post_ids ) ) {
                continue;
            }
            // Clear the queue before processing
            delete_option( $option_key );
            // Re-crawl only the changed posts
            $crawler = new Inqyra_Crawler();
            $result = $crawler->crawl_posts( $agent->id, $post_ids );
            $chunks_created = $result['chunks_created'] ?? 0;
            $this->log( 'Auto re-crawl (cron): updated ' . count( $post_ids ) . ' post(s) for agent #' . $agent->id . ' (' . ($result['posts_processed'] ?? 0) . ' processed, ' . $chunks_created . ' chunks created)' );
            // Mark pending embeddings
            if ( $chunks_created > 0 ) {
                $pending = (int) get_transient( 'inqyra_pending_embeddings_' . $agent->id );
                set_transient( 'inqyra_pending_embeddings_' . $agent->id, $pending + $chunks_created, DAY_IN_SECONDS );
            }
        }
    }

    /**
     * Log a message.
     *
     * @param string $message The message to log.
     */
    private function log( $message ) {
        if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Gated behind WP_DEBUG.
            error_log( '[Inqyra] ' . $message );
        }
    }

    /**
     * Archive old conversations.
     */
    public function archive_conversations() {
        global $wpdb;
        // Free: delete conversations older than 30 days.
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Cron job, no caching context.
        $agents = $wpdb->get_results( $wpdb->prepare( 'SELECT id FROM %i WHERE active = 1', $wpdb->prefix . 'inqyra_agents' ) );
        foreach ( $agents as $agent ) {
            $this->delete_old_conversations( $agent->id, 30 );
        }
    }

    /**
     * Delete conversations older than a given number of days.
     *
     * @param int $agent_id   The agent ID.
     * @param int $older_than Days threshold.
     */
    private function delete_old_conversations( $agent_id, $older_than ) {
        global $wpdb;
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Cron job cleanup query.
        $conversation_ids = $wpdb->get_col( $wpdb->prepare(
            'SELECT id FROM %i
             WHERE agent_id = %d
             AND started_at < DATE_SUB(NOW(), INTERVAL %d DAY)',
            $wpdb->prefix . 'inqyra_conversations',
            $agent_id,
            $older_than
        ) );
        if ( !empty( $conversation_ids ) ) {
            $ids_placeholder = implode( ',', array_fill( 0, count( $conversation_ids ), '%d' ) );
            // Delete messages first.
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Dynamic placeholders built safely with array_fill.
            $wpdb->query( $wpdb->prepare( 'DELETE FROM %i
                 WHERE conversation_id IN (' . $ids_placeholder . ')', $wpdb->prefix . 'inqyra_messages', ...$conversation_ids ) );
            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, PluginCheck.Security.DirectDB.UnescapedDBParameter
            // Delete conversations.
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Dynamic placeholders built safely with array_fill.
            $wpdb->query( $wpdb->prepare( 'DELETE FROM %i
                 WHERE id IN (' . $ids_placeholder . ')', $wpdb->prefix . 'inqyra_conversations', ...$conversation_ids ) );
            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, PluginCheck.Security.DirectDB.UnescapedDBParameter
        }
    }

    /**
     * Run a crawl for an agent.
     *
     * @param int $run_id The crawl run ID.
     */
    public function run_crawl( $run_id ) {
        global $wpdb;
        // Get the run to find the agent ID
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Cron job, no caching context.
        $run = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE id = %d', $wpdb->prefix . 'inqyra_crawl_runs', $run_id ) );
        if ( !$run ) {
            return;
        }
        // Run the crawler with the existing run_id
        $crawler = new Inqyra_Crawler();
        $crawler->crawl_agent( $run->agent_id, $run_id );
    }

    /**
     * Process an uploaded document.
     *
     * @param int $document_id The document ID.
     */
    public function process_document( $document_id ) {
        $processor = new Inqyra_Document_Processor();
        $processor->process( $document_id );
    }

    /**
     * Dispatch a webhook (called by WP-Cron).
     *
     * @param int    $webhook_id The webhook ID.
     * @param string $event      Event type.
     * @param array  $data       Event data.
     */
    public function dispatch_webhook( $webhook_id, $event, $data ) {
        if ( !class_exists( 'Inqyra_Webhook_Service' ) ) {
            require_once INQYRA_PATH . 'premium/class-inqyra-webhook-service.php';
        }
        $service = new Inqyra_Webhook_Service();
        $service->dispatch_webhook( $webhook_id, $event, $data );
    }

    /**
     * Retry a failed webhook (called by WP-Cron).
     *
     * @param int    $webhook_id The webhook ID.
     * @param string $event      Event type.
     * @param array  $data       Event data.
     * @param int    $attempt    Current retry attempt.
     * @param int    $log_id     Original log entry ID.
     */
    public function retry_webhook(
        $webhook_id,
        $event,
        $data,
        $attempt,
        $log_id
    ) {
        if ( !class_exists( 'Inqyra_Webhook_Service' ) ) {
            require_once INQYRA_PATH . 'premium/class-inqyra-webhook-service.php';
        }
        $service = new Inqyra_Webhook_Service();
        $service->retry_webhook(
            $webhook_id,
            $event,
            $data,
            $attempt,
            $log_id
        );
    }

    /**
     * Check for ended conversations and fire webhooks.
     *
     * Finds conversations with no new messages for 30+ minutes
     * that haven't had their ended webhook sent yet.
     */
    public function check_ended_conversations() {
        global $wpdb;
        if ( !class_exists( 'Inqyra_Webhook_Service' ) ) {
            require_once INQYRA_PATH . 'premium/class-inqyra-webhook-service.php';
        }
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Cron job, no caching context.
        $conversations = $wpdb->get_results( $wpdb->prepare( 'SELECT c.*, a.name as agent_name FROM %i c JOIN %i a ON a.id = c.agent_id WHERE c.ended_webhook_sent = 0 AND c.message_count > 0 AND c.last_message_at IS NOT NULL AND c.last_message_at < DATE_SUB(NOW(), INTERVAL 30 MINUTE)', $wpdb->prefix . 'inqyra_conversations', $wpdb->prefix . 'inqyra_agents' ) );
        if ( empty( $conversations ) ) {
            return;
        }
        $webhook_service = new Inqyra_Webhook_Service();
        foreach ( $conversations as $conversation ) {
            // Calculate duration
            $started = strtotime( $conversation->started_at );
            $ended = strtotime( $conversation->last_message_at );
            $duration = max( 0, $ended - $started );
            // Get lead data if captured
            $lead_data = [];
            if ( $conversation->lead_captured ) {
                if ( !class_exists( 'Inqyra_Lead_Extractor' ) ) {
                    require_once INQYRA_PATH . 'premium/class-inqyra-lead-extractor.php';
                }
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Cron job, no caching context.
                $messages = $wpdb->get_results( $wpdb->prepare( 'SELECT role, content FROM %i
                     WHERE conversation_id = %d ORDER BY created_at ASC', $wpdb->prefix . 'inqyra_messages', $conversation->id ) );
                $extractor = new Inqyra_Lead_Extractor();
                $lead_data = $extractor->extract_from_messages( $messages );
            }
            // Build conversation transcript
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Cron job, no caching context.
            $messages = $wpdb->get_results( $wpdb->prepare( 'SELECT role, content FROM %i
                 WHERE conversation_id = %d ORDER BY created_at ASC', $wpdb->prefix . 'inqyra_messages', $conversation->id ) );
            $transcript_lines = [];
            foreach ( $messages as $msg ) {
                $label = ( $msg->role === 'user' ? 'Visitor' : 'Bot' );
                $transcript_lines[] = $label . ': ' . $msg->content;
            }
            $data = [
                'conversation_id'         => (int) $conversation->id,
                'session_id'              => $conversation->session_id,
                'duration_seconds'        => $duration,
                'message_count'           => (int) $conversation->message_count,
                'total_tokens'            => (int) $conversation->total_tokens,
                'source_url'              => ( $conversation->referrer_url ?: '' ),
                'conversation_transcript' => implode( "\n", $transcript_lines ),
            ];
            if ( !empty( $lead_data ) ) {
                $data['lead_data'] = $lead_data;
            }
            $webhook_service->trigger( (int) $conversation->agent_id, 'conversation.ended', $data );
            // Mark as sent
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Cron job, no caching context.
            $wpdb->update(
                $wpdb->prefix . 'inqyra_conversations',
                [
                    'ended_webhook_sent' => 1,
                ],
                [
                    'id' => $conversation->id,
                ],
                ['%d'],
                ['%d']
            );
        }
    }

    /**
     * Clean old webhook logs.
     */
    public function clean_webhook_logs() {
        if ( !class_exists( 'Inqyra_Webhook_Service' ) ) {
            require_once INQYRA_PATH . 'premium/class-inqyra-webhook-service.php';
        }
        $service = new Inqyra_Webhook_Service();
        $service->clean_old_logs( 30 );
    }

    /**
     * Run the loader to execute all hooks.
     */
    public function run() {
        $this->loader->run();
    }

    /**
     * Get the plugin name.
     *
     * @return string The plugin name.
     */
    public function get_plugin_name() {
        return $this->plugin_name;
    }

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

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

}
