//! Synonym engine for full-text search
//!
//! Provides `SynonymMap` supporting:
//! - Bidirectional synonyms: "数据库" ↔ ["DB", "database"]
//! - Unidirectional synonyms: "AI" → ["artificial intelligence"]
//! - Loading from JSON files
//! - Query expansion: "数据库" → BooleanQuery(OR: ["数据库", "DB", "database"])

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

use serde::{Deserialize, Serialize};

use super::traits::{FTSError, FTSResult};

/// A single synonym entry
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SynonymEntry {
    /// The source term
    pub term: String,
    /// List of synonyms
    pub synonyms: Vec<String>,
    /// Whether this mapping is bidirectional
    #[serde(default = "default_true")]
    pub bidirectional: bool,
}

fn default_true() -> bool {
    true
}

/// Synonym file format (JSON)
///
/// Example JSON:
/// ```json
/// {
///   "entries": [
///     { "term": "数据库", "synonyms": ["DB", "database", "資料庫"], "bidirectional": true },
///     { "term": "AI", "synonyms": ["artificial intelligence", "人工智能"], "bidirectional": false }
///   ]
/// }
/// ```
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SynonymFile {
    /// Synonym entries
    pub entries: Vec<SynonymEntry>,
}

/// Synonym map for query expansion
///
/// Supports both bidirectional and unidirectional synonym mappings.
/// Used at query-parse time to expand search terms into OR-combined alternatives.
///
/// # Example
///
/// ```rust,ignore
/// let mut map = SynonymMap::new();
/// map.add_bidirectional("数据库", vec!["DB", "database"]);
/// let expanded = map.expand("数据库");
/// // expanded = ["数据库", "DB", "database"]
/// ```
#[derive(Debug, Clone, Default)]
pub struct SynonymMap {
    /// Forward mapping: term → [synonyms]
    /// Every term maps to its full synonym group (including itself)
    forward: HashMap<String, Vec<String>>,
}

impl SynonymMap {
    /// Create an empty synonym map
    pub fn new() -> Self {
        Self {
            forward: HashMap::new(),
        }
    }

    /// Add a bidirectional synonym group
    ///
    /// All terms in the group become interchangeable:
    /// `add_bidirectional("数据库", ["DB", "database"])` means:
    /// - searching "数据库" also matches "DB" and "database"
    /// - searching "DB" also matches "数据库" and "database"
    /// - searching "database" also matches "数据库" and "DB"
    pub fn add_bidirectional(&mut self, term: &str, synonyms: Vec<&str>) {
        let normalized_term = term.to_lowercase();
        let mut group: Vec<String> = vec![normalized_term.clone()];
        for s in &synonyms {
            let normalized = s.to_lowercase();
            if !group.contains(&normalized) {
                group.push(normalized);
            }
        }

        // Every member of the group maps to all OTHER members
        for member in &group {
            let others: Vec<String> = group.iter().filter(|g| *g != member).cloned().collect();
            let entry = self.forward.entry(member.clone()).or_insert_with(Vec::new);
            for o in others {
                if !entry.contains(&o) {
                    entry.push(o);
                }
            }
        }
    }

    /// Add a unidirectional synonym mapping
    ///
    /// Only expands from `term` to `synonyms`, not the reverse:
    /// `add_unidirectional("AI", ["artificial intelligence"])` means:
    /// - searching "AI" also matches "artificial intelligence"
    /// - but searching "artificial intelligence" does NOT match "AI"
    pub fn add_unidirectional(&mut self, term: &str, synonyms: Vec<&str>) {
        let normalized_term = term.to_lowercase();
        let entry = self.forward.entry(normalized_term).or_insert_with(Vec::new);
        for s in synonyms {
            let normalized = s.to_lowercase();
            if !entry.contains(&normalized) {
                entry.push(normalized);
            }
        }
    }

    /// Expand a single term into all its synonyms (including itself)
    ///
    /// Returns `None` if the term has no synonyms.
    pub fn expand(&self, term: &str) -> Option<Vec<String>> {
        let normalized = term.to_lowercase();
        self.forward.get(&normalized).map(|synonyms| {
            let mut result = vec![normalized.clone()];
            for s in synonyms {
                if !result.contains(s) {
                    result.push(s.clone());
                }
            }
            result
        })
    }

    /// Expand all terms in a query text
    ///
    /// Splits by whitespace, expands each term, returns the expanded terms grouped.
    /// Returns `None` if no expansions were made.
    pub fn expand_query(&self, query_text: &str) -> Option<Vec<Vec<String>>> {
        let terms: Vec<&str> = query_text.split_whitespace().collect();
        let mut any_expanded = false;
        let mut result = Vec::with_capacity(terms.len());

        for term in terms {
            if let Some(expanded) = self.expand(term) {
                any_expanded = true;
                result.push(expanded);
            } else {
                result.push(vec![term.to_lowercase()]);
            }
        }

        if any_expanded {
            Some(result)
        } else {
            None
        }
    }

    /// Get the total number of terms with synonyms
    pub fn len(&self) -> usize {
        self.forward.len()
    }

    /// Check if the map is empty
    pub fn is_empty(&self) -> bool {
        self.forward.is_empty()
    }

    /// Load synonym map from a JSON file
    ///
    /// File format:
    /// ```json
    /// {
    ///   "entries": [
    ///     { "term": "数据库", "synonyms": ["DB", "database"], "bidirectional": true },
    ///     { "term": "AI", "synonyms": ["artificial intelligence"], "bidirectional": false }
    ///   ]
    /// }
    /// ```
    pub fn load_from_json(path: impl AsRef<Path>) -> FTSResult<Self> {
        let content = std::fs::read_to_string(path.as_ref()).map_err(|e| {
            FTSError::Io(std::io::Error::new(
                e.kind(),
                format!("Failed to read synonym file '{}': {}", path.as_ref().display(), e),
            ))
        })?;

        Self::from_json_str(&content)
    }

    /// Parse synonym map from a JSON string
    pub fn from_json_str(json: &str) -> FTSResult<Self> {
        let file: SynonymFile =
            serde_json::from_str(json).map_err(|e| FTSError::Internal(format!("Invalid synonym JSON: {}", e)))?;

        let mut map = Self::new();

        for entry in &file.entries {
            let synonyms: Vec<&str> = entry.synonyms.iter().map(|s| s.as_str()).collect();
            if entry.bidirectional {
                map.add_bidirectional(&entry.term, synonyms);
            } else {
                map.add_unidirectional(&entry.term, synonyms);
            }
        }

        Ok(map)
    }

    /// Serialize the synonym map to a JSON string
    pub fn to_json_string(&self) -> FTSResult<String> {
        // Reconstruct entries from forward map (deduplicate bidirectional groups)
        let mut visited: Vec<String> = Vec::new();
        let mut entries = Vec::new();

        for (term, synonyms) in &self.forward {
            if visited.contains(term) {
                continue;
            }

            // Check if this is bidirectional by testing if synonyms also map back
            let is_bidi = synonyms.iter().all(|s| {
                self.forward
                    .get(s)
                    .map(|back| back.contains(term))
                    .unwrap_or(false)
            });

            if is_bidi {
                // Mark all group members as visited
                visited.push(term.clone());
                for s in synonyms {
                    visited.push(s.clone());
                }
            }

            entries.push(SynonymEntry {
                term: term.clone(),
                synonyms: synonyms.clone(),
                bidirectional: is_bidi,
            });
        }

        let file = SynonymFile { entries };
        serde_json::to_string_pretty(&file).map_err(|e| FTSError::Internal(e.to_string()))
    }

    /// Clear all synonym mappings
    pub fn clear(&mut self) {
        self.forward.clear();
    }
}

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

    #[test]
    fn test_bidirectional_synonyms() {
        let mut map = SynonymMap::new();
        map.add_bidirectional("数据库", vec!["DB", "database"]);

        // Forward: 数据库 → [DB, database]
        let expanded = map.expand("数据库").unwrap();
        assert!(expanded.contains(&"数据库".to_string()));
        assert!(expanded.contains(&"db".to_string()));
        assert!(expanded.contains(&"database".to_string()));

        // Reverse: DB → [数据库, database]
        let expanded = map.expand("DB").unwrap();
        assert!(expanded.contains(&"db".to_string()));
        assert!(expanded.contains(&"数据库".to_string()));
        assert!(expanded.contains(&"database".to_string()));

        // Reverse: database → [数据库, DB]
        let expanded = map.expand("database").unwrap();
        assert!(expanded.contains(&"database".to_string()));
        assert!(expanded.contains(&"数据库".to_string()));
        assert!(expanded.contains(&"db".to_string()));
    }

    #[test]
    fn test_unidirectional_synonyms() {
        let mut map = SynonymMap::new();
        map.add_unidirectional("AI", vec!["artificial intelligence", "人工智能"]);

        // Forward: AI → [artificial intelligence, 人工智能]
        let expanded = map.expand("AI").unwrap();
        assert!(expanded.contains(&"ai".to_string()));
        assert!(expanded.contains(&"artificial intelligence".to_string()));
        assert!(expanded.contains(&"人工智能".to_string()));

        // Reverse: should NOT expand
        assert!(map.expand("artificial intelligence").is_none());
        assert!(map.expand("人工智能").is_none());
    }

    #[test]
    fn test_case_insensitive() {
        let mut map = SynonymMap::new();
        map.add_bidirectional("Rust", vec!["rust-lang"]);

        let expanded = map.expand("RUST").unwrap();
        assert!(expanded.contains(&"rust".to_string()));
        assert!(expanded.contains(&"rust-lang".to_string()));
    }

    #[test]
    fn test_no_synonyms() {
        let map = SynonymMap::new();
        assert!(map.expand("unknown").is_none());
    }

    #[test]
    fn test_expand_query() {
        let mut map = SynonymMap::new();
        map.add_bidirectional("数据库", vec!["DB"]);
        map.add_unidirectional("快速", vec!["高速", "迅速"]);

        let expanded = map.expand_query("快速 数据库").unwrap();
        assert_eq!(expanded.len(), 2);
        // "快速" should expand
        assert!(expanded[0].contains(&"快速".to_string()));
        assert!(expanded[0].contains(&"高速".to_string()));
        assert!(expanded[0].contains(&"迅速".to_string()));
        // "数据库" should expand
        assert!(expanded[1].contains(&"数据库".to_string()));
        assert!(expanded[1].contains(&"db".to_string()));
    }

    #[test]
    fn test_expand_query_no_matches() {
        let map = SynonymMap::new();
        assert!(map.expand_query("hello world").is_none());
    }

    #[test]
    fn test_load_from_json_str() {
        let json = r#"{
            "entries": [
                { "term": "数据库", "synonyms": ["DB", "database"], "bidirectional": true },
                { "term": "AI", "synonyms": ["artificial intelligence"], "bidirectional": false }
            ]
        }"#;

        let map = SynonymMap::from_json_str(json).unwrap();
        assert_eq!(map.len(), 4); // 数据库, DB, database (bidi) + AI (uni) = 4

        // Bidirectional
        assert!(map.expand("数据库").is_some());
        assert!(map.expand("DB").is_some());
        assert!(map.expand("database").is_some());

        // Unidirectional
        assert!(map.expand("AI").is_some());
        assert!(map.expand("artificial intelligence").is_none());
    }

    #[test]
    fn test_serialize_roundtrip() {
        let mut map = SynonymMap::new();
        map.add_bidirectional("hello", vec!["hi", "hey"]);

        let json = map.to_json_string().unwrap();
        let map2 = SynonymMap::from_json_str(&json).unwrap();

        let expanded = map2.expand("hello").unwrap();
        assert!(expanded.contains(&"hi".to_string()));
        assert!(expanded.contains(&"hey".to_string()));
    }

    #[test]
    fn test_empty_map() {
        let map = SynonymMap::new();
        assert!(map.is_empty());
        assert_eq!(map.len(), 0);
    }

    #[test]
    fn test_clear() {
        let mut map = SynonymMap::new();
        map.add_bidirectional("a", vec!["b"]);
        assert!(!map.is_empty());

        map.clear();
        assert!(map.is_empty());
    }

    #[test]
    fn test_duplicate_synonyms_deduplication() {
        let mut map = SynonymMap::new();
        map.add_bidirectional("a", vec!["b", "c"]);
        map.add_bidirectional("a", vec!["b", "d"]); // b is duplicate

        let expanded = map.expand("a").unwrap();
        // Should contain b, c, d without duplicates
        let count_b = expanded.iter().filter(|x| *x == "b").count();
        assert_eq!(count_b, 1);
        assert!(expanded.contains(&"c".to_string()));
        assert!(expanded.contains(&"d".to_string()));
    }
}
