//! Full-text search trait definitions
//!
//! Provides the `FullTextIndex` trait that abstracts over different
//! full-text search engine implementations (Tantivy, etc.).

use std::collections::HashMap;
use serde::{Deserialize, Serialize};

/// Error type for full-text search operations
#[derive(Debug, thiserror::Error)]
pub enum FTSError {
    /// Index IO error
    #[error("FTS IO error: {0}")]
    Io(#[from] std::io::Error),

    /// Index schema error
    #[error("FTS schema error: {0}")]
    Schema(String),

    /// Query parse error
    #[error("FTS query parse error: {0}")]
    QueryParse(String),

    /// Index corruption
    #[error("FTS index corrupted: {0}")]
    IndexCorrupted(String),

    /// Generic internal error
    #[error("FTS internal error: {0}")]
    Internal(String),
}

pub type FTSResult<T> = Result<T, FTSError>;

/// A single full-text search result
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FTSSearchResult {
    /// Document ID (maps to MemoryNode.id)
    pub id: String,

    /// BM25 relevance score (higher = more relevant)
    pub score: f64,

    /// Optional snippet with matched terms highlighted
    pub snippet: Option<String>,

    /// Which fields matched (e.g., "content", "tags")
    pub matched_fields: Vec<String>,
}

/// Query mode for full-text search
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FTSQueryMode {
    /// Standard keyword search with BM25 ranking
    /// Splits query into terms and searches across all indexed fields
    Standard,

    /// Exact phrase match — terms must appear in order
    Phrase,

    /// Fuzzy search with configurable edit distance (default = 1)
    Fuzzy {
        /// Maximum edit distance (1 or 2)
        distance: u8,
    },

    /// Boolean query with explicit AND / OR / NOT operators
    /// Syntax: `rust AND database NOT sql`, `"exact phrase" OR keyword`
    Boolean,
}

impl Default for FTSQueryMode {
    fn default() -> Self {
        Self::Standard
    }
}

/// Full-text search query parameters
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FTSQuery {
    /// The search text
    pub text: String,

    /// Query mode (Standard / Phrase / Fuzzy / Boolean)
    #[serde(default)]
    pub mode: FTSQueryMode,

    /// Maximum number of results
    #[serde(default = "default_fts_limit")]
    pub limit: usize,

    /// Result offset for pagination
    #[serde(default)]
    pub offset: usize,

    /// Which fields to search in (None = all indexed fields)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub fields: Option<Vec<String>>,

    /// Whether to generate highlighted snippets
    #[serde(default = "default_true")]
    pub highlight: bool,

    /// Maximum snippet length in characters
    #[serde(default = "default_snippet_len")]
    pub snippet_max_len: usize,

    /// Per-field relevance boost factors (Phase-73 T73.4)
    ///
    /// Example: `{"content": 1.0, "tags": 2.0, "metadata": 0.5}`
    /// Fields not listed use a default boost of 1.0.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub field_boosts: Option<HashMap<String, f64>>,

    /// Whether to expand query using synonym map (Phase-73 T73.1)
    ///
    /// Defaults to `true`. When enabled and a synonym map is loaded,
    /// query terms are expanded into OR-combined synonym groups.
    #[serde(default = "default_true")]
    pub expand_synonyms: bool,
}

fn default_fts_limit() -> usize { 10 }
fn default_true() -> bool { true }
fn default_snippet_len() -> usize { 200 }

impl FTSQuery {
    /// Create a standard keyword search query
    pub fn new(text: impl Into<String>) -> Self {
        Self {
            text: text.into(),
            mode: FTSQueryMode::Standard,
            limit: 10,
            offset: 0,
            fields: None,
            highlight: true,
            snippet_max_len: 200,
            field_boosts: None,
            expand_synonyms: true,
        }
    }

    /// Create a phrase search query
    pub fn phrase(text: impl Into<String>) -> Self {
        Self {
            mode: FTSQueryMode::Phrase,
            ..Self::new(text)
        }
    }

    /// Create a fuzzy search query
    pub fn fuzzy(text: impl Into<String>, distance: u8) -> Self {
        Self {
            mode: FTSQueryMode::Fuzzy { distance: distance.min(2) },
            ..Self::new(text)
        }
    }

    /// Create a boolean query
    pub fn boolean(text: impl Into<String>) -> Self {
        Self {
            mode: FTSQueryMode::Boolean,
            ..Self::new(text)
        }
    }

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

    /// Set offset
    pub fn with_offset(mut self, offset: usize) -> Self {
        self.offset = offset;
        self
    }

    /// Restrict search to specific fields
    pub fn with_fields(mut self, fields: Vec<String>) -> Self {
        self.fields = Some(fields);
        self
    }

    /// Disable snippet highlighting
    pub fn no_highlight(mut self) -> Self {
        self.highlight = false;
        self
    }

    /// Set per-field relevance boost factors
    ///
    /// Example: `query.with_field_boosts(HashMap::from([("tags".into(), 2.0)]))`
    pub fn with_field_boosts(mut self, boosts: HashMap<String, f64>) -> Self {
        self.field_boosts = Some(boosts);
        self
    }

    /// Disable synonym expansion for this query
    pub fn no_synonym_expansion(mut self) -> Self {
        self.expand_synonyms = false;
        self
    }
}

/// A document to be indexed for full-text search
#[derive(Debug, Clone)]
pub struct FTSDocument {
    /// Document ID (must match MemoryNode.id)
    pub id: String,

    /// Main content text
    pub content: String,

    /// Tags (searchable as space-separated string)
    pub tags: Vec<String>,

    /// Metadata key-value pairs (searchable as JSON text)
    pub metadata: HashMap<String, String>,
}

/// Tokenizer language selection (Phase-73 T73.5)
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum TokenizerLanguage {
    /// Chinese — uses jieba-rs (default)
    Chinese,
    /// Japanese — uses lindera with ipadic dictionary
    Japanese,
    /// Korean — uses lindera with ko-dic dictionary
    Korean,
    /// English — uses Tantivy default tokenizer
    English,
    /// Auto-detect (defaults to Chinese for backward compatibility)
    Auto,
}

impl Default for TokenizerLanguage {
    fn default() -> Self {
        Self::Auto
    }
}

/// Configuration for full-text search index
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FTSConfig {
    /// Index storage directory path
    pub index_path: String,

    /// Heap size for indexing in bytes (default: 50MB)
    #[serde(default = "default_heap_size")]
    pub heap_size: usize,

    /// Enable Chinese tokenizer (jieba-rs)
    /// Deprecated: use `language` field instead. Kept for backward compatibility.
    #[serde(default = "default_true")]
    pub enable_chinese: bool,

    /// Number of indexer threads (0 = auto-detect)
    #[serde(default)]
    pub num_threads: usize,

    // ─── Phase-73 additions ─────────────────────────────────

    /// Path to synonym map JSON file (Phase-73 T73.1)
    ///
    /// File format:
    /// ```json
    /// { "entries": [{ "term": "数据库", "synonyms": ["DB"], "bidirectional": true }] }
    /// ```
    #[serde(skip_serializing_if = "Option::is_none")]
    pub synonym_path: Option<String>,

    /// Path to jieba user dictionary file (Phase-73 T73.2)
    ///
    /// Format: one entry per line — `词语 词频 词性`
    /// Example: `向量数据库 5 n`
    #[serde(skip_serializing_if = "Option::is_none")]
    pub user_dict_path: Option<String>,

    /// Enable stop word filtering (Phase-73 T73.3)
    #[serde(default)]
    pub enable_stop_words: bool,

    /// Path to custom stop words file — one word per line (Phase-73 T73.3)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub custom_stop_words_path: Option<String>,

    /// Enable English stemming (SnowballStemmer) (Phase-73 T73.3)
    #[serde(default)]
    pub enable_stemmer: bool,

    /// Primary tokenizer language (Phase-73 T73.5)
    ///
    /// Determines which tokenizer is registered:
    /// - `Chinese` → jieba-rs
    /// - `Japanese` → lindera (ipadic)
    /// - `Korean` → lindera (ko-dic)
    /// - `English` → Tantivy default
    /// - `Auto` → uses `enable_chinese` for backward compat
    #[serde(default)]
    pub language: TokenizerLanguage,

    /// Default field boost weights applied when query doesn't specify boosts (Phase-73 T73.4)
    ///
    /// Example: `{"content": 1.0, "tags": 1.5, "metadata": 0.5}`
    #[serde(skip_serializing_if = "Option::is_none")]
    pub default_field_boosts: Option<HashMap<String, f64>>,
}

fn default_heap_size() -> usize { 50 * 1024 * 1024 } // 50MB

impl Default for FTSConfig {
    fn default() -> Self {
        Self {
            index_path: "fts_index".to_string(),
            heap_size: default_heap_size(),
            enable_chinese: true,
            num_threads: 0,
            synonym_path: None,
            user_dict_path: None,
            enable_stop_words: false,
            custom_stop_words_path: None,
            enable_stemmer: false,
            language: TokenizerLanguage::Auto,
            default_field_boosts: None,
        }
    }
}

impl FTSConfig {
    /// Create config with a specific index path
    pub fn new(index_path: impl Into<String>) -> Self {
        Self {
            index_path: index_path.into(),
            ..Default::default()
        }
    }

    /// Set synonym file path
    pub fn with_synonyms(mut self, path: impl Into<String>) -> Self {
        self.synonym_path = Some(path.into());
        self
    }

    /// Set user dictionary path for jieba
    pub fn with_user_dict(mut self, path: impl Into<String>) -> Self {
        self.user_dict_path = Some(path.into());
        self
    }

    /// Enable stop word filtering
    pub fn with_stop_words(mut self) -> Self {
        self.enable_stop_words = true;
        self
    }

    /// Set custom stop words file path
    pub fn with_custom_stop_words(mut self, path: impl Into<String>) -> Self {
        self.enable_stop_words = true;
        self.custom_stop_words_path = Some(path.into());
        self
    }

    /// Enable English stemming
    pub fn with_stemmer(mut self) -> Self {
        self.enable_stemmer = true;
        self
    }

    /// Set tokenizer language
    pub fn with_language(mut self, lang: TokenizerLanguage) -> Self {
        // Sync backward-compat flag
        self.enable_chinese = matches!(lang, TokenizerLanguage::Chinese | TokenizerLanguage::Auto);
        self.language = lang;
        self
    }

    /// Set default field boost weights
    pub fn with_default_boosts(mut self, boosts: HashMap<String, f64>) -> Self {
        self.default_field_boosts = Some(boosts);
        self
    }

    /// Resolve the effective language (handles Auto → Chinese fallback)
    pub fn effective_language(&self) -> TokenizerLanguage {
        match &self.language {
            TokenizerLanguage::Auto => {
                if self.enable_chinese {
                    TokenizerLanguage::Chinese
                } else {
                    TokenizerLanguage::English
                }
            }
            other => other.clone(),
        }
    }
}

/// Trait for full-text search index implementations
///
/// This trait provides a backend-agnostic interface for full-text search.
/// The primary implementation is `TantivyFullTextIndex` using the Tantivy engine.
///
/// # Example
///
/// ```rust,no_run
/// use aifastdb_core::search::{FullTextIndex, FTSQuery, FTSDocument};
///
/// fn search_documents(index: &dyn FullTextIndex) {
///     let results = index.search(&FTSQuery::new("rust database")).unwrap();
///     for r in results {
///         println!("{}: score={:.3}", r.id, r.score);
///     }
/// }
/// ```
pub trait FullTextIndex: Send + Sync {
    /// Add a document to the full-text index
    fn add_document(&self, doc: &FTSDocument) -> FTSResult<()>;

    /// Add multiple documents in a batch (more efficient than individual adds)
    fn add_documents(&self, docs: &[FTSDocument]) -> FTSResult<usize>;

    /// Delete a document from the index by ID
    fn delete_document(&self, id: &str) -> FTSResult<()>;

    /// Update a document (delete + re-add)
    fn update_document(&self, doc: &FTSDocument) -> FTSResult<()> {
        self.delete_document(&doc.id)?;
        self.add_document(doc)?;
        Ok(())
    }

    /// Execute a full-text search query
    fn search(&self, query: &FTSQuery) -> FTSResult<Vec<FTSSearchResult>>;

    /// Get the total number of indexed documents
    fn doc_count(&self) -> FTSResult<u64>;

    /// Rebuild the entire index from scratch
    fn rebuild_index(&self) -> FTSResult<()>;

    /// Commit pending changes to disk
    fn commit(&self) -> FTSResult<()>;

    /// Get the name of the FTS backend
    fn backend_name(&self) -> &str;

    // ─── Phase-73 hot reload methods (T73.6) ────────────────

    /// Reload synonym map from a file path
    ///
    /// Returns `Ok(count)` with the number of synonym entries loaded.
    /// Default implementation returns an error (not supported).
    fn reload_synonyms(&self, _path: &str) -> FTSResult<usize> {
        Err(FTSError::Internal("reload_synonyms not supported by this backend".into()))
    }

    /// Reload user dictionary from a file path
    ///
    /// Default implementation returns an error (not supported).
    fn reload_user_dict(&self, _path: &str) -> FTSResult<()> {
        Err(FTSError::Internal("reload_user_dict not supported by this backend".into()))
    }

    /// Update stop words list at runtime
    ///
    /// Default implementation returns an error (not supported).
    fn update_stop_words(&self, _words: Vec<String>) -> FTSResult<()> {
        Err(FTSError::Internal("update_stop_words not supported by this backend".into()))
    }

    /// Get the current FTS configuration snapshot
    fn get_config(&self) -> Option<&FTSConfig> {
        None
    }
}

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

    #[test]
    fn test_fts_query_builder() {
        let q = FTSQuery::new("hello world")
            .with_limit(20)
            .with_offset(5)
            .with_fields(vec!["content".into()])
            .no_highlight();

        assert_eq!(q.text, "hello world");
        assert_eq!(q.limit, 20);
        assert_eq!(q.offset, 5);
        assert_eq!(q.fields.as_ref().unwrap().len(), 1);
        assert!(!q.highlight);
    }

    #[test]
    fn test_fts_query_phrase() {
        let q = FTSQuery::phrase("exact match");
        assert!(matches!(q.mode, FTSQueryMode::Phrase));
    }

    #[test]
    fn test_fts_query_fuzzy() {
        let q = FTSQuery::fuzzy("misspelled", 2);
        assert!(matches!(q.mode, FTSQueryMode::Fuzzy { distance: 2 }));
    }

    #[test]
    fn test_fts_query_fuzzy_clamped() {
        let q = FTSQuery::fuzzy("test", 5); // should be clamped to 2
        assert!(matches!(q.mode, FTSQueryMode::Fuzzy { distance: 2 }));
    }

    #[test]
    fn test_fts_config_default() {
        let config = FTSConfig::default();
        assert!(config.enable_chinese);
        assert_eq!(config.heap_size, 50 * 1024 * 1024);
    }

    #[test]
    fn test_fts_document_creation() {
        let doc = FTSDocument {
            id: "doc-1".to_string(),
            content: "Hello world".to_string(),
            tags: vec!["test".to_string()],
            metadata: HashMap::new(),
        };
        assert_eq!(doc.id, "doc-1");
    }
}
