use serde::{Deserialize, Serialize};
use unicode_normalization::UnicodeNormalization;
use sha2::{Digest, Sha256};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum NormalizationResult {
    Normalized(String),
    Invalid(String),
}

pub trait Normalizer: Send + Sync {
    fn normalize(&self, value: &str) -> NormalizationResult;
}

pub struct EmailNormalizer;
impl Normalizer for EmailNormalizer {
    fn normalize(&self, value: &str) -> NormalizationResult {
        let trimmed = value.trim().to_lowercase();
        let parts: Vec<&str> = trimmed.splitn(2, '@').collect();
        if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() {
            return NormalizationResult::Invalid("Invalid email format".to_string());
        }

        let (local, domain) = (parts[0], parts[1]);

        let normalized_local = if domain == "gmail.com" || domain == "googlemail.com" {
            let without_plus = local.split('+').next().unwrap_or(local);
            without_plus.replace('.', "")
        } else {
            local.to_string()
        };

        NormalizationResult::Normalized(format!("{}@{}", normalized_local, domain))
    }
}

/// ITU-T E.164 country calling codes, sorted for binary search.
/// Covers all assigned codes as of 2025. E.164 codes are prefix-free:
/// no valid code is a prefix of another, so shortest-match-first is unambiguous.
const E164_COUNTRY_CODES: &[u16] = &[
    // 1-digit
    1, 7,
    // 2-digit
    20, 27, 30, 31, 32, 33, 34, 36, 39,
    40, 41, 43, 44, 45, 46, 47, 48, 49,
    51, 52, 53, 54, 55, 56, 57, 58,
    60, 61, 62, 63, 64, 65, 66,
    81, 82, 84, 86,
    90, 91, 92, 93, 94, 95, 98,
    // 3-digit: Africa
    211, 212, 213, 216, 218,
    220, 221, 222, 223, 224, 225, 226, 227, 228, 229,
    230, 231, 232, 233, 234, 235, 236, 237, 238, 239,
    240, 241, 242, 243, 244, 245, 246, 247, 248, 249,
    250, 251, 252, 253, 254, 255, 256, 257, 258,
    260, 261, 262, 263, 264, 265, 266, 267, 268, 269,
    290, 291, 297, 298, 299,
    // 3-digit: Europe
    350, 351, 352, 353, 354, 355, 356, 357, 358, 359,
    370, 371, 372, 373, 374, 375, 376, 377, 378,
    380, 381, 382, 383, 385, 386, 387, 389,
    420, 421, 423,
    // 3-digit: Americas
    500, 501, 502, 503, 504, 505, 506, 507, 508, 509,
    590, 591, 592, 593, 594, 595, 596, 597, 598, 599,
    // 3-digit: Asia-Pacific
    670, 672, 673, 674, 675, 676, 677, 678, 679,
    680, 681, 682, 683, 684, 685, 686, 687, 688, 689,
    690, 691, 692,
    // 3-digit: East Asia, satellite, global services
    800, 808,
    850, 852, 853, 855, 856, 870, 878,
    880, 881, 882, 883, 886, 888,
    // 3-digit: Middle East, Central/South Asia
    960, 961, 962, 963, 964, 965, 966, 967, 968,
    970, 971, 972, 973, 974, 975, 976, 977, 979,
    992, 993, 994, 995, 996, 998,
];

fn is_valid_country_code(code: u16) -> bool {
    E164_COUNTRY_CODES.binary_search(&code).is_ok()
}

/// Extract a valid E.164 country code from the start of a digit string.
/// Returns (code, digit_length). Tries 1-digit, then 2-digit, then 3-digit
/// - correct because E.164 codes are prefix-free.
fn extract_country_code(digits: &str) -> Option<(u16, usize)> {
    let max_len = 3.min(digits.len());
    for len in 1..=max_len {
        if let Ok(code) = digits[..len].parse::<u16>() {
            if is_valid_country_code(code) {
                return Some((code, len));
            }
        }
    }
    None
}

pub struct PhoneNormalizer;
impl Normalizer for PhoneNormalizer {
    fn normalize(&self, value: &str) -> NormalizationResult {
        let trimmed = value.trim();
        let has_plus = trimmed.starts_with('+');
        let digits: String = trimmed.chars().filter(|c| c.is_ascii_digit()).collect();

        if digits.is_empty() {
            return NormalizationResult::Invalid("No digits found".to_string());
        }

        // Step 1: Determine the international digit string (country code + subscriber).
        // Strip international dialing prefixes (IDD) and trunk prefixes when present.
        let international = if has_plus {
            // +{cc}{subscriber} - already international format
            digits.as_str()
        } else if digits.starts_with("011") && digits.len() >= 11 {
            // US/Canada IDD prefix: 011 + cc + subscriber
            &digits[3..]
        } else if digits.starts_with("00") && digits.len() >= 10 {
            // ITU standard IDD prefix (most countries): 00 + cc + subscriber
            &digits[2..]
        } else if digits.starts_with('0') && digits.len() >= 10 {
            // Leading 0 is a trunk prefix used in many countries (UK, France,
            // Germany, Japan, etc.). Strip it and normalize the remainder.
            // Without country context we can't know the true CC, but stripping
            // the trunk prefix produces a consistent key for same-source matching.
            &digits[1..]
        } else if digits.len() == 10 {
            // 10 digits, no leading zero -> assume NANP (+1)
            return NormalizationResult::Normalized(format!("+1{}", digits));
        } else if digits.len() >= 11 && digits.len() <= 15 {
            // 11-15 digits without prefix -> assume cc + subscriber
            digits.as_str()
        } else if digits.len() < 10 {
            return NormalizationResult::Invalid("Too few digits for E.164".to_string());
        } else {
            return NormalizationResult::Invalid("Too many digits for E.164".to_string());
        };

        // Step 2: Validate E.164 constraints.
        // Country codes never start with 0. Total max 15 digits.
        if international.starts_with('0') {
            return NormalizationResult::Invalid(
                "Invalid E.164: country codes never start with 0".to_string(),
            );
        }
        if international.len() > 15 {
            return NormalizationResult::Invalid("Too many digits for E.164".to_string());
        }
        if international.len() < 7 {
            return NormalizationResult::Invalid("Too few digits for E.164".to_string());
        }

        // Step 3: Validate that the leading digits are a real country code.
        if extract_country_code(international).is_none() {
            return NormalizationResult::Invalid("Unrecognized country code".to_string());
        }

        NormalizationResult::Normalized(format!("+{}", international))
    }
}

pub struct NameNormalizer;
impl Normalizer for NameNormalizer {
    fn normalize(&self, value: &str) -> NormalizationResult {
        let nfc: String = value.nfc().collect();
        let lowered = nfc.to_lowercase();
        let normalized = lowered
            .split_whitespace()
            .collect::<Vec<_>>()
            .join(" ");
        NormalizationResult::Normalized(normalized)
    }
}

pub struct DomainNormalizer;
impl Normalizer for DomainNormalizer {
    fn normalize(&self, value: &str) -> NormalizationResult {
        let mut normalized = value.trim().to_lowercase();
        normalized = normalized
            .strip_prefix("https://")
            .or_else(|| normalized.strip_prefix("http://"))
            .unwrap_or(&normalized)
            .to_string();
        normalized = normalized.strip_prefix("www.").unwrap_or(&normalized).to_string();
        if let Some(pos) = normalized.find('/') {
            normalized = normalized[..pos].to_string();
        }
        NormalizationResult::Normalized(normalized)
    }
}

/// Nickname normalizer: applies NameNormalizer, then maps common nicknames
/// to their canonical form (e.g., "bob" → "robert", "liz" → "elizabeth").
/// This closes the gap with Splink's nickname resolution preprocessing.
pub struct NicknameNormalizer;
impl NicknameNormalizer {
    fn canonical(name: &str) -> &str {
        match name {
            "bob" | "rob" | "robbie" | "robby" | "bobby" => "robert",
            "bill" | "will" | "willy" | "willie" | "billy" | "liam" => "william",
            "dick" | "rick" | "rich" | "richie" | "ricky" => "richard",
            "jim" | "jimmy" | "jamie" => "james",
            "mike" | "mikey" | "mick" => "michael",
            "jen" | "jenny" => "jennifer",
            "liz" | "beth" | "betty" | "betsy" | "eliza" | "lizzy" => "elizabeth",
            "pat" | "patty" | "tricia" | "trish" => "patricia",
            "chris" => "christopher",
            "kate" | "kathy" | "katie" | "kat" | "kath" => "katherine",
            "ben" | "benny" => "benjamin",
            "nick" | "nicky" => "nicholas",
            "tom" | "tommy" => "thomas",
            "dan" | "danny" => "daniel",
            "dave" | "davy" => "david",
            "steve" | "stephen" => "steven",
            "joe" | "joey" => "joseph",
            "tony" => "anthony",
            "ed" | "eddie" | "ted" | "teddy" | "ned" => "edward",
            "sam" | "sammy" => "samuel",
            "matt" | "matty" => "matthew",
            "andy" | "drew" => "andrew",
            "alex" | "xander" => "alexander",
            "charlie" | "chuck" | "chaz" => "charles",
            "harry" | "hank" | "hal" => "henry",
            "jack" | "johnny" => "john",
            "larry" => "lawrence",
            "jerry" => "gerald",
            "terry" => "terrence",
            "ray" => "raymond",
            "al" => "alan",
            "meg" | "maggie" | "peggy" | "marge" => "margaret",
            "sue" | "susie" | "suzy" => "susan",
            "deb" | "debbie" | "debby" => "deborah",
            "barb" | "babs" => "barbara",
            "cathy" | "cath" => "catherine",
            "vicky" | "vicki" => "victoria",
            "mandy" => "amanda",
            "becky" => "rebecca",
            "cindy" => "cynthia",
            "sandy" => "sandra",
            "wendy" => "gwendolyn",
            "nate" => "nathan",
            "walt" => "walter",
            "fred" | "freddy" => "frederick",
            "frank" | "frankie" => "francis",
            "phil" => "philip",
            "ron" | "ronnie" => "ronald",
            "don" | "donnie" => "donald",
            "doug" => "douglas",
            "greg" => "gregory",
            "jeff" => "jeffrey",
            "ken" | "kenny" => "kenneth",
            "tim" | "timmy" => "timothy",
            "abe" => "abraham",
            "gus" => "augustus",
            "pete" => "peter",
            "gene" => "eugene",
            "ernie" => "ernest",
            "archie" => "archibald",
            "lenny" | "len" => "leonard",
            _ => name,
        }
    }
}
impl Normalizer for NicknameNormalizer {
    fn normalize(&self, value: &str) -> NormalizationResult {
        // First apply standard name normalization (NFC, lowercase, whitespace)
        let name_result = NameNormalizer.normalize(value);
        match name_result {
            NormalizationResult::Normalized(normalized) => {
                let canonical = Self::canonical(&normalized);
                NormalizationResult::Normalized(canonical.to_string())
            }
            other => other,
        }
    }
}

pub struct GenericNormalizer;
impl Normalizer for GenericNormalizer {
    fn normalize(&self, value: &str) -> NormalizationResult {
        NormalizationResult::Normalized(value.trim().to_lowercase())
    }
}

pub struct HashedNormalizer {
    pub inner: Box<dyn Normalizer>,
    pub salt: String,
}

impl Normalizer for HashedNormalizer {
    fn normalize(&self, value: &str) -> NormalizationResult {
        match self.inner.normalize(value) {
            NormalizationResult::Normalized(n) => {
                let mut hasher = Sha256::new();
                hasher.update(self.salt.as_bytes());
                hasher.update(n.as_bytes());
                let result = hasher.finalize();
                NormalizationResult::Normalized(hex::encode(result))
            }
            res => res,
        }
    }
}

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

    fn assert_phone(input: &str, expected: &str) {
        match PhoneNormalizer.normalize(input) {
            NormalizationResult::Normalized(result) => {
                assert_eq!(result, expected, "Input: {:?}", input);
            }
            NormalizationResult::Invalid(reason) => {
                panic!("Expected {:?} for {:?}, got Invalid({:?})", expected, input, reason);
            }
        }
    }

    fn assert_phone_invalid(input: &str) {
        match PhoneNormalizer.normalize(input) {
            NormalizationResult::Normalized(result) => {
                panic!("Expected Invalid for {:?}, got Normalized({:?})", input, result);
            }
            NormalizationResult::Invalid(_) => {}
        }
    }

    // -- US/NANP numbers (country code 1) --

    #[test]
    fn phone_us_10_digits() {
        assert_phone("5551234567", "+15551234567");
    }

    #[test]
    fn phone_us_formatted_parens() {
        assert_phone("(555) 123-4567", "+15551234567");
    }

    #[test]
    fn phone_us_formatted_dashes() {
        assert_phone("555-123-4567", "+15551234567");
    }

    #[test]
    fn phone_us_formatted_dots() {
        assert_phone("555.123.4567", "+15551234567");
    }

    #[test]
    fn phone_us_with_country_code() {
        assert_phone("15551234567", "+15551234567");
    }

    #[test]
    fn phone_us_e164() {
        assert_phone("+15551234567", "+15551234567");
    }

    #[test]
    fn phone_us_e164_formatted() {
        assert_phone("+1-555-123-4567", "+15551234567");
    }

    #[test]
    fn phone_us_with_spaces() {
        assert_phone("  +1 555 123 4567  ", "+15551234567");
    }

    // -- UK numbers (country code 44) --

    #[test]
    fn phone_uk_e164() {
        assert_phone("+442079460958", "+442079460958");
    }

    #[test]
    fn phone_uk_e164_formatted() {
        assert_phone("+44 20 7946 0958", "+442079460958");
    }

    #[test]
    fn phone_uk_without_plus() {
        assert_phone("442079460958", "+442079460958");
    }

    #[test]
    fn phone_uk_idd_00() {
        assert_phone("00442079460958", "+442079460958");
    }

    #[test]
    fn phone_uk_idd_011() {
        assert_phone("011442079460958", "+442079460958");
    }

    #[test]
    fn phone_uk_mobile_e164() {
        assert_phone("+447700900000", "+447700900000");
    }

    // -- German numbers (country code 49) --

    #[test]
    fn phone_germany_e164() {
        assert_phone("+49891234567", "+49891234567");
    }

    #[test]
    fn phone_germany_idd_00() {
        assert_phone("0049891234567", "+49891234567");
    }

    // -- Japanese numbers (country code 81) --

    #[test]
    fn phone_japan_e164() {
        assert_phone("+81312345678", "+81312345678");
    }

    #[test]
    fn phone_japan_without_plus() {
        assert_phone("81312345678", "+81312345678");
    }

    // -- Indian numbers (country code 91) --

    #[test]
    fn phone_india_e164() {
        assert_phone("+919876543210", "+919876543210");
    }

    #[test]
    fn phone_india_without_plus() {
        assert_phone("919876543210", "+919876543210");
    }

    // -- Australian numbers (country code 61) --

    #[test]
    fn phone_australia_e164() {
        assert_phone("+61412345678", "+61412345678");
    }

    // -- Chinese numbers (country code 86) --

    #[test]
    fn phone_china_e164() {
        assert_phone("+8613800138000", "+8613800138000");
    }

    // -- Brazilian numbers (country code 55) --

    #[test]
    fn phone_brazil_e164() {
        assert_phone("+5511987654321", "+5511987654321");
    }

    // -- South Korean numbers (country code 82) --

    #[test]
    fn phone_korea_e164() {
        assert_phone("+821012345678", "+821012345678");
    }

    // -- IDD prefix handling --

    #[test]
    fn phone_idd_00_to_us() {
        assert_phone("0015551234567", "+15551234567");
    }

    #[test]
    fn phone_idd_011_to_uk() {
        assert_phone("011442079460958", "+442079460958");
    }

    #[test]
    fn phone_idd_00_to_india() {
        assert_phone("00919876543210", "+919876543210");
    }

    // -- Cross-format matching (same number, different formats -> same output) --

    #[test]
    fn phone_cross_format_us() {
        let formats = [
            "5551234567",
            "(555) 123-4567",
            "555-123-4567",
            "+15551234567",
            "+1-555-123-4567",
            "15551234567",
            "0015551234567",
        ];
        for fmt in &formats {
            assert_phone(fmt, "+15551234567");
        }
    }

    #[test]
    fn phone_cross_format_uk() {
        let formats = [
            "+442079460958",
            "+44 20 7946 0958",
            "442079460958",
            "00442079460958",
            "011442079460958",
        ];
        for fmt in &formats {
            assert_phone(fmt, "+442079460958");
        }
    }

    // -- Idempotence: normalizing an already-normalized number --

    #[test]
    fn phone_idempotent_us() {
        assert_phone("+15551234567", "+15551234567");
    }

    #[test]
    fn phone_idempotent_uk() {
        assert_phone("+442079460958", "+442079460958");
    }

    // -- Invalid inputs --

    #[test]
    fn phone_invalid_empty() {
        assert_phone_invalid("");
    }

    #[test]
    fn phone_invalid_no_digits() {
        assert_phone_invalid("abc-def-ghij");
    }

    #[test]
    fn phone_invalid_too_short() {
        assert_phone_invalid("12345");
    }

    #[test]
    fn phone_invalid_too_long() {
        assert_phone_invalid("1234567890123456789");
    }

    // -- Trunk prefix stripping (leading 0) --

    #[test]
    fn phone_uk_local_trunk_prefix() {
        // UK local: 020 7946 0958 -> strip 0 -> 2079460958 -> CC=20, normalized
        assert_phone("02079460958", "+2079460958");
    }

    #[test]
    fn phone_uk_local_formatted() {
        // Same UK local with formatting -> same result
        assert_phone("020 7946 0958", "+2079460958");
    }

    #[test]
    fn phone_french_local_trunk_prefix() {
        // French local: 06 12 34 56 78 -> strip 0 -> 612345678 -> CC=61, normalized
        assert_phone("0612345678", "+612345678");
    }

    #[test]
    fn phone_uk_mobile_local() {
        // UK mobile local: 07700 900000 -> strip 0 -> 7700900000 -> CC=7, normalized
        assert_phone("07700900000", "+7700900000");
    }

    #[test]
    fn phone_german_local_trunk_prefix() {
        // German local: 089 1234567 -> strip 0 -> 891234567 -> CC=86? No, 8 invalid, 89 invalid.
        // Actually 891234567 = 9 digits. CC try: 8 (invalid), 89 (invalid), 891 (invalid) -> invalid
        // Wait - let me check: is there a CC starting with 89? No. So this would be invalid.
        // But German local 089 has 10 digits typically: 089 12345678 -> 008912345678 -> strip 0 -> 8912345678
        // CC: 8 invalid, 89 invalid, 891 invalid -> rejected. Hmm.
        // Actually for 10-digit German: 08912345678 -> strip 0 -> 8912345678 -> CC: 8 nope, 89 nope -> invalid
        // This IS a limitation for countries where stripped trunk digits don't match any CC.
        // German numbers NEED +49 prefix or 0049 IDD to normalize correctly.
        assert_phone_invalid("0891234567");
    }

    #[test]
    fn phone_trunk_prefix_too_short() {
        // 0 + 8 digits = 9 digits total, below the 10-digit minimum for trunk stripping
        assert_phone_invalid("012345678");
    }

    #[test]
    fn phone_cross_format_uk_local_to_local() {
        // Two UK local formats normalize consistently (both strip trunk 0)
        assert_phone("020 7946 0958", "+2079460958");
        assert_phone("02079460958", "+2079460958");
    }

    #[test]
    fn phone_cross_format_french_local_to_local() {
        // Two French local formats normalize consistently
        assert_phone("06 12 34 56 78", "+612345678");
        assert_phone("0612345678", "+612345678");
    }

    #[test]
    fn phone_invalid_bad_country_code() {
        // +0 is never valid (no country code starts with 0)
        assert_phone_invalid("+0123456789");
    }

    // -- Country code validation --

    #[test]
    fn country_code_lookup_1digit() {
        assert!(is_valid_country_code(1));  // US/NANP
        assert!(is_valid_country_code(7));  // Russia
        assert!(!is_valid_country_code(2)); // Not a valid 1-digit code
        assert!(!is_valid_country_code(9)); // Not a valid 1-digit code
    }

    #[test]
    fn country_code_lookup_2digit() {
        assert!(is_valid_country_code(44)); // UK
        assert!(is_valid_country_code(91)); // India
        assert!(is_valid_country_code(86)); // China
        assert!(!is_valid_country_code(99)); // Not assigned
        assert!(!is_valid_country_code(35)); // Not a 2-digit code (350+ are)
    }

    #[test]
    fn country_code_lookup_3digit() {
        assert!(is_valid_country_code(353)); // Ireland
        assert!(is_valid_country_code(234)); // Nigeria
        assert!(is_valid_country_code(852)); // Hong Kong
        assert!(!is_valid_country_code(999)); // Not assigned
    }

    #[test]
    fn extract_cc_prefix_free() {
        // E.164 codes are prefix-free: shortest valid match is always correct
        assert_eq!(extract_country_code("15551234567"), Some((1, 1)));
        assert_eq!(extract_country_code("442079460958"), Some((44, 2)));
        assert_eq!(extract_country_code("353871234567"), Some((353, 3)));
        assert_eq!(extract_country_code("8613800138000"), Some((86, 2)));
        assert_eq!(extract_country_code("919876543210"), Some((91, 2)));
    }
}
