//! Lexical (keyword-based) search using Tantivy.

use std::collections::HashMap;
use std::path::Path;

use tantivy::collector::TopDocs;
use tantivy::query::{BooleanQuery, FuzzyTermQuery, Occur, Query, TermQuery};
use tantivy::schema::{Field, Schema, STORED, STRING, TEXT, OwnedValue};
use tantivy::{doc, Index, IndexReader, IndexWriter, TantivyDocument, Term};

use crate::config::SearchConfig;
use crate::error::{Error, Result};
use crate::search::{create_highlights, normalize_scores, SearchQuery};
use crate::types::{Chunk, Document, MatchType, SearchResult};

/// Tantivy-based lexical search index.
pub struct LexicalIndex {
    index: Index,
    reader: IndexReader,
    #[allow(dead_code)]
    schema: Schema,
    fields: LexicalFields,
    config: SearchConfig,
}

/// Field definitions for the lexical index.
struct LexicalFields {
    chunk_id: Field,
    document_id: Field,
    content: Field,
    symbol_name: Field,
    path: Field,
    language: Field,
    chunk_type: Field,
}

impl LexicalIndex {
    /// Create a new lexical index in the given directory.
    pub fn create(index_dir: &Path, config: SearchConfig) -> Result<Self> {
        std::fs::create_dir_all(index_dir)?;

        let (schema, fields) = Self::build_schema();
        let index = Index::create_in_dir(index_dir, schema.clone())?;
        let reader = index.reader()?;

        Ok(Self {
            index,
            reader,
            schema,
            fields,
            config,
        })
    }

    /// Open an existing lexical index.
    pub fn open(index_dir: &Path, config: SearchConfig) -> Result<Self> {
        let (schema, fields) = Self::build_schema();
        let index = Index::open_in_dir(index_dir)?;
        let reader = index.reader()?;

        Ok(Self {
            index,
            reader,
            schema,
            fields,
            config,
        })
    }

    /// Build the schema for the index.
    fn build_schema() -> (Schema, LexicalFields) {
        let mut schema_builder = Schema::builder();

        let chunk_id = schema_builder.add_text_field("chunk_id", STRING | STORED);
        let document_id = schema_builder.add_text_field("document_id", STRING | STORED);
        let content = schema_builder.add_text_field("content", TEXT | STORED);
        let symbol_name = schema_builder.add_text_field("symbol_name", TEXT | STORED);
        let path = schema_builder.add_text_field("path", STRING | STORED);
        let language = schema_builder.add_text_field("language", STRING);
        let chunk_type = schema_builder.add_text_field("chunk_type", STRING);

        let schema = schema_builder.build();
        let fields = LexicalFields {
            chunk_id,
            document_id,
            content,
            symbol_name,
            path,
            language,
            chunk_type,
        };

        (schema, fields)
    }

    /// Get an index writer.
    pub fn writer(&self) -> Result<IndexWriter> {
        let writer = self.index.writer(50_000_000)?; // 50MB heap
        Ok(writer)
    }

    /// Index chunks.
    pub fn index_chunks(
        &self,
        chunks: &[Chunk],
        documents: &HashMap<String, &Document>,
    ) -> Result<()> {
        let mut writer = self.writer()?;

        for chunk in chunks {
            let doc_id = chunk.document_id.0.to_string();
            let document = documents.get(&doc_id);
            let (path, language) = match document {
                Some(doc) => (
                    doc.relative_path.to_string_lossy().to_string(),
                    doc.language.as_str().to_string(),
                ),
                None => ("unknown".to_string(), "unknown".to_string()),
            };

            let tantivy_doc = doc!(
                self.fields.chunk_id => chunk.id.0.to_string(),
                self.fields.document_id => doc_id,
                self.fields.content => chunk.content.clone(),
                self.fields.symbol_name => chunk.symbol_name.as_deref().unwrap_or(""),
                self.fields.path => path,
                self.fields.language => language,
                self.fields.chunk_type => format!("{:?}", chunk.chunk_type)
            );

            writer.add_document(tantivy_doc)?;
        }

        writer.commit()?;
        self.reader.reload()?;

        Ok(())
    }

    /// Remove chunks from the index.
    #[allow(dead_code)]
    pub fn remove_chunks(&self, chunk_ids: &[String]) -> Result<()> {
        let mut writer = self.writer()?;

        for chunk_id in chunk_ids {
            let term = Term::from_field_text(self.fields.chunk_id, chunk_id);
            writer.delete_term(term);
        }

        writer.commit()?;
        self.reader.reload()?;

        Ok(())
    }

    /// Clear the entire index.
    #[allow(dead_code)]
    pub fn clear(&self) -> Result<()> {
        let mut writer = self.writer()?;
        writer.delete_all_documents()?;
        writer.commit()?;
        self.reader.reload()?;
        Ok(())
    }

    /// Build a query from search text.
    fn build_query(&self, query: &SearchQuery) -> Result<Box<dyn Query>> {
        let terms = query.terms();
        if terms.is_empty() {
            return Err(Error::invalid_query("Empty query"));
        }

        let mut subqueries: Vec<(Occur, Box<dyn Query>)> = Vec::new();

        for term in &terms {
            // Content search (full-text)
            let content_term = Term::from_field_text(self.fields.content, term);
            if self.config.fuzzy_matching {
                let fuzzy = FuzzyTermQuery::new(content_term, self.config.fuzzy_distance, true);
                subqueries.push((Occur::Should, Box::new(fuzzy)));
            } else {
                let exact = TermQuery::new(content_term, tantivy::schema::IndexRecordOption::Basic);
                subqueries.push((Occur::Should, Box::new(exact)));
            }

            // Symbol name search (boosted)
            let symbol_term = Term::from_field_text(self.fields.symbol_name, term);
            let symbol_query = TermQuery::new(symbol_term, tantivy::schema::IndexRecordOption::Basic);
            // Note: Tantivy doesn't have direct boosting in TermQuery,
            // but symbol matches will naturally score higher
            subqueries.push((Occur::Should, Box::new(symbol_query)));
        }

        Ok(Box::new(BooleanQuery::new(subqueries)))
    }

    /// Extract text value from OwnedValue
    fn extract_text(value: Option<&OwnedValue>) -> &str {
        match value {
            Some(OwnedValue::Str(s)) => s.as_str(),
            _ => "",
        }
    }

    /// Search the index.
    pub fn search_index(
        &self,
        query: &SearchQuery,
        chunk_map: &HashMap<String, &Chunk>,
        doc_map: &HashMap<String, &Document>,
    ) -> Result<Vec<SearchResult>> {
        let tantivy_query = self.build_query(query)?;
        let searcher = self.reader.searcher();

        let top_docs = searcher.search(
            &tantivy_query,
            &TopDocs::with_limit(query.limit * 2), // Fetch more for filtering
        )?;

        let mut results = Vec::new();

        for (score, doc_address) in top_docs {
            let retrieved_doc: TantivyDocument = searcher.doc(doc_address)?;

            let chunk_id = Self::extract_text(retrieved_doc.get_first(self.fields.chunk_id));
            let document_id = Self::extract_text(retrieved_doc.get_first(self.fields.document_id));

            // Look up the full chunk and document
            if let (Some(chunk), Some(document)) = (
                chunk_map.get(chunk_id),
                doc_map.get(document_id),
            ) {
                // Apply filter
                if let Some(ref filter) = query.filter {
                    if !filter.matches(chunk, document) {
                        continue;
                    }
                }

                let highlights = create_highlights(&chunk.content, &query.terms());

                results.push(SearchResult {
                    chunk: (*chunk).clone(),
                    document: (*document).clone(),
                    score,
                    match_type: MatchType::Lexical,
                    highlights,
                });
            }
        }

        // Normalize and filter by score
        normalize_scores(&mut results);
        results.retain(|r| r.score >= query.min_score);
        results.truncate(query.limit);

        Ok(results)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::types::{ChunkId, ChunkType, DocumentId, Language};
    use std::path::PathBuf;
    use tempfile::TempDir;

    fn make_test_data() -> (Vec<Chunk>, Vec<Document>) {
        let doc_id = DocumentId::new();
        let doc = Document {
            id: doc_id.clone(),
            relative_path: PathBuf::from("test.rs"),
            absolute_path: PathBuf::from("/test/test.rs"),
            language: Language::Rust,
            content_hash: "hash".to_string(),
            size_bytes: 100,
            modified_at: chrono::Utc::now(),
        };

        let chunks = vec![
            Chunk {
                id: ChunkId::new(),
                document_id: doc_id.clone(),
                content: "fn hello_world() { println!(\"Hello, world!\"); }".to_string(),
                chunk_type: ChunkType::Function,
                start_line: 1,
                end_line: 1,
                start_byte: 0,
                end_byte: 47,
                symbol_name: Some("hello_world".to_string()),
                parent_symbol: None,
            },
            Chunk {
                id: ChunkId::new(),
                document_id: doc_id.clone(),
                content: "fn add(a: i32, b: i32) -> i32 { a + b }".to_string(),
                chunk_type: ChunkType::Function,
                start_line: 3,
                end_line: 3,
                start_byte: 50,
                end_byte: 88,
                symbol_name: Some("add".to_string()),
                parent_symbol: None,
            },
        ];

        (chunks, vec![doc])
    }

    #[test]
    fn test_lexical_index_create_and_search() {
        let temp_dir = TempDir::new().unwrap();
        let config = SearchConfig::default();
        let index = LexicalIndex::create(temp_dir.path(), config).unwrap();

        let (chunks, documents) = make_test_data();

        // Build document map
        let doc_map: HashMap<String, &Document> = documents
            .iter()
            .map(|d| (d.id.0.to_string(), d))
            .collect();

        // Build chunk map
        let chunk_map: HashMap<String, &Chunk> = chunks
            .iter()
            .map(|c| (c.id.0.to_string(), c))
            .collect();

        // Index chunks
        index.index_chunks(&chunks, &doc_map).unwrap();

        // Search
        let query = SearchQuery::new("hello");
        let results = index.search_index(&query, &chunk_map, &doc_map).unwrap();

        assert!(!results.is_empty());
        assert!(results[0].chunk.content.contains("hello"));
    }
}
