use super::types::{NormalizedEntity, RuleResult};
use super::normalization::*;
use strsim::{jaro_winkler, normalized_levenshtein};
use rphonetic::{Soundex, DoubleMetaphone, Encoder};
use std::collections::HashMap;

pub trait MatchRule: Send + Sync {
    fn name(&self) -> &str;
    fn evaluate(&self, a: &NormalizedEntity, b: &NormalizedEntity) -> RuleResult;
}

fn apply_normalizer(name: &str, value: &str) -> String {
    let result = match name {
        "phone" => PhoneNormalizer.normalize(value),
        "email" => EmailNormalizer.normalize(value),
        "name" => NameNormalizer.normalize(value),
        "domain" => DomainNormalizer.normalize(value),
        _ => GenericNormalizer.normalize(value),
    };
    match result {
        NormalizationResult::Normalized(s) => s,
        NormalizationResult::Invalid(_) => value.to_string(),
    }
}

pub struct ExactMatch {
    pub field: String,
    pub weight: f64,
    pub case_insensitive: bool,
    pub normalize: bool,
    pub normalizer: Option<String>,
    pub rule_name: String,
}

impl MatchRule for ExactMatch {
    fn name(&self) -> &str {
        &self.rule_name
    }

    fn evaluate(&self, a: &NormalizedEntity, b: &NormalizedEntity) -> RuleResult {
        let val_a = a.data.get(&self.field).map(|v| v.as_str());
        let val_b = b.data.get(&self.field).map(|v| v.as_str());

        let matched = match (val_a, val_b) {
            (Some(a), Some(b)) => {
                let mut s_a = a.to_string();
                let mut s_b = b.to_string();

                if let Some(ref n) = self.normalizer {
                    s_a = apply_normalizer(n, &s_a);
                    s_b = apply_normalizer(n, &s_b);
                }

                if self.normalize {
                    s_a = s_a.trim().to_string();
                    s_b = s_b.trim().to_string();
                }

                if self.case_insensitive {
                    s_a = s_a.to_lowercase();
                    s_b = s_b.to_lowercase();
                }

                s_a == s_b
            },
            _ => false,
        };

        RuleResult {
            rule_name: self.rule_name.clone(),
            score: if matched { 1.0 } else { 0.0 },
            weight: self.weight,
            matched,
            explanation: if matched {
                format!("Exact match on field {}", self.field)
            } else {
                format!("No match on field {}", self.field)
            },
        }
    }
}

pub struct RangeMatch {
    pub field: String,
    pub tolerance: f64,
    pub weight: f64,
    pub normalizer: Option<String>,
    pub rule_name: String,
}

impl MatchRule for RangeMatch {
    fn name(&self) -> &str {
        &self.rule_name
    }

    fn evaluate(&self, a: &NormalizedEntity, b: &NormalizedEntity) -> RuleResult {
        fn parse_f64(s: &str) -> Option<f64> {
            s.parse::<f64>().ok()
        }

        let val_a = a.data.get(&self.field).and_then(|s| parse_f64(s));
        let val_b = b.data.get(&self.field).and_then(|s| parse_f64(s));

        let (matched, score) = match (val_a, val_b) {
            (Some(a), Some(b)) => {
                let diff = (a - b).abs();
                if diff <= self.tolerance {
                    (true, 1.0)
                } else {
                    (false, 0.0)
                }
            },
            _ => (false, 0.0),
        };

        RuleResult {
            rule_name: self.rule_name.clone(),
            score,
            weight: self.weight,
            matched,
            explanation: if matched {
                format!("Range match on field {} (diff within {})", self.field, self.tolerance)
            } else {
                format!("Range mismatch on field {}", self.field)
            },
        }
    }
}

pub struct MlMatch {
    pub model_id: String,
    pub features: Vec<String>,
    pub threshold: f64,
    pub weight: f64,
    pub rule_name: String,
}

impl MatchRule for MlMatch {
    fn name(&self) -> &str {
        &self.rule_name
    }

    fn evaluate(&self, _a: &NormalizedEntity, _b: &NormalizedEntity) -> RuleResult {
         RuleResult {
            rule_name: self.rule_name.clone(),
            score: 0.0,
            weight: self.weight,
            matched: false,
            explanation: "ML matching not fully implemented currently".to_string(),
        }
    }
}

pub struct FuzzyStringMatch {
    pub field: String,
    pub threshold: f64,
    pub weight: f64,
    pub normalizer: Option<String>,
    pub rule_name: String,
}

impl MatchRule for FuzzyStringMatch {
    fn name(&self) -> &str {
        &self.rule_name
    }

    fn evaluate(&self, a: &NormalizedEntity, b: &NormalizedEntity) -> RuleResult {
        let val_a = a.data.get(&self.field);
        let val_b = b.data.get(&self.field);

        let score = match (val_a, val_b) {
            (Some(a), Some(b)) => {
                let (a_str, b_str) = if let Some(ref n) = self.normalizer {
                    (apply_normalizer(n, a), apply_normalizer(n, b))
                } else {
                    (a.to_string(), b.to_string())
                };
                jaro_winkler(&a_str, &b_str)
            }
            _ => 0.0,
        };

        let matched = score >= self.threshold;

        RuleResult {
            rule_name: self.rule_name.clone(),
            score,
            weight: self.weight,
            matched,
            explanation: if matched {
                format!("Fuzzy match on field {} (score: {:.2})", self.field, score)
            } else {
                format!("Insufficient fuzzy score on field {} (score: {:.2})", self.field, score)
            },
        }
    }
}

pub struct LevenshteinMatch {
    pub field: String,
    pub threshold: f64,
    pub weight: f64,
    pub normalizer: Option<String>,
    pub rule_name: String,
}

impl MatchRule for LevenshteinMatch {
    fn name(&self) -> &str {
        &self.rule_name
    }

    fn evaluate(&self, a: &NormalizedEntity, b: &NormalizedEntity) -> RuleResult {
        let val_a = a.data.get(&self.field);
        let val_b = b.data.get(&self.field);

        let score = match (val_a, val_b) {
            (Some(a), Some(b)) => {
                let (a_str, b_str) = if let Some(ref n) = self.normalizer {
                    (apply_normalizer(n, a), apply_normalizer(n, b))
                } else {
                    (a.to_string(), b.to_string())
                };
                normalized_levenshtein(&a_str, &b_str)
            }
            _ => 0.0,
        };

        let matched = score >= self.threshold;

        RuleResult {
            rule_name: self.rule_name.clone(),
            score,
            weight: self.weight,
            matched,
            explanation: if matched {
                format!("Levenshtein match on field {} (score: {:.2})", self.field, score)
            } else {
                format!("Insufficient Levenshtein score on field {} (score: {:.2})", self.field, score)
            },
        }
    }
}

pub struct DomainMatch {
    pub fields: Vec<String>,
    pub weight: f64,
    pub rule_name: String,
}

impl MatchRule for DomainMatch {
    fn name(&self) -> &str {
        &self.rule_name
    }

    fn evaluate(&self, a: &NormalizedEntity, b: &NormalizedEntity) -> RuleResult {
        fn extract_domain(s: &str) -> Option<String> {
            if let Some(at_pos) = s.rfind('@') {
                Some(s[at_pos + 1..].to_lowercase())
            } else if s.contains('.') {
                let s = s.strip_prefix("https://").or_else(|| s.strip_prefix("http://")).unwrap_or(s);
                let s = s.strip_prefix("www.").unwrap_or(s);
                Some(s.split('/').next().unwrap_or(s).to_lowercase())
            } else {
                None
            }
        }

        // Collect all domains from both entities across all configured fields
        let domains_a: Vec<String> = self.fields.iter()
            .filter_map(|f| a.data.get(f))
            .filter_map(|v| extract_domain(v))
            .collect();
        let domains_b: Vec<String> = self.fields.iter()
            .filter_map(|f| b.data.get(f))
            .filter_map(|v| extract_domain(v))
            .collect();

        // Check if any domain from A matches any domain from B (cross-field)
        for da in &domains_a {
            for db in &domains_b {
                if da == db {
                    return RuleResult {
                        rule_name: self.rule_name.clone(),
                        score: 1.0,
                        weight: self.weight,
                        matched: true,
                        explanation: format!("Domain match ({})", da),
                    };
                }
            }
        }

        RuleResult {
            rule_name: self.rule_name.clone(),
            score: 0.0,
            weight: self.weight,
            matched: false,
            explanation: "No domain match found".to_string(),
        }
    }
}

pub struct CompositeMatch {
    pub rules: Vec<Box<dyn MatchRule>>,
    pub weight: f64,
    pub require_all: bool,
    pub rule_name: String,
}

impl MatchRule for CompositeMatch {
    fn name(&self) -> &str {
        &self.rule_name
    }

    fn evaluate(&self, a: &NormalizedEntity, b: &NormalizedEntity) -> RuleResult {
        let results: Vec<RuleResult> = self.rules.iter().map(|r| r.evaluate(a, b)).collect();

        let matched = if self.require_all {
            results.iter().all(|r| r.matched)
        } else {
            results.iter().any(|r| r.matched)
        };

        let score = if results.is_empty() {
            0.0
        } else {
            results.iter().map(|r| r.score).sum::<f64>() / results.len() as f64
        };

        RuleResult {
            rule_name: self.rule_name.clone(),
            score,
            weight: self.weight,
            matched,
            explanation: format!(
                "Composite match ({} component rules, matched: {})",
                results.len(),
                matched
            ),
        }
    }
}

// --- Soundex phonetic matching ---

pub struct SoundexMatch {
    pub field: String,
    pub threshold: f64,
    pub weight: f64,
    pub rule_name: String,
}

impl MatchRule for SoundexMatch {
    fn name(&self) -> &str {
        &self.rule_name
    }

    fn evaluate(&self, a: &NormalizedEntity, b: &NormalizedEntity) -> RuleResult {
        let val_a = a.data.get(&self.field);
        let val_b = b.data.get(&self.field);

        let score = match (val_a, val_b) {
            (Some(a_str), Some(b_str)) => {
                let encoder = Soundex::default();
                let code_a = encoder.encode(a_str);
                let code_b = encoder.encode(b_str);
                if code_a == code_b { 1.0 } else { 0.0 }
            }
            _ => 0.0,
        };

        let matched = score >= self.threshold;

        RuleResult {
            rule_name: self.rule_name.clone(),
            score,
            weight: self.weight,
            matched,
            explanation: if matched {
                format!("Soundex match on field {} (codes match)", self.field)
            } else {
                format!("Soundex mismatch on field {} (codes differ)", self.field)
            },
        }
    }
}

// --- Double Metaphone phonetic matching ---

pub struct MetaphoneMatch {
    pub field: String,
    pub threshold: f64,
    pub weight: f64,
    pub rule_name: String,
}

impl MatchRule for MetaphoneMatch {
    fn name(&self) -> &str {
        &self.rule_name
    }

    fn evaluate(&self, a: &NormalizedEntity, b: &NormalizedEntity) -> RuleResult {
        let val_a = a.data.get(&self.field);
        let val_b = b.data.get(&self.field);

        let score = match (val_a, val_b) {
            (Some(a_str), Some(b_str)) => {
                let encoder = DoubleMetaphone::default();
                let primary_a = encoder.encode(a_str);
                let primary_b = encoder.encode(b_str);

                if primary_a == primary_b {
                    // Primary codes match: full score
                    1.0
                } else {
                    // Check alternate codes for partial match
                    let alt_a = encoder.encode_alternate(a_str);
                    let alt_b = encoder.encode_alternate(b_str);

                    if primary_a == alt_b || alt_a == primary_b {
                        0.5
                    } else {
                        0.0
                    }
                }
            }
            _ => 0.0,
        };

        let matched = score >= self.threshold;

        RuleResult {
            rule_name: self.rule_name.clone(),
            score,
            weight: self.weight,
            matched,
            explanation: if matched {
                format!("Metaphone match on field {} (score: {:.2})", self.field, score)
            } else {
                format!("Metaphone mismatch on field {} (score: {:.2})", self.field, score)
            },
        }
    }
}

// --- Cosine similarity (character bigram) ---

pub struct CosineMatch {
    pub field: String,
    pub threshold: f64,
    pub weight: f64,
    pub rule_name: String,
}

impl CosineMatch {
    /// Build a bag-of-bigrams frequency map from a string.
    fn bigram_vector(s: &str) -> HashMap<(char, char), f64> {
        let chars: Vec<char> = s.chars().collect();
        let mut freq: HashMap<(char, char), f64> = HashMap::new();
        if chars.len() >= 2 {
            for window in chars.windows(2) {
                *freq.entry((window[0], window[1])).or_insert(0.0) += 1.0;
            }
        }
        freq
    }

    /// Compute cosine similarity between two bigram frequency maps.
    fn cosine_similarity(a: &HashMap<(char, char), f64>, b: &HashMap<(char, char), f64>) -> f64 {
        if a.is_empty() || b.is_empty() {
            return 0.0;
        }

        let mut dot = 0.0;
        let mut norm_a = 0.0;
        let mut norm_b = 0.0;

        for (key, &va) in a {
            norm_a += va * va;
            if let Some(&vb) = b.get(key) {
                dot += va * vb;
            }
        }

        for &vb in b.values() {
            norm_b += vb * vb;
        }

        if norm_a == 0.0 || norm_b == 0.0 {
            return 0.0;
        }

        dot / (norm_a.sqrt() * norm_b.sqrt())
    }
}

impl MatchRule for CosineMatch {
    fn name(&self) -> &str {
        &self.rule_name
    }

    fn evaluate(&self, a: &NormalizedEntity, b: &NormalizedEntity) -> RuleResult {
        let val_a = a.data.get(&self.field);
        let val_b = b.data.get(&self.field);

        let score = match (val_a, val_b) {
            (Some(a_str), Some(b_str)) => {
                let vec_a = Self::bigram_vector(a_str);
                let vec_b = Self::bigram_vector(b_str);
                Self::cosine_similarity(&vec_a, &vec_b)
            }
            _ => 0.0,
        };

        let matched = score >= self.threshold;

        RuleResult {
            rule_name: self.rule_name.clone(),
            score,
            weight: self.weight,
            matched,
            explanation: if matched {
                format!("Cosine match on field {} (score: {:.2})", self.field, score)
            } else {
                format!("Insufficient cosine score on field {} (score: {:.2})", self.field, score)
            },
        }
    }
}

// --- Haversine geographic distance matching ---

pub struct HaversineMatch {
    pub field: String,
    pub threshold: f64, // tolerance in km
    pub weight: f64,
    pub rule_name: String,
}

impl HaversineMatch {
    const EARTH_RADIUS_KM: f64 = 6371.0;

    /// Parse a "lat,lon" string into (latitude, longitude) in degrees.
    fn parse_latlon(s: &str) -> Option<(f64, f64)> {
        let parts: Vec<&str> = s.split(',').collect();
        if parts.len() == 2 {
            let lat = parts[0].trim().parse::<f64>().ok()?;
            let lon = parts[1].trim().parse::<f64>().ok()?;
            if (-90.0..=90.0).contains(&lat) && (-180.0..=180.0).contains(&lon) {
                Some((lat, lon))
            } else {
                None
            }
        } else {
            None
        }
    }

    /// Compute the haversine distance (in km) between two points.
    fn haversine_distance(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
        let lat1_rad = lat1.to_radians();
        let lat2_rad = lat2.to_radians();
        let dlat = (lat2 - lat1).to_radians();
        let dlon = (lon2 - lon1).to_radians();

        let a = (dlat / 2.0).sin().powi(2)
            + lat1_rad.cos() * lat2_rad.cos() * (dlon / 2.0).sin().powi(2);
        let c = 2.0 * a.sqrt().asin();

        Self::EARTH_RADIUS_KM * c
    }
}

impl MatchRule for HaversineMatch {
    fn name(&self) -> &str {
        &self.rule_name
    }

    fn evaluate(&self, a: &NormalizedEntity, b: &NormalizedEntity) -> RuleResult {
        let val_a = a.data.get(&self.field);
        let val_b = b.data.get(&self.field);

        let (score, distance_info) = match (val_a, val_b) {
            (Some(a_str), Some(b_str)) => {
                match (Self::parse_latlon(a_str), Self::parse_latlon(b_str)) {
                    (Some((lat1, lon1)), Some((lat2, lon2))) => {
                        let dist = Self::haversine_distance(lat1, lon1, lat2, lon2);
                        let tolerance = self.threshold;
                        let s = if dist <= tolerance {
                            1.0
                        } else if dist <= 2.0 * tolerance {
                            // Linear scale from 1.0 at tolerance to 0.0 at 2x tolerance
                            1.0 - (dist - tolerance) / tolerance
                        } else {
                            0.0
                        };
                        (s, Some(dist))
                    }
                    _ => (0.0, None),
                }
            }
            _ => (0.0, None),
        };

        // For haversine, matched means distance <= tolerance (score == 1.0)
        let matched = match distance_info {
            Some(dist) => dist <= self.threshold,
            None => false,
        };

        RuleResult {
            rule_name: self.rule_name.clone(),
            score,
            weight: self.weight,
            matched,
            explanation: match distance_info {
                Some(dist) => {
                    if matched {
                        format!("Haversine match on field {} (distance: {:.2} km, within {:.2} km tolerance)", self.field, dist, self.threshold)
                    } else {
                        format!("Haversine mismatch on field {} (distance: {:.2} km, exceeds {:.2} km tolerance)", self.field, dist, self.threshold)
                    }
                }
                None => format!("Haversine: could not parse coordinates from field {}", self.field),
            },
        }
    }
}
