<?php
/**
 * RAG (Retrieval-Augmented Generation) Engine.
 *
 * Combines vector search, keyword search, and path search for optimal retrieval.
 *
 * @package Inqyra
 */

// If this file is called directly, abort.
if (!defined('WPINC')) {
    die;
}

/**
 * RAG Engine service class.
 */
class Inqyra_RAG_Engine {

    /**
     * Vector store instance.
     *
     * @var Inqyra_Vector_Store
     */
    private $vector_store;

    /**
     * Embedder instance.
     *
     * @var Inqyra_Embedder
     */
    private $embedder;

    /**
     * Synonym service instance.
     *
     * @var Inqyra_Synonym_Service|null
     */
    private $synonym_service;

    /**
     * Path match boost factor.
     */
    const PATH_BOOST = 2.0;

    /**
     * Keyword match boost factor.
     */
    const KEYWORD_BOOST = 1.2;

    /**
     * Title match boost factor.
     */
    const TITLE_BOOST = 2.5;

    /**
     * Constructor.
     */
    public function __construct() {
        $this->vector_store = new Inqyra_Vector_Store();
        $this->embedder = new Inqyra_Embedder();

        if (class_exists('Inqyra_Synonym_Service')) {
            $this->synonym_service = new Inqyra_Synonym_Service();
        }
    }

    /**
     * Search for relevant content.
     *
     * @param int    $agent_id The agent ID.
     * @param string $query    The search query.
     * @param array  $options  Search options. Can include 'query_embedding' for browser-based embeddings.
     * @return array Array of relevant chunks.
     */
    /**
     * Search debug logs.
     *
     * @var array
     */
    private $search_logs = [];

    /**
     * Get search debug logs.
     *
     * @return array
     */
    public function get_search_logs() {
        return $this->search_logs;
    }

    public function search($agent_id, $query, $options = []) {
        global $wpdb;

        $dbg = Inqyra_Debug_Logger::instance();

        $this->search_logs = [];
        $this->search_logs[] = "Query: '{$query}'";

        // Get agent configuration
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Agent config needed fresh for search.
        $agent = $wpdb->get_row($wpdb->prepare(
            'SELECT * FROM %i WHERE id = %d',
            $wpdb->prefix . 'inqyra_agents',
            $agent_id
        ));

        if (!$agent) {
            $this->search_logs[] = "Agent not found";
            return [];
        }

        // Default options
        $options = wp_parse_args($options, [
            'top_k' => 5,
            'min_score' => 0.3,
            'use_vector' => true,
            'use_keyword' => true,
            'use_path' => true,
            'expand_synonyms' => true,
            'query_embedding' => null, // Can be passed from browser-based Transformers.js
        ]);

        // Track rewrite stats
        $rewrite_stats = null;

        // 0. LLM-based query rewriting (if enabled)
        $search_query = $query;
        if (class_exists('Inqyra_Query_Rewriter')) {
            $query_rewriter = new Inqyra_Query_Rewriter($agent);
            if ($query_rewriter->is_enabled()) {
                $conversation_history = $options['conversation_history'] ?? [];
                $rewrite_result = $query_rewriter->rewrite($query, $agent->language ?? 'nl', $conversation_history);
                if ($rewrite_result['used_rewriter']) {
                    $search_query = $rewrite_result['rewritten'];
                    $rewrite_stats = [
                        'original' => $query,
                        'rewritten' => $search_query,
                        'tokens' => $rewrite_result['tokens'],
                        'cost' => $rewrite_result['cost'],
                    ];
                }

                $dbg->log('5a_query_rewrite', [
                    'enabled' => true,
                    'used' => $rewrite_result['used_rewriter'],
                    'original' => $query,
                    'rewritten' => $rewrite_result['rewritten'],
                    'provider' => $agent->query_rewrite_provider ?? 'claude',
                    'model' => $agent->query_rewrite_model ?? 'claude-3-haiku-20240307',
                    'tokens' => $rewrite_result['tokens'],
                    'cost' => $rewrite_result['cost'],
                ]);
            } else {
                $dbg->log('5a_query_rewrite', ['enabled' => false]);
            }
        } else {
            $dbg->log('5a_query_rewrite', ['status' => 'class_not_available']);
        }

        // 1. Expand query with synonyms (applied on top of rewritten query)
        $pre_synonym_query = $search_query;
        if ($options['expand_synonyms'] && $this->synonym_service) {
            $expanded = $this->synonym_service->expand_query(
                $search_query,
                $agent_id,
                $agent->synonym_expansion_strategy ?? 'append'
            );
            $search_query = $expanded['expanded_query'];

            $dbg->log('5b_synonym_expansion_search', [
                'before' => $pre_synonym_query,
                'after' => $search_query,
                'matched' => $expanded['matched_terms'] ?? [],
                'changed' => $pre_synonym_query !== $search_query,
            ]);
        }

        // 2. Classify intent using EXPANDED query (so synonyms trigger correct intent)
        $intent = $this->classify_intent($search_query);
        $this->search_logs[] = "Intent: {$intent['type']} (skip_search=" . ($intent['skip_search'] ? 'true' : 'false') . ")";

        $dbg->log('5c_intent_classification', [
            'query_used' => $search_query,
            'intent_type' => $intent['type'],
            'skip_search' => $intent['skip_search'],
            'boost_keyword' => $intent['boost_keyword'],
        ]);

        // Skip search for pure conversational queries
        if ($intent['skip_search']) {
            $this->search_logs[] = "Search skipped due to conversational intent";
            return [];
        }

        // 3. Determine search options based on agent config
        $search_config = $this->get_search_config($agent);
        $this->search_logs[] = "Content types: " . implode(', ', $search_config['content_types']);

        // 4. Collect results from different search methods
        $all_results = [];

        // Vector search (using browser-provided embedding or disabled if not available)
        if ($options['use_vector'] && !empty($options['query_embedding'])) {
            $vector_results = $this->vector_store->search_similar_cached(
                $agent_id,
                $query, // Original query for cache key
                $options['query_embedding'],
                $options['top_k'] * 2, // Get more to allow for merging
                $options['min_score'],
                $search_config['content_types'],
                300 // Cache for 5 minutes
            );
            $this->search_logs[] = "Vector search: " . count($vector_results) . " results";
            $all_results = array_merge($all_results, $vector_results);

            $dbg->log('6a_vector_search', [
                'result_count' => count($vector_results),
                'min_score' => $options['min_score'],
                'results' => array_map(function ($r) {
                    return [
                        'id' => $r['id'] ?? null,
                        'score' => round((float) ($r['score'] ?? 0), 4),
                        'title' => $r['title'] ?? '',
                        'source_url' => $r['source_url'] ?? '',
                        'content_preview' => mb_substr($r['content'] ?? '', 0, 150),
                    ];
                }, $vector_results),
            ]);
        } else {
            $this->search_logs[] = "Vector search: skipped (no embedding)";
            $dbg->log('6a_vector_search', [
                'status' => 'skipped',
                'reason' => empty($options['query_embedding']) ? 'no_embedding_provided' : 'disabled',
            ]);
        }

        // Keyword search (fulltext) - always available
        if ($options['use_keyword']) {
            $keyword_results = $this->keyword_search(
                $agent_id,
                $search_query,
                $options['top_k'] * 2,
                $search_config['content_types']
            );
            $this->search_logs[] = "Keyword search for '{$search_query}': " . count($keyword_results) . " results";
            foreach ($keyword_results as &$result) {
                $result['match_type'] = 'keyword';
            }
            $all_results = array_merge($all_results, $keyword_results);

            $dbg->log('6b_keyword_search', [
                'search_query' => $search_query,
                'result_count' => count($keyword_results),
                'content_types' => $search_config['content_types'],
                'results' => array_map(function ($r) {
                    return [
                        'id' => $r['id'] ?? null,
                        'score' => round((float) ($r['score'] ?? 0), 4),
                        'title' => $r['title'] ?? '',
                        'source_url' => $r['source_url'] ?? '',
                        'content_preview' => mb_substr($r['content'] ?? '', 0, 150),
                    ];
                }, $keyword_results),
            ]);
        }

        // Path search (for navigational queries)
        if ($options['use_path'] && $intent['boost_keyword']) {
            $path_results = $this->path_search($agent_id, $search_query, $options['top_k']);
            foreach ($path_results as &$result) {
                $result['match_type'] = 'path';
                $result['score'] = ($result['score'] ?? 1) * self::PATH_BOOST;
            }
            $all_results = array_merge($all_results, $path_results);

            $dbg->log('6c_path_search', [
                'search_query' => $search_query,
                'result_count' => count($path_results),
                'results' => array_map(function ($r) {
                    return [
                        'id' => $r['id'] ?? null,
                        'score' => round((float) ($r['score'] ?? 0), 4),
                        'source_url' => $r['source_url'] ?? '',
                    ];
                }, $path_results),
            ]);
        }

        // Title search - find source_ids with matching titles for boosting
        $title_matched_sources = $this->title_search($agent_id, $search_query);

        if (!empty($title_matched_sources)) {
            $dbg->log('6d_title_search', [
                'search_query' => $search_query,
                'matched_sources' => $title_matched_sources,
            ]);
        }

        // 5. Merge and rank results (pass title matches for boosting)
        $merged = $this->merge_results($all_results, $search_config, $title_matched_sources);
        $this->search_logs[] = "Merged results: " . count($merged);

        $dbg->log('7a_merged_results', [
            'total_before_merge' => count($all_results),
            'total_after_merge' => count($merged),
            'results' => array_map(function ($r) {
                return [
                    'id' => $r['id'] ?? null,
                    'score' => round((float) ($r['score'] ?? 0), 4),
                    'match_type' => $r['match_type'] ?? '',
                    'title' => $r['title'] ?? '',
                    'source_url' => $r['source_url'] ?? '',
                    'content_type' => $r['content_type'] ?? '',
                ];
            }, array_slice($merged, 0, 10)),
        ]);

        // Also try a direct LIKE search for debugging
        if (empty($merged)) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Debug fallback LIKE search when no results found.
            $like_count = $wpdb->get_var($wpdb->prepare(
                'SELECT COUNT(*) FROM %i
                 WHERE agent_id = %d AND content LIKE %s',
                $wpdb->prefix . 'inqyra_knowledge_chunks',
                $agent_id,
                '%' . $wpdb->esc_like($query) . '%'
            ));
            $this->search_logs[] = "LIKE fallback: {$like_count} chunks contain '{$query}'";

            $dbg->log('7b_like_fallback', [
                'query' => $query,
                'matching_chunks' => (int) $like_count,
            ]);
        }

        // 6. Get linked documents if configured
        if ($search_config['fetch_linked_documents'] && !empty($merged)) {
            $website_chunk_ids = array_column(
                array_filter($merged, fn($r) => ($r['content_type'] ?? '') === 'website'),
                'id'
            );
            if (!empty($website_chunk_ids)) {
                $linked_docs = $this->get_linked_documents(array_slice($website_chunk_ids, 0, 3));
                $merged = array_merge($merged, $linked_docs);

                $dbg->log('7c_linked_documents', [
                    'website_chunk_ids_searched' => array_slice($website_chunk_ids, 0, 3),
                    'linked_docs_found' => count($linked_docs),
                    'results' => array_map(function ($r) {
                        return [
                            'id' => $r['id'] ?? null,
                            'title' => $r['title'] ?? '',
                            'source_url' => $r['source_url'] ?? '',
                            'content_preview' => mb_substr($r['content'] ?? '', 0, 150),
                        ];
                    }, $linked_docs),
                ]);
            }
        }

        // 7. Return top K results
        return array_slice($merged, 0, $options['top_k']);
    }

    /**
     * Classify the intent of a query.
     *
     * @param string $query The query to classify.
     * @return array Intent classification result.
     */
    private function classify_intent($query) {
        $query = strtolower(trim($query));

        // Conversational patterns (greetings, thanks, etc.)
        $conversational = [
            'hallo', 'hoi', 'hey', 'hi', 'goedemorgen', 'goedemiddag', 'goedenavond',
            'bedankt', 'dank je', 'dankjewel', 'thanks', 'thank you', 'thx',
            'bye', 'doei', 'tot ziens', 'dag',
            'ja', 'nee', 'ok', 'oke', 'prima', 'goed', 'top',
        ];

        foreach ($conversational as $pattern) {
            if ($query === $pattern || str_starts_with($query, $pattern . ' ') || str_starts_with($query, $pattern . ',')) {
                return [
                    'type' => 'conversational',
                    'skip_search' => true,
                    'boost_keyword' => false,
                ];
            }
        }

        // Contact/navigational patterns (these benefit from path search)
        $contact = [
            'telefoon', 'telefoonnummer', 'bellen', 'nummer',
            'adres', 'locatie', 'waar', 'route',
            'contact', 'email', 'e-mail', 'mail',
            'openingstijden', 'open', 'dicht', 'bereikbaar',
        ];

        foreach ($contact as $pattern) {
            if (str_contains($query, $pattern)) {
                return [
                    'type' => 'contact',
                    'skip_search' => false,
                    'boost_keyword' => true,
                ];
            }
        }

        // Default: informational query
        return [
            'type' => 'informational',
            'skip_search' => false,
            'boost_keyword' => false,
        ];
    }

    /**
     * Get search configuration based on agent settings.
     *
     * @param object $agent The agent configuration.
     * @return array Search configuration.
     */
    private function get_search_config($agent) {
        $mode = $agent->document_search_mode ?? 'linked';

        return [
            'content_types' => $mode === 'linked' ? ['website'] : ['website', 'document'],
            'document_weight' => (float) ($agent->document_score_weight ?? 0.7),
            'fetch_linked_documents' => $mode !== 'direct',
        ];
    }

    /**
     * Perform keyword (fulltext) search.
     *
     * @param int      $agent_id      The agent ID.
     * @param string   $query         The search query.
     * @param int      $limit         Maximum results.
     * @param array    $content_types Content types to search.
     * @return array Search results.
     */
    private function keyword_search($agent_id, $query, $limit, $content_types = null) {
        global $wpdb;

        // Escape query for FULLTEXT
        $query = $wpdb->esc_like($query);

        // Build content type filter
        $type_filter = '';
        $prepare_params = array($query, $wpdb->prefix . 'inqyra_knowledge_chunks', $agent_id, $query);
        if (!empty($content_types)) {
            $placeholders = implode(',', array_fill(0, count($content_types), '%s'));
            $type_filter = " AND content_type IN ($placeholders)";
            $prepare_params = array_merge($prepare_params, $content_types);
        }
        $prepare_params[] = $limit;

        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $type_filter contains only placeholders, merged into single prepare via spread operator. Fulltext search not cacheable.
        $results = $wpdb->get_results($wpdb->prepare(
            'SELECT id, content, title, content_type, source_url, source_type, source_id,
                    MATCH(content) AGAINST(%s IN NATURAL LANGUAGE MODE) as score
             FROM %i
             WHERE agent_id = %d
             AND MATCH(content) AGAINST(%s IN NATURAL LANGUAGE MODE)
             ' . $type_filter . '
             ORDER BY score DESC
             LIMIT %d',
            ...$prepare_params
        ), ARRAY_A);
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, PluginCheck.Security.DirectDB.UnescapedDBParameter

        return $results ?: [];
    }

    /**
     * Perform path-based search.
     *
     * @param int    $agent_id The agent ID.
     * @param string $query    The search query.
     * @param int    $limit    Maximum results.
     * @return array Search results.
     */
    private function path_search($agent_id, $query, $limit) {
        global $wpdb;

        $query = $wpdb->esc_like($query);

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Fulltext path search not cacheable.
        $results = $wpdb->get_results($wpdb->prepare(
            'SELECT id, content, title, content_type, source_url, source_type, source_id,
                    MATCH(url_path) AGAINST(%s IN NATURAL LANGUAGE MODE) as score
             FROM %i
             WHERE agent_id = %d
             AND url_path IS NOT NULL
             AND MATCH(url_path) AGAINST(%s IN NATURAL LANGUAGE MODE)
             ORDER BY score DESC
             LIMIT %d',
            $query, $wpdb->prefix . 'inqyra_knowledge_chunks', $agent_id, $query, $limit
        ), ARRAY_A);

        return $results ?: [];
    }

    /**
     * Search titles and return source_ids with match scores for boosting.
     *
     * Instead of returning chunks, this returns an array of source_id => score
     * so we can boost ALL chunks from pages with matching titles.
     *
     * @param int    $agent_id The agent ID.
     * @param string $query    The search query.
     * @return array Associative array of source_id => score.
     */
    private function title_search($agent_id, $query) {
        global $wpdb;

        $query = $wpdb->esc_like($query);

        // Get distinct source_ids with their best title match score
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Fulltext title search not cacheable.
        $results = $wpdb->get_results($wpdb->prepare(
            'SELECT source_id, MAX(MATCH(title) AGAINST(%s IN NATURAL LANGUAGE MODE)) as title_score
             FROM %i
             WHERE agent_id = %d
             AND title IS NOT NULL
             AND MATCH(title) AGAINST(%s IN NATURAL LANGUAGE MODE)
             GROUP BY source_id
             ORDER BY title_score DESC
             LIMIT 20',
            $query, $wpdb->prefix . 'inqyra_knowledge_chunks', $agent_id, $query
        ), ARRAY_A);

        // Convert to associative array: source_id => score
        $title_matches = [];
        foreach ($results ?: [] as $row) {
            if ($row['source_id'] && $row['title_score'] > 0) {
                $title_matches[$row['source_id']] = (float) $row['title_score'];
            }
        }

        return $title_matches;
    }

    /**
     * Merge and rank results from different search methods.
     *
     * @param array $all_results         All results from different methods.
     * @param array $config              Search configuration.
     * @param array $title_matched_sources Associative array of source_id => title_score for boosting.
     * @return array Merged and ranked results.
     */
    private function merge_results($all_results, $config, $title_matched_sources = []) {
        $merged = [];

        foreach ($all_results as $result) {
            $id = $result['id'];
            $score = (float) ($result['score'] ?? 0);

            // Apply document weight
            if (($result['content_type'] ?? '') === 'document') {
                $score *= $config['document_weight'];
            }

            // Apply keyword boost
            if (($result['match_type'] ?? '') === 'keyword') {
                $score *= self::KEYWORD_BOOST;
            }

            // Apply title boost if this chunk's source has a title match
            $source_id = $result['source_id'] ?? null;
            $has_title_match = false;
            if ($source_id && isset($title_matched_sources[$source_id])) {
                $score *= self::TITLE_BOOST;
                $has_title_match = true;
            }

            if (isset($merged[$id])) {
                // Combine scores from different match types
                $merged[$id]['score'] = max($merged[$id]['score'], $score);
                $merged[$id]['match_types'][] = $result['match_type'] ?? 'unknown';
                if ($has_title_match) {
                    $merged[$id]['match_types'][] = 'title';
                }
            } else {
                $merged[$id] = $result;
                $merged[$id]['score'] = $score;
                $merged[$id]['match_types'] = [$result['match_type'] ?? 'unknown'];
                if ($has_title_match) {
                    $merged[$id]['match_types'][] = 'title';
                }
            }
        }

        // Convert match_types array to string
        foreach ($merged as &$result) {
            $result['match_type'] = implode('+', array_unique($result['match_types']));
            unset($result['match_types']);
        }

        // Sort by score descending
        usort($merged, fn($a, $b) => ($b['score'] ?? 0) <=> ($a['score'] ?? 0));

        return array_values($merged);
    }

    /**
     * Get linked documents for website chunks.
     *
     * @param array $website_chunk_ids Array of website chunk IDs.
     * @return array Linked document chunks.
     */
    private function get_linked_documents($website_chunk_ids) {
        global $wpdb;

        if (empty($website_chunk_ids)) {
            return [];
        }

        $placeholders = implode(',', array_fill(0, count($website_chunk_ids), '%d'));

        // 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 -- $placeholders are generated %d placeholders for chunk IDs.
        $results = $wpdb->get_results($wpdb->prepare(
            'SELECT kc.id, kc.content, kc.title, kc.content_type, kc.source_url, kc.source_type, kc.source_id
             FROM %i cl
             JOIN %i kc ON kc.id = cl.document_chunk_id
             WHERE cl.website_chunk_id IN (' . $placeholders . ')
             LIMIT 5',
            $wpdb->prefix . 'inqyra_chunk_links',
            $wpdb->prefix . 'inqyra_knowledge_chunks',
            ...$website_chunk_ids
        ), ARRAY_A);
        // 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

        foreach ($results as &$result) {
            $result['match_type'] = 'linked';
            $result['score'] = 0.5; // Default score for linked docs
        }

        return $results ?: [];
    }

    /**
     * Build context string from search results.
     *
     * @param array $results Search results.
     * @param int   $max_length Maximum context length in characters.
     * @return string Context string for AI prompt.
     */
    public function build_context($results, $max_length = 8000) {
        if (empty($results)) {
            return '';
        }

        $context_parts = [];
        $current_length = 0;

        foreach ($results as $result) {
            $content = $result['content'] ?? '';
            $source = $result['source_url'] ?? '';
            $title = $result['title'] ?? '';

            // Format context chunk
            $chunk = "---\n";
            if ($title) {
                $chunk .= "Titel: {$title}\n";
            }
            if ($source) {
                $chunk .= "Bron: {$source}\n";
            }
            $chunk .= $content . "\n";

            $chunk_length = strlen($chunk);

            // Check if we have room
            if ($current_length + $chunk_length > $max_length) {
                // Try to fit a truncated version
                $remaining = $max_length - $current_length - 50; // Buffer for "..."
                if ($remaining > 200) {
                    $chunk = substr($chunk, 0, $remaining) . "...\n";
                    $context_parts[] = $chunk;
                }
                break;
            }

            $context_parts[] = $chunk;
            $current_length += $chunk_length;
        }

        return implode("\n", $context_parts);
    }

    /**
     * Get search statistics for debugging.
     *
     * @param int    $agent_id The agent ID.
     * @param string $query    The search query.
     * @return array Debug information.
     */
    public function get_search_debug($agent_id, $query) {
        $intent = $this->classify_intent($query);
        $stats = $this->vector_store->get_statistics($agent_id);

        return [
            'query' => $query,
            'intent' => $intent,
            'embedder_available' => $this->embedder->is_available(),
            'vector_stats' => $stats,
        ];
    }

}
