<?php
if (!defined('ABSPATH')) {
    exit;
}

/**
 * Service for managing vector indexing, database lookups, and AI knowledge cataloging.
 */
class EEP_Chatbot_Knowledge_Service {

    /**
     * @var string
     */
    private $post_type;

    /**
     * @var string
     */
    private $table_name;

    /**
     * @var mixed EEP_Chatbot_Service (for API requests and configs)
     */
    private $chatbot_service;

    /**
     * @var mixed EEP_Chatbot_Ranking_Service (for keyword extraction)
     */
    private $ranking_service;

    /**
     * @var EEP_Event_Meta_Service
     */
    private $event_meta;

    /**
     * @var mixed EEP_Runtime_Log_Service
     */
    private $logger;

    /**
     * Constructor.
     */
    public function __construct($post_type, $table_name, $chatbot_service, $ranking_service, $event_meta, $logger) {
        $this->post_type = sanitize_key((string) $post_type);
        $this->table_name = eep_sanitize_text($table_name);
        $this->chatbot_service = $chatbot_service;
        $this->ranking_service = $ranking_service;
        $this->event_meta = $event_meta;
        $this->logger = $logger;
    }

    /**
     * Retrieves the fully resolved database table name for embeddings.
     */
    private function get_table_name() {
        global $wpdb;
        return $wpdb->prefix . ltrim($this->table_name, $wpdb->prefix);
    }

    /**
     * Gets all public post types available for indexing.
     */
    public function get_chatbot_available_post_types() {
        $objects = get_post_types(['show_ui' => true], 'objects');
        if (!is_array($objects)) {
            $objects = [];
        }

        $excluded = [
            'attachment', 'revision', 'nav_menu_item', 'custom_css', 'customize_changeset',
            'oembed_cache', 'user_request', 'wp_block', 'wp_template', 'wp_template_part',
            'wp_navigation', 'wp_global_styles', 'wp_font_family', 'wp_font_face',
        ];

        $available = [];
        foreach ($objects as $post_type => $post_type_obj) {
            $slug = sanitize_key((string) $post_type);
            if ($slug === '' || in_array($slug, $excluded, true) || strpos($slug, 'wp_') === 0) {
                continue;
            }
            if (!$post_type_obj instanceof WP_Post_Type) {
                continue;
            }
            if (!$post_type_obj->public && !$post_type_obj->publicly_queryable && $slug !== $this->post_type) {
                continue;
            }
            
            $label = '';
            if (is_object($post_type_obj->labels) && isset($post_type_obj->labels->name)) {
                $label = sanitize_text_field((string) $post_type_obj->labels->name);
            }
            if ($label === '') {
                $label = ucfirst(str_replace('_', ' ', $slug));
            }
            $available[$slug] = $label;
        }

        if (!isset($available[$this->post_type])) {
            $primary_label = __('Listings', 'nexdirectory');
            $primary_obj = get_post_type_object($this->post_type);
            if ($primary_obj instanceof WP_Post_Type && is_object($primary_obj->labels) && isset($primary_obj->labels->name)) {
                $candidate = sanitize_text_field((string) $primary_obj->labels->name);
                if ($candidate !== '') {
                    $primary_label = $candidate;
                }
            }
            $available[$this->post_type] = $primary_label;
        }

        uksort($available, function ($a, $b) {
            if ((string) $a === $this->post_type) return -1;
            if ((string) $b === $this->post_type) return 1;
            return strcasecmp((string) $a, (string) $b);
        });

        return $available;
    }

    /**
     * Gets post types currently selected by the admin to index.
     */
    public function get_chatbot_selected_post_types() {
        $available = $this->get_chatbot_available_post_types();
        $allowed = array_keys($available);
        $raw = get_option('eep_chatbot_index_post_types', '');
        
        $candidates = [];
        if (is_array($raw)) {
            $candidates = $raw;
        } elseif (is_string($raw) && trim($raw) !== '') {
            $decoded = json_decode($raw, true);
            $candidates = is_array($decoded) ? $decoded : explode(',', $raw);
        }

        $selected = [];
        foreach ((array) $candidates as $candidate) {
            $slug = sanitize_key((string) $candidate);
            if ($slug !== '' && in_array($slug, $allowed, true)) {
                $selected[] = $slug;
            }
        }
        $selected = array_values(array_unique($selected));
        
        if (empty($selected)) {
            $selected = $this->chatbot_default_source_post_types($allowed);
            if (empty($selected) && !empty($allowed)) {
                $selected = [(string) reset($allowed)];
            }
        }
        return $selected;
    }

    private function chatbot_default_source_post_types(array $allowed = []) {
        $allowed = array_values(array_filter(array_map('sanitize_key', $allowed)));
        if (empty($allowed)) return [];
        
        $defaults = [$this->post_type, 'post', 'page'];
        $selected = [];
        foreach ($defaults as $default_slug) {
            if (in_array((string) $default_slug, $allowed, true)) {
                $selected[] = (string) $default_slug;
            }
        }
        return array_values(array_unique($selected));
    }

    /**
     * Is AI catalog enrichment enabled?
     */
    public function chatbot_ai_catalog_enabled() {
        return (string) get_option('eep_chatbot_ai_catalog_enabled', '1') === '1';
    }

    /**
     * Retrieves the stored JSON knowledge catalog.
     */
    public function chatbot_get_knowledge_catalog() {
        $raw = get_option('eep_chatbot_knowledge_catalog_json', '');
        $catalog = is_string($raw) && trim($raw) !== '' ? json_decode($raw, true) : (is_array($raw) ? $raw : []);
        if (!is_array($catalog)) {
            $catalog = [];
        }
        
        $catalog['content_types'] = $catalog['content_types'] ?? [];
        $catalog['topics'] = $catalog['topics'] ?? [];
        $catalog['user_ask_patterns'] = $catalog['user_ask_patterns'] ?? [];
        
        return $catalog;
    }

    /**
     * Saves the knowledge catalog to the DB.
     */
    public function chatbot_save_knowledge_catalog(array $catalog) {
        $catalog['generated_at'] = sanitize_text_field((string) ($catalog['generated_at'] ?? current_time('mysql')));
        $catalog['summary'] = $catalog['summary'] ?? '';
        
        update_option('eep_chatbot_knowledge_catalog_json', wp_json_encode($catalog), false);
        update_option('eep_chatbot_knowledge_catalog_updated_at', current_time('mysql'), false);
        update_option('eep_chatbot_knowledge_catalog_stale', '0', false);
    }

    /**
     * Generates an indexed summary string of all fields for a single post.
     */
    public function build_chatbot_event_index_text($post_id) {
        $post_id = absint((int) $post_id);
        $post_type = sanitize_key((string) get_post_type($post_id));
        if ($post_id <= 0 || $post_type === '') {
            return '';
        }
        
        $selected_post_types = $this->get_chatbot_selected_post_types();
        if (!in_array($post_type, $selected_post_types, true)) {
            return '';
        }
        $post = get_post($post_id);
        if (!$post instanceof WP_Post) {
            return '';
        }

        $truncate_text = static function ($text, $limit) {
            $text = (string) $text;
            $limit = max(1, (int) $limit);
            return function_exists('mb_substr') ? mb_substr($text, 0, $limit) : substr($text, 0, $limit);
        };

        $normalize_scalar = static function ($value) {
            if (is_bool($value)) return $value ? '1' : '0';
            if (is_numeric($value)) return (string) $value;
            if (is_string($value)) {
                $candidate = trim($value);
                if ($candidate === '') return '';
                $maybe_json = json_decode($candidate, true);
                if (is_array($maybe_json)) {
                    $value = $maybe_json;
                } else {
                    $candidate = eep_strip_all_tags($candidate, true);
                    $candidate = preg_replace('/\s+/u', ' ', (string) $candidate);
                    $candidate = trim((string) $candidate);
                    if ($candidate === '') return '';
                    return function_exists('mb_substr') ? mb_substr($candidate, 0, 2000) : substr($candidate, 0, 2000);
                }
            }
            if (is_array($value)) {
                $flatten = [];
                array_walk_recursive($value, function($item) use (&$flatten) {
                    if (is_scalar($item) && trim((string)$item) !== '') {
                        $flatten[] = trim(preg_replace('/\s+/u', ' ', eep_strip_all_tags((string)$item, true)));
                    }
                });
                $joined = implode(' | ', array_slice(array_unique(array_filter($flatten)), 0, 60));
                return function_exists('mb_substr') ? mb_substr($joined, 0, 3000) : substr($joined, 0, 3000);
            }
            return '';
        };

        $parts = [];
        $append = static function (&$parts_ref, $label, $value) use ($normalize_scalar) {
            $label = sanitize_text_field((string) $label);
            if ($label === '') return;
            $normalized = $normalize_scalar($value);
            if ($normalized === '') return;
            $parts_ref[] = $label . ': ' . $normalized;
        };

        $append($parts, 'Post ID', $post_id);
        
        $post_type_label = $post_type;
        $post_type_obj = get_post_type_object($post_type);
        if ($post_type_obj && isset($post_type_obj->labels->name)) {
            $post_type_label = sanitize_text_field((string) $post_type_obj->labels->name);
        }
        $append($parts, 'Post Type', $post_type_label . ' (' . $post_type . ')');
        $append($parts, 'Title', get_the_title($post_id));
        $append($parts, 'Status', $post->post_status);
        $append($parts, 'Excerpt', get_post_field('post_excerpt', $post_id));
        $append($parts, 'Description', get_post_field('post_content', $post_id));

        $author_id = absint((int) $post->post_author);
        if ($author_id > 0 && ($author = get_userdata($author_id))) {
            $append($parts, 'Organizer Name', $author->display_name);
        }

        $handled_meta_keys = [];
        if ($post_type === $this->post_type) {
            $classification = $this->event_meta->get_official_event_flag($post_id) ? __('Official', 'nexdirectory') : __('Unofficial', 'nexdirectory');
            $append($parts, 'Classification', $classification);

            $meta_fields = $this->event_meta->get_event_meta_fields();
            foreach ($meta_fields as $meta_key => $meta_type) {
                $meta_key = sanitize_key((string) $meta_key);
                $handled_meta_keys[$meta_key] = true;
                
                $raw_value = get_post_meta($post_id, $meta_key, true);
                if ($raw_value === '' || $raw_value === null) continue;
                
                // Exclude raw media JSONs from embedding text, it wastes tokens
                if (in_array($meta_key, ['eep_video_urls', 'eep_gallery_urls', 'eep_supporting_docs'], true)) continue;

                $sanitized = $this->event_meta->sanitize_meta_value($meta_key, $raw_value);
                $append($parts, str_replace('eep_', '', $meta_key), $sanitized);
            }

            // Custom fields & Sessions
            $sessions = $this->event_meta->parse_sessions((string) get_post_meta($post_id, 'eep_sessions', true));
            $session_bits = [];
            foreach ($sessions as $session) {
                $session_line = trim(implode(' | ', array_filter([$session['title'] ?? '', $session['start'] ?? '', $session['end'] ?? '', $session['location'] ?? ''])));
                if ($session_line !== '') $session_bits[] = $session_line;
            }
            $append($parts, 'Sessions', implode(' || ', $session_bits));

            $custom_field_schema = $this->event_meta->get_post_custom_field_schema($post_id, true);
            $custom_field_values = $this->event_meta->get_post_custom_field_values($post_id, $custom_field_schema);
            foreach ($custom_field_schema as $field) {
                $field_id = sanitize_key((string) ($field['id'] ?? ''));
                if ($field_id === '') continue;
                
                $val = $custom_field_values[$field_id] ?? '';
                if ($field['type'] === 'checkbox') $val = ((string) $val === '1') ? 'Yes' : 'No';
                if ($field['type'] === 'wysiwyg') $val = eep_strip_all_tags((string) $val, true);
                
                $append($parts, 'Custom Field - ' . eep_sanitize_text($field['label'] ?? $field_id), $val);
            }
        }

        // Taxonomies
        $taxonomy_objects = get_object_taxonomies($post_type, 'objects');
        if (is_array($taxonomy_objects)) {
            foreach ($taxonomy_objects as $taxonomy_key => $taxonomy_obj) {
                $terms = wp_get_post_terms($post_id, $taxonomy_key);
                if (is_wp_error($terms) || empty($terms)) continue;
                
                $term_names = [];
                foreach ($terms as $term) {
                    $term_names[] = sanitize_text_field((string) $term->name);
                }
                $taxonomy_label = isset($taxonomy_obj->labels->name) ? eep_sanitize_text($taxonomy_obj->labels->name) : $taxonomy_key;
                $append($parts, 'Taxonomy - ' . $taxonomy_label, implode(', ', $term_names));
            }
        }

        $text = trim((string) implode("\n", $parts));
        if (function_exists('mb_strlen') && mb_strlen($text) > 40000) {
            $text = (string) $truncate_text($text, 40000) . "\n[Index truncated]";
        } elseif (strlen($text) > 40000) {
            $text = (string) $truncate_text($text, 40000) . "\n[Index truncated]";
        }
        return $text;
    }

    /**
     * Upserts an event into the vector database using OpenAI's embedding model.
     */
    public function update_chatbot_embedding_for_event($post_id, $force = false) {
        $post_id = absint((int) $post_id);
        $post_type = sanitize_key((string) get_post_type($post_id));

        if ($post_id <= 0 || $post_type === '') {
            return new WP_Error('eep_chatbot_invalid_post', __('Invalid post for chatbot indexing.', 'nexdirectory'));
        }
        if (!in_array($post_type, $this->get_chatbot_selected_post_types(), true)) {
            $this->delete_chatbot_embedding_for_event($post_id);
            return 'deleted';
        }
        if ((string) get_post_status($post_id) !== 'publish') {
            $this->delete_chatbot_embedding_for_event($post_id);
            return 'deleted';
        }
        if (!is_object($this->chatbot_service) || !method_exists($this->chatbot_service, 'chatbot_has_api_configuration') || !$this->chatbot_service->chatbot_has_api_configuration()) {
            return new WP_Error('eep_chatbot_api_missing', __('Chatbot OpenAI API key is missing.', 'nexdirectory'));
        }

        $index_text = $this->build_chatbot_event_index_text($post_id);
        if ($index_text === '') {
            return new WP_Error('eep_chatbot_empty_index_text', __('Event content was empty for indexing.', 'nexdirectory'));
        }

        $content_hash = hash('sha256', $index_text);
        $table = $this->get_table_name();
        global $wpdb;
        $existing = $wpdb->get_row($wpdb->prepare(
            "SELECT id, content_hash FROM {$table} WHERE post_id = %d LIMIT 1",
            $post_id
        ), ARRAY_A); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
        $existing_hash = is_array($existing) ? (string) ($existing['content_hash'] ?? '') : '';

        if (!$force && $existing_hash !== '' && hash_equals($existing_hash, $content_hash)) {
            return 'skipped';
        }

        if (!method_exists($this->chatbot_service, 'chatbot_create_embedding')) {
            return new WP_Error('eep_chatbot_embedding_missing', __('Chatbot embedding service is unavailable.', 'nexdirectory'));
        }
        $embedding = $this->chatbot_service->chatbot_create_embedding($index_text);
        if (is_wp_error($embedding)) {
            return $embedding;
        }

        $embedding_model = method_exists($this->chatbot_service, 'get_chatbot_embedding_model')
            ? sanitize_text_field((string) $this->chatbot_service->get_chatbot_embedding_model())
            : 'text-embedding-3-small';

        $now = current_time('mysql', 1);
        $data = [
            'post_id' => $post_id,
            'content_hash' => $content_hash,
            'embedding_model' => $embedding_model,
            'embedding_dimensions' => count((array) $embedding),
            'embedding' => wp_json_encode($embedding),
            'chunk_text' => $index_text,
            'updated_at' => $now,
        ];

        $format = ['%d', '%s', '%s', '%d', '%s', '%s', '%s'];
        if (is_array($existing) && !empty($existing['id'])) {
            $ok = $wpdb->update($table, $data, ['post_id' => $post_id], $format, ['%d']); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
        } else {
            $data['created_at'] = $now;
            $ok = $wpdb->insert($table, $data, ['%d', '%s', '%s', '%d', '%s', '%s', '%s', '%s']); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
        }

        if ($ok === false) {
            $this->append_runtime_log('error', 'Chatbot embedding write failed', [
                'post_id' => (string) $post_id,
                'error' => (string) ($wpdb->last_error ?? ''),
            ]);
            return new WP_Error('eep_chatbot_index_write_failed', __('Could not save chatbot embedding row.', 'nexdirectory'));
        }

        update_option('eep_chatbot_last_indexed_at', current_time('mysql'), false);
        update_option('eep_chatbot_knowledge_catalog_stale', '1', false);

        return 'indexed';
    }

    /**
     * Removes an embedding row for a given post.
     */
    public function delete_chatbot_embedding_for_event($post_id) {
        $post_id = absint((int) $post_id);
        if ($post_id <= 0) {
            return;
        }
        global $wpdb;
        $deleted = $wpdb->delete($this->get_table_name(), ['post_id' => $post_id], ['%d']); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
        if ($deleted !== false && (int) $deleted > 0) {
            update_option('eep_chatbot_knowledge_catalog_stale', '1', false);
        }
    }

    /**
     * Returns chatbot index summary data for admin UI.
     */
    public function get_chatbot_index_stats() {
        global $wpdb;
        $table = $this->get_table_name();
        $catalog = $this->chatbot_get_knowledge_catalog();
        $catalog_updated_at = sanitize_text_field((string) get_option('eep_chatbot_knowledge_catalog_updated_at', ''));
        $catalog_summary = sanitize_text_field((string) ($catalog['summary'] ?? ''));
        $catalog_types = absint((int) count((array) ($catalog['content_types'] ?? [])));
        $catalog_topics = absint((int) count((array) ($catalog['topics'] ?? [])));
        $catalog_patterns = absint((int) count((array) ($catalog['user_ask_patterns'] ?? [])));

        $type_labels = [];
        $content_types = (array) ($catalog['content_types'] ?? []);
        if (!empty($content_types)) {
            uasort($content_types, static function ($a, $b) {
                $ac = absint((int) ($a['count'] ?? 0));
                $bc = absint((int) ($b['count'] ?? 0));
                if ($ac === $bc) {
                    return strcmp((string) ($a['label'] ?? ''), (string) ($b['label'] ?? ''));
                }
                return $bc <=> $ac;
            });
            foreach ($content_types as $row) {
                if (!is_array($row)) {
                    continue;
                }
                $label = sanitize_text_field((string) ($row['label'] ?? $row['slug'] ?? ''));
                if ($label === '') {
                    continue;
                }
                $type_labels[] = $label;
                if (count($type_labels) >= 5) {
                    break;
                }
            }
        }

        $selected_post_types = $this->get_chatbot_selected_post_types();
        if (empty($selected_post_types)) {
            return [
                'rows' => 0,
                'latest_update' => '',
                'last_reindex_at' => sanitize_text_field((string) get_option('eep_chatbot_last_indexed_at', '')),
                'last_query_at' => sanitize_text_field((string) get_option('eep_chatbot_last_query_at', '')),
                'query_count' => absint((int) get_option('eep_chatbot_query_count', 0)),
                'last_summary' => sanitize_text_field((string) get_option('eep_chatbot_last_reindex_summary_text', '')),
                'catalog_updated_at' => $catalog_updated_at,
                'catalog_summary' => $catalog_summary,
                'catalog_type_labels' => $type_labels,
                'catalog_types' => $catalog_types,
                'catalog_topics' => $catalog_topics,
                'catalog_patterns' => $catalog_patterns,
            ];
        }

        $posts_table = $wpdb->posts;
        $placeholders = implode(',', array_fill(0, count($selected_post_types), '%s'));
        $sql = $wpdb->prepare(
            "SELECT COUNT(*) AS total_rows, MAX(e.updated_at) AS latest_update
             FROM {$table} e
             INNER JOIN {$posts_table} p ON p.ID = e.post_id
             WHERE p.post_status = 'publish'
               AND p.post_type IN ({$placeholders})",
            $selected_post_types
        );
        $row = $wpdb->get_row($sql, ARRAY_A); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
        $count = is_array($row) ? absint((int) ($row['total_rows'] ?? 0)) : 0;
        $latest = is_array($row) ? sanitize_text_field((string) ($row['latest_update'] ?? '')) : '';

        return [
            'rows' => $count,
            'latest_update' => $latest,
            'last_reindex_at' => sanitize_text_field((string) get_option('eep_chatbot_last_indexed_at', '')),
            'last_query_at' => sanitize_text_field((string) get_option('eep_chatbot_last_query_at', '')),
            'query_count' => absint((int) get_option('eep_chatbot_query_count', 0)),
            'last_summary' => sanitize_text_field((string) get_option('eep_chatbot_last_reindex_summary_text', '')),
            'catalog_updated_at' => $catalog_updated_at,
            'catalog_summary' => $catalog_summary,
            'catalog_type_labels' => $type_labels,
            'catalog_types' => $catalog_types,
            'catalog_topics' => $catalog_topics,
            'catalog_patterns' => $catalog_patterns,
        ];
    }

    /**
     * Cron entrypoint for chatbot reindexing.
     */
    public function run_chatbot_reindex_cron() {
        if ((string) get_option('eep_chatbot_enabled', '0') !== '1') {
            return;
        }

        $summary = $this->run_chatbot_reindex(false);
        if (is_wp_error($summary)) {
            $this->append_runtime_log('error', 'Chatbot cron reindex failed', [
                'message' => $summary->get_error_message(),
            ]);
            return;
        }

        $summary_text = sprintf(
            'indexed=%d skipped=%d failed=%d catalog_types=%d catalog_topics=%d',
            (int) ($summary['indexed'] ?? 0),
            (int) ($summary['skipped'] ?? 0),
            (int) ($summary['failed'] ?? 0),
            (int) ($summary['catalog_types'] ?? 0),
            (int) ($summary['catalog_topics'] ?? 0)
        );
        update_option('eep_chatbot_last_reindex_summary_text', $summary_text, false);
        $this->append_runtime_log('info', 'Chatbot cron reindex complete', [
            'summary' => $summary_text,
            'duration_ms' => (string) (int) ($summary['duration_ms'] ?? 0),
        ]);
    }

    /**
     * Reindexes selected post types into the chatbot embeddings table.
     */
    public function run_chatbot_reindex($force = false) {
        if (!is_object($this->chatbot_service) || !method_exists($this->chatbot_service, 'chatbot_has_api_configuration') || !$this->chatbot_service->chatbot_has_api_configuration()) {
            return new WP_Error('eep_chatbot_api_missing', __('Set the chatbot OpenAI API key before indexing.', 'nexdirectory'));
        }

        $lock_key = 'eep_chatbot_reindex_lock';
        if ((int) get_transient($lock_key) === 1) {
            return new WP_Error('eep_chatbot_reindex_busy', __('Chatbot reindex is already running.', 'nexdirectory'));
        }
        set_transient($lock_key, 1, 20 * MINUTE_IN_SECONDS);

        $started_at = microtime(true);
        $indexed = 0;
        $skipped = 0;
        $failed = 0;
        $errors = [];
        $selected_post_types = $this->get_chatbot_selected_post_types();
        if (empty($selected_post_types)) {
            delete_transient($lock_key);
            return new WP_Error('eep_chatbot_no_post_types', __('Select at least one post type for chatbot indexing.', 'nexdirectory'));
        }

        try {
            $paged = 1;
            do {
                $query = new WP_Query([
                    'post_type' => $selected_post_types,
                    'post_status' => 'publish',
                    'posts_per_page' => 120,
                    'paged' => $paged,
                    'fields' => 'ids',
                    'orderby' => 'modified',
                    'order' => 'DESC',
                    'no_found_rows' => true,
                ]);
                $ids = is_array($query->posts ?? null) ? (array) $query->posts : [];
                foreach ($ids as $post_id) {
                    $result = $this->update_chatbot_embedding_for_event((int) $post_id, (bool) $force);
                    if (is_wp_error($result)) {
                        $failed++;
                        if (count($errors) < 8) {
                            $errors[] = sprintf('#%d %s', (int) $post_id, $result->get_error_message());
                        }
                        continue;
                    }
                    if ($result === 'indexed') {
                        $indexed++;
                    } elseif ($result === 'skipped') {
                        $skipped++;
                    }
                }
                $paged++;
            } while (!empty($ids));
        } finally {
            delete_transient($lock_key);
        }

        $duration_ms = (int) round((microtime(true) - $started_at) * 1000);
        update_option('eep_chatbot_last_indexed_at', current_time('mysql'), false);
        update_option('eep_chatbot_knowledge_catalog_stale', '1', false);

        $catalog = $this->chatbot_get_knowledge_catalog();
        $catalog_summary = sanitize_text_field((string) ($catalog['summary'] ?? ''));
        $catalog_types = absint((int) count((array) ($catalog['content_types'] ?? [])));
        $catalog_topics = absint((int) count((array) ($catalog['topics'] ?? [])));

        return [
            'indexed' => $indexed,
            'skipped' => $skipped,
            'failed' => $failed,
            'duration_ms' => $duration_ms,
            'errors' => $errors,
            'catalog_summary' => $catalog_summary,
            'catalog_types' => $catalog_types,
            'catalog_topics' => $catalog_topics,
        ];
    }

    /**
     * Clears the chatbot embeddings table.
     */
    public function clear_chatbot_index() {
        global $wpdb;
        $table = $this->get_table_name();
        $result = $wpdb->query("TRUNCATE TABLE {$table}"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
        if ($result === false && !empty($wpdb->last_error)) {
            $this->append_runtime_log('error', 'Chatbot index truncate failed', [
                'table' => $table,
                'error' => (string) $wpdb->last_error,
            ]);
        }
        update_option('eep_chatbot_last_reindex_summary_text', '', false);
        update_option('eep_chatbot_knowledge_catalog_json', '', false);
        update_option('eep_chatbot_knowledge_catalog_updated_at', '', false);
        update_option('eep_chatbot_knowledge_catalog_stale', '1', false);
    }

    /**
     * Safe runtime logger bridge.
     */
    private function append_runtime_log($level, $message, array $context = []) {
        if (is_object($this->logger) && method_exists($this->logger, 'append_runtime_log')) {
            $this->logger->append_runtime_log($level, $message, $context);
        }
    }
}
