//! Hybrid blocking: combine multiple strategies with union or intersection.

use ahash::AHashSet;

use crate::blocking::BlockingStrategy;
use crate::record::{CandidatePair, RecordBatch};

/// How to combine candidate pairs from multiple blocking strategies.
#[derive(Debug, Clone, Copy)]
pub enum HybridMode {
    /// Keep pairs generated by ANY strategy (increases recall).
    Union,
    /// Keep only pairs generated by ALL strategies (increases precision).
    Intersection,
}

/// Combines multiple blocking strategies into one.
pub struct HybridBlocking {
    strategies: Vec<Box<dyn BlockingStrategy>>,
    mode: HybridMode,
}

impl HybridBlocking {
    /// Creates a new hybrid blocker.
    #[must_use]
    pub fn new(strategies: Vec<Box<dyn BlockingStrategy>>, mode: HybridMode) -> Self {
        Self { strategies, mode }
    }
}

impl BlockingStrategy for HybridBlocking {
    fn block_dedup(&self, records: &RecordBatch) -> Vec<CandidatePair> {
        let all: Vec<Vec<CandidatePair>> = self
            .strategies
            .iter()
            .map(|s| s.block_dedup(records))
            .collect();
        combine(all, self.mode, self.strategies.len())
    }

    fn block_link(&self, left: &RecordBatch, right: &RecordBatch) -> Vec<CandidatePair> {
        let all: Vec<Vec<CandidatePair>> = self
            .strategies
            .iter()
            .map(|s| s.block_link(left, right))
            .collect();
        combine(all, self.mode, self.strategies.len())
    }
}

fn combine(
    all: Vec<Vec<CandidatePair>>,
    mode: HybridMode,
    num_strategies: usize,
) -> Vec<CandidatePair> {
    match mode {
        HybridMode::Union => {
            let mut seen = AHashSet::new();
            let mut result = Vec::new();
            for pairs in all {
                for p in pairs {
                    let key = (p.left.min(p.right), p.left.max(p.right));
                    if seen.insert(key) {
                        result.push(p);
                    }
                }
            }
            result
        }
        HybridMode::Intersection => {
            let mut counts: ahash::AHashMap<(usize, usize), usize> = ahash::AHashMap::new();
            let mut first_seen: ahash::AHashMap<(usize, usize), CandidatePair> =
                ahash::AHashMap::new();
            for pairs in all {
                // Deduplicate within each strategy first
                let mut strategy_seen = AHashSet::new();
                for p in pairs {
                    let key = (p.left.min(p.right), p.left.max(p.right));
                    if strategy_seen.insert(key) {
                        *counts.entry(key).or_insert(0) += 1;
                        first_seen.entry(key).or_insert(p);
                    }
                }
            }
            counts
                .into_iter()
                .filter(|&(_, count)| count >= num_strategies)
                .map(|(key, _)| first_seen[&key])
                .collect()
        }
    }
}

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

    fn make_batch(field: &str, values: &[&str]) -> RecordBatch {
        use crate::record::{FieldValue, Record};
        let records: Vec<Record> = values
            .iter()
            .map(|v| {
                let mut fields = ahash::AHashMap::new();
                fields.insert(field.to_string(), FieldValue::Text(v.to_string()));
                Record {
                    id: v.to_string(),
                    fields,
                }
            })
            .collect();
        RecordBatch::new(vec![field.to_string()], records)
    }

    #[test]
    fn union_deduplicates() {
        let batch = make_batch("name", &["alice", "alice", "bob"]);
        let s1 = ExactBlocking::new("name".to_string());
        let s2 = ExactBlocking::new("name".to_string());
        let hybrid = HybridBlocking::new(vec![Box::new(s1), Box::new(s2)], HybridMode::Union);
        let pairs = hybrid.block_dedup(&batch);
        // Should deduplicate: only one pair (0,1)
        assert_eq!(pairs.len(), 1);
    }

    #[test]
    fn intersection_requires_all() {
        let batch = make_batch("name", &["alice", "alice", "bob"]);
        let s1 = ExactBlocking::new("name".to_string());
        let s2 = ExactBlocking::new("name".to_string());
        let hybrid =
            HybridBlocking::new(vec![Box::new(s1), Box::new(s2)], HybridMode::Intersection);
        let pairs = hybrid.block_dedup(&batch);
        // Both strategies produce (0,1), so intersection keeps it
        assert_eq!(pairs.len(), 1);
    }
}
