//! Fuzzy string matching for generating "Did you mean?" suggestions.
//!
//! Uses Jaro-Winkler similarity for name matching.

use super::types::{Suggestion, SuggestionKind};
use strsim::jaro_winkler;

/// Default similarity threshold for suggestions (0.0 - 1.0).
const DEFAULT_THRESHOLD: f64 = 0.7;

/// Maximum number of suggestions to return.
const MAX_SUGGESTIONS: usize = 3;

/// Find the closest matching names from a list of candidates.
///
/// Returns suggestions sorted by confidence (highest first),
/// filtered by threshold, and capped at MAX_SUGGESTIONS.
pub fn fuzzy_match(input: &str, candidates: &[&str], threshold: f64) -> Vec<Suggestion> {
    let input_lower = input.to_lowercase();

    let mut matches: Vec<(String, f64)> = candidates
        .iter()
        .map(|candidate| {
            let score = jaro_winkler(&input_lower, &candidate.to_lowercase());
            (candidate.to_string(), score)
        })
        .filter(|(_, score)| *score >= threshold)
        .collect();

    // Sort by confidence descending
    matches.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
    matches.truncate(MAX_SUGGESTIONS);

    matches
        .into_iter()
        .map(|(name, confidence)| Suggestion {
            kind: SuggestionKind::DidYouMean { name: name.clone() },
            message: format!("Did you mean \"{name}\"?"),
            confidence,
        })
        .collect()
}

/// Convenience: fuzzy match with default threshold.
pub fn fuzzy_match_default(input: &str, candidates: &[&str]) -> Vec<Suggestion> {
    fuzzy_match(input, candidates, DEFAULT_THRESHOLD)
}

/// Find matching table names in a schema.
pub fn match_table_name(input: &str, table_names: &[&str]) -> Vec<Suggestion> {
    fuzzy_match_default(input, table_names)
}

/// Find matching column names across all tables.
/// Returns suggestions with table qualification hints.
pub fn match_column_name(
    input: &str,
    columns: &[(&str, &str)], // (table_name, column_name)
) -> Vec<Suggestion> {
    let input_lower = input.to_lowercase();

    let mut matches: Vec<(String, String, f64)> = columns
        .iter()
        .map(|(table, col)| {
            let score = jaro_winkler(&input_lower, &col.to_lowercase());
            (table.to_string(), col.to_string(), score)
        })
        .filter(|(_, _, score)| *score >= DEFAULT_THRESHOLD)
        .collect();

    matches.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal));
    matches.truncate(MAX_SUGGESTIONS);

    matches
        .into_iter()
        .map(|(table, col, confidence)| Suggestion {
            kind: SuggestionKind::DidYouMean {
                name: format!("{table}.{col}"),
            },
            message: format!("Did you mean \"{table}.{col}\"?"),
            confidence,
        })
        .collect()
}

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

    #[test]
    fn test_exact_match() {
        let suggestions = fuzzy_match("users", &["users", "orders", "products"], 0.7);
        assert_eq!(suggestions.len(), 1);
        assert!(suggestions[0].confidence > 0.99);
    }

    #[test]
    fn test_typo_match() {
        let suggestions = fuzzy_match("usrs", &["users", "orders", "products"], 0.7);
        assert!(!suggestions.is_empty());
        assert_eq!(
            match &suggestions[0].kind {
                SuggestionKind::DidYouMean { name } => name.as_str(),
                _ => "",
            },
            "users"
        );
    }

    #[test]
    fn test_singular_plural() {
        let suggestions = fuzzy_match("order", &["users", "orders", "products"], 0.7);
        assert!(!suggestions.is_empty());
        assert_eq!(
            match &suggestions[0].kind {
                SuggestionKind::DidYouMean { name } => name.as_str(),
                _ => "",
            },
            "orders"
        );
    }

    #[test]
    fn test_no_match() {
        let suggestions = fuzzy_match("zzzzzzz", &["users", "orders"], 0.7);
        assert!(suggestions.is_empty());
    }

    #[test]
    fn test_column_match() {
        let columns = vec![
            ("users", "created_at"),
            ("users", "updated_at"),
            ("orders", "created_at"),
        ];
        let suggestions = match_column_name("created_date", &columns);
        assert!(!suggestions.is_empty());
    }

    #[test]
    fn test_max_suggestions() {
        let candidates: Vec<&str> = (0..20).map(|_| "test").collect();
        let suggestions = fuzzy_match("test", &candidates, 0.7);
        assert!(suggestions.len() <= MAX_SUGGESTIONS);
    }
}
