//! Search query types and filters.

use serde::{Deserialize, Serialize};
use std::path::PathBuf;

use crate::config::SearchMode;
use crate::types::{ChunkType, Language};

/// A search query with options.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchQuery {
    /// The search text.
    pub text: String,
    /// Search mode (lexical, semantic, or hybrid).
    pub mode: SearchMode,
    /// Maximum number of results.
    pub limit: usize,
    /// Minimum score threshold (0.0 to 1.0).
    pub min_score: f32,
    /// Optional filters.
    pub filter: Option<SearchFilter>,
}

impl SearchQuery {
    /// Create a new search query.
    pub fn new(text: impl Into<String>) -> Self {
        Self {
            text: text.into(),
            mode: SearchMode::Hybrid,
            limit: 20,
            min_score: 0.0,
            filter: None,
        }
    }

    /// Set the search mode.
    pub fn mode(mut self, mode: SearchMode) -> Self {
        self.mode = mode;
        self
    }

    /// Set the result limit.
    pub fn limit(mut self, limit: usize) -> Self {
        self.limit = limit;
        self
    }

    /// Set the minimum score threshold.
    pub fn min_score(mut self, score: f32) -> Self {
        self.min_score = score.clamp(0.0, 1.0);
        self
    }

    /// Set a filter.
    pub fn filter(mut self, filter: SearchFilter) -> Self {
        self.filter = Some(filter);
        self
    }

    /// Extract search terms from the query text.
    pub fn terms(&self) -> Vec<String> {
        self.text
            .split_whitespace()
            .filter(|s| s.len() >= 2) // Skip very short terms
            .map(|s| s.to_lowercase())
            .collect()
    }

    /// Check if this is an empty query.
    pub fn is_empty(&self) -> bool {
        self.text.trim().is_empty()
    }
}

/// Filters to apply to search results.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SearchFilter {
    /// Filter by languages.
    pub languages: Option<Vec<Language>>,
    /// Filter by chunk types.
    pub chunk_types: Option<Vec<ChunkType>>,
    /// Filter by file path patterns (glob).
    pub path_patterns: Option<Vec<String>>,
    /// Filter by symbol names (partial match).
    pub symbol_names: Option<Vec<String>>,
    /// Include only files in these directories.
    pub directories: Option<Vec<PathBuf>>,
    /// Exclude files in these directories.
    pub exclude_directories: Option<Vec<PathBuf>>,
}

impl SearchFilter {
    /// Create an empty filter.
    pub fn new() -> Self {
        Self::default()
    }

    /// Filter by languages.
    pub fn languages(mut self, languages: Vec<Language>) -> Self {
        self.languages = Some(languages);
        self
    }

    /// Filter by chunk types.
    pub fn chunk_types(mut self, types: Vec<ChunkType>) -> Self {
        self.chunk_types = Some(types);
        self
    }

    /// Filter by path patterns.
    pub fn path_patterns(mut self, patterns: Vec<String>) -> Self {
        self.path_patterns = Some(patterns);
        self
    }

    /// Filter by symbol names.
    pub fn symbol_names(mut self, names: Vec<String>) -> Self {
        self.symbol_names = Some(names);
        self
    }

    /// Include only files in specific directories.
    pub fn in_directories(mut self, dirs: Vec<PathBuf>) -> Self {
        self.directories = Some(dirs);
        self
    }

    /// Exclude files in specific directories.
    pub fn exclude_directories(mut self, dirs: Vec<PathBuf>) -> Self {
        self.exclude_directories = Some(dirs);
        self
    }

    /// Check if a chunk matches the filter.
    pub fn matches(
        &self,
        chunk: &crate::types::Chunk,
        document: &crate::types::Document,
    ) -> bool {
        // Check language filter
        if let Some(ref languages) = self.languages {
            if !languages.contains(&document.language) {
                return false;
            }
        }

        // Check chunk type filter
        if let Some(ref types) = self.chunk_types {
            if !types.contains(&chunk.chunk_type) {
                return false;
            }
        }

        // Check path pattern filter
        if let Some(ref patterns) = self.path_patterns {
            let path_str = document.relative_path.to_string_lossy();
            let matches_pattern = patterns.iter().any(|pattern| {
                glob::Pattern::new(pattern)
                    .map(|p| p.matches(&path_str))
                    .unwrap_or(false)
            });
            if !matches_pattern {
                return false;
            }
        }

        // Check symbol name filter
        if let Some(ref names) = self.symbol_names {
            if let Some(ref symbol) = chunk.symbol_name {
                let symbol_lower = symbol.to_lowercase();
                let matches_symbol = names
                    .iter()
                    .any(|n| symbol_lower.contains(&n.to_lowercase()));
                if !matches_symbol {
                    return false;
                }
            } else {
                return false;
            }
        }

        // Check directory inclusion
        if let Some(ref dirs) = self.directories {
            let in_dir = dirs.iter().any(|dir| {
                document.relative_path.starts_with(dir)
            });
            if !in_dir {
                return false;
            }
        }

        // Check directory exclusion
        if let Some(ref dirs) = self.exclude_directories {
            let in_excluded = dirs.iter().any(|dir| {
                document.relative_path.starts_with(dir)
            });
            if in_excluded {
                return false;
            }
        }

        true
    }

    /// Check if the filter is empty (no constraints).
    pub fn is_empty(&self) -> bool {
        self.languages.is_none()
            && self.chunk_types.is_none()
            && self.path_patterns.is_none()
            && self.symbol_names.is_none()
            && self.directories.is_none()
            && self.exclude_directories.is_none()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_search_query() {
        let query = SearchQuery::new("fn main")
            .mode(SearchMode::Lexical)
            .limit(10)
            .min_score(0.5);

        assert_eq!(query.text, "fn main");
        assert_eq!(query.mode, SearchMode::Lexical);
        assert_eq!(query.limit, 10);
        assert_eq!(query.min_score, 0.5);
    }

    #[test]
    fn test_query_terms() {
        let query = SearchQuery::new("hello world a test");
        let terms = query.terms();

        assert_eq!(terms.len(), 3); // "a" is too short
        assert!(terms.contains(&"hello".to_string()));
        assert!(terms.contains(&"world".to_string()));
        assert!(terms.contains(&"test".to_string()));
    }

    #[test]
    fn test_filter_language() {
        let filter = SearchFilter::new().languages(vec![Language::Rust]);

        let chunk = crate::types::Chunk {
            id: crate::types::ChunkId::new(),
            document_id: crate::types::DocumentId::new(),
            content: "test".to_string(),
            chunk_type: ChunkType::Function,
            start_line: 1,
            end_line: 1,
            start_byte: 0,
            end_byte: 4,
            symbol_name: None,
            parent_symbol: None,
        };

        let rust_doc = crate::types::Document {
            id: crate::types::DocumentId::new(),
            relative_path: PathBuf::from("test.rs"),
            absolute_path: PathBuf::from("/test.rs"),
            language: Language::Rust,
            content_hash: "hash".to_string(),
            size_bytes: 100,
            modified_at: chrono::Utc::now(),
        };

        let py_doc = crate::types::Document {
            id: crate::types::DocumentId::new(),
            relative_path: PathBuf::from("test.py"),
            absolute_path: PathBuf::from("/test.py"),
            language: Language::Python,
            content_hash: "hash".to_string(),
            size_bytes: 100,
            modified_at: chrono::Utc::now(),
        };

        assert!(filter.matches(&chunk, &rust_doc));
        assert!(!filter.matches(&chunk, &py_doc));
    }
}
