use std::collections::HashMap;

use tracing::{debug, instrument};

use crate::types::SearchResult;

/// Deduplicates search results by file path (and optionally line number),
/// keeping the entry with the highest relevance score for each unique key.
pub struct Deduplicator;

impl Deduplicator {
    pub fn new() -> Self {
        Self
    }

    /// Remove duplicate search results. Two results are considered duplicates
    /// if they share the same `file_path` and `line_number`. When duplicates
    /// are found the result with the highest `relevance_score` is kept.
    #[instrument(skip(self, results), fields(input_count = results.len()))]
    pub fn deduplicate(&self, results: Vec<SearchResult>) -> Vec<SearchResult> {
        if results.is_empty() {
            return results;
        }

        // Build a map keyed by (file_path, line_number). For each key we keep
        // only the result with the best relevance score.
        let mut best: HashMap<(String, Option<usize>), SearchResult> = HashMap::new();

        for result in results {
            let key = (result.file_path.clone(), result.line_number);

            let dominated = best
                .get(&key)
                .map(|existing| existing.relevance_score < result.relevance_score)
                .unwrap_or(true);

            if dominated {
                best.insert(key, result);
            }
        }

        let mut deduplicated: Vec<SearchResult> = best.into_values().collect();

        // Sort descending by relevance so callers get a deterministic order.
        deduplicated.sort_by(|a, b| {
            b.relevance_score
                .partial_cmp(&a.relevance_score)
                .unwrap_or(std::cmp::Ordering::Equal)
        });

        debug!(
            output_count = deduplicated.len(),
            "deduplication complete"
        );

        deduplicated
    }

    /// A stricter variant that deduplicates purely by file path, ignoring
    /// line numbers. Useful when you want at most one result per file.
    #[instrument(skip(self, results), fields(input_count = results.len()))]
    pub fn deduplicate_by_file(&self, results: Vec<SearchResult>) -> Vec<SearchResult> {
        if results.is_empty() {
            return results;
        }

        let mut best: HashMap<String, SearchResult> = HashMap::new();

        for result in results {
            let key = result.file_path.clone();

            let dominated = best
                .get(&key)
                .map(|existing| existing.relevance_score < result.relevance_score)
                .unwrap_or(true);

            if dominated {
                best.insert(key, result);
            }
        }

        let mut deduplicated: Vec<SearchResult> = best.into_values().collect();

        deduplicated.sort_by(|a, b| {
            b.relevance_score
                .partial_cmp(&a.relevance_score)
                .unwrap_or(std::cmp::Ordering::Equal)
        });

        debug!(
            output_count = deduplicated.len(),
            "file-level deduplication complete"
        );

        deduplicated
    }
}

impl Default for Deduplicator {
    fn default() -> Self {
        Self::new()
    }
}

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

    fn make_result(path: &str, line: Option<usize>, score: f64) -> SearchResult {
        SearchResult {
            file_path: path.to_string(),
            content: format!("content from {path}"),
            relevance_score: score,
            line_number: line,
        }
    }

    #[test]
    fn test_deduplicate_keeps_highest_score() {
        let dedup = Deduplicator::new();
        let results = vec![
            make_result("a.rs", Some(10), 0.5),
            make_result("a.rs", Some(10), 0.9),
            make_result("b.rs", Some(1), 0.7),
        ];

        let out = dedup.deduplicate(results);
        assert_eq!(out.len(), 2);
        assert_eq!(out[0].file_path, "a.rs");
        assert!((out[0].relevance_score - 0.9).abs() < f64::EPSILON);
    }

    #[test]
    fn test_deduplicate_different_lines_kept() {
        let dedup = Deduplicator::new();
        let results = vec![
            make_result("a.rs", Some(10), 0.5),
            make_result("a.rs", Some(20), 0.9),
        ];

        let out = dedup.deduplicate(results);
        assert_eq!(out.len(), 2);
    }

    #[test]
    fn test_deduplicate_by_file() {
        let dedup = Deduplicator::new();
        let results = vec![
            make_result("a.rs", Some(10), 0.5),
            make_result("a.rs", Some(20), 0.9),
            make_result("b.rs", Some(1), 0.7),
        ];

        let out = dedup.deduplicate_by_file(results);
        assert_eq!(out.len(), 2);
        // highest score first
        assert_eq!(out[0].file_path, "a.rs");
        assert!((out[0].relevance_score - 0.9).abs() < f64::EPSILON);
    }

    #[test]
    fn test_empty_input() {
        let dedup = Deduplicator::new();
        let out = dedup.deduplicate(vec![]);
        assert!(out.is_empty());
    }
}
