//! Query engine for text and semantic search

use regex::Regex;

use crate::error::Result;
use crate::models::{Graph, Node};

/// Query engine for searching nodes in a graph
///
/// Provides text and semantic search capabilities across nodes.
/// Supports:
/// - Full-text search (title, content, tags)
/// - Tag-based filtering
/// - Type-based filtering
pub struct QueryEngine {
    graph: Graph,
}

impl QueryEngine {
    /// Create a new query engine for the given graph
    #[must_use]
    pub fn new(graph: Graph) -> Self {
        Self { graph }
    }

    /// Search for nodes matching the query
    ///
    /// Performs full-text search across:
    /// - Node titles (case-insensitive)
    /// - Node content (case-insensitive)
    /// - Node tags (exact match)
    ///
    /// # Arguments
    ///
    /// * `query` - Search query string
    ///
    /// # Returns
    ///
    /// Vector of nodes matching the query, sorted by relevance
    ///
    /// # Errors
    ///
    /// This function does not fail; it always returns Ok with results.
    pub fn search(&self, query: &str) -> Result<Vec<Node>> {
        if query.is_empty() {
            return Ok(Vec::new());
        }

        let query_lower = query.to_lowercase();
        let mut results = Vec::new();

        for node in self.graph.nodes.values() {
            let mut score = 0;

            // Title match (highest score)
            if node.title.to_lowercase().contains(&query_lower) {
                score += 10;
            }

            // Tag match (high score)
            if node
                .tags
                .iter()
                .any(|tag| tag.to_lowercase().contains(&query_lower))
            {
                score += 5;
            }

            // Content match (medium score)
            if node.content.to_lowercase().contains(&query_lower) {
                score += 2;
            }

            if score > 0 {
                results.push((score, node.clone()));
            }
        }

        // Sort by score (descending)
        results.sort_by_key(|b| std::cmp::Reverse(b.0));

        Ok(results.into_iter().map(|(_, node)| node).collect())
    }

    /// Search nodes by tag
    ///
    /// Returns all nodes that have the specified tag.
    ///
    /// # Errors
    ///
    /// This function does not fail; it always returns Ok with results.
    pub fn search_by_tag(&self, tag: &str) -> Result<Vec<Node>> {
        Ok(self
            .graph
            .nodes
            .values()
            .filter(|node| node.tags.iter().any(|t| t.eq_ignore_ascii_case(tag)))
            .cloned()
            .collect())
    }

    /// Search nodes by regex pattern
    ///
    /// Searches content and title using regex.
    ///
    /// # Errors
    ///
    /// Returns error if regex pattern is invalid.
    pub fn search_regex(&self, pattern: &str) -> Result<Vec<Node>> {
        let re = Regex::new(pattern)
            .map_err(|e| crate::error::KbError::Query(format!("Invalid regex pattern: {e}")))?;

        Ok(self
            .graph
            .nodes
            .values()
            .filter(|node| re.is_match(&node.title) || re.is_match(&node.content))
            .cloned()
            .collect())
    }

    /// Search nodes by type
    ///
    /// Returns all nodes of the specified type.
    ///
    /// # Errors
    ///
    /// This function does not fail; it always returns Ok with results.
    pub fn search_by_type(&self, node_type: crate::models::NodeType) -> Result<Vec<Node>> {
        Ok(self
            .graph
            .nodes
            .values()
            .filter(|node| node.node_type == node_type)
            .cloned()
            .collect())
    }

    /// Get all nodes related to the given node
    ///
    /// Returns nodes that have incoming or outgoing edges.
    ///
    /// # Errors
    ///
    /// This function does not fail; it always returns Ok with results.
    pub fn get_related_nodes(&self, node_id: &str) -> Result<Vec<Node>> {
        let mut related_ids = std::collections::HashSet::new();

        // Add nodes that this node relates to
        if let Some(node) = self.graph.nodes.get(node_id) {
            related_ids.extend(node.relates_to.iter().cloned());
            related_ids.extend(node.depends_on.iter().cloned());
            related_ids.extend(node.implements.iter().cloned());
            related_ids.extend(node.extends.iter().cloned());
        }

        // Add nodes that relate to this node
        for node in self.graph.nodes.values() {
            if node.relates_to.contains(&node_id.to_string())
                || node.depends_on.contains(&node_id.to_string())
                || node.implements.contains(&node_id.to_string())
                || node.extends.contains(&node_id.to_string())
            {
                related_ids.insert(node.id.clone());
            }
        }

        Ok(related_ids
            .into_iter()
            .filter_map(|id| self.graph.nodes.get(&id).cloned())
            .collect())
    }
}
