use super::types::NormalizedEntity;

pub trait BlockingStrategy: Send + Sync {
    fn generate_block_key(&self, entity: &NormalizedEntity) -> Vec<String>;
}

pub mod lsh;

pub struct FieldBlocking {
    pub fields: Vec<String>,
}

impl BlockingStrategy for FieldBlocking {
    fn generate_block_key(&self, entity: &NormalizedEntity) -> Vec<String> {
        self.fields
            .iter()
            .filter_map(|field| entity.data.get(field).map(|v| format!("{}:{}", field, v)))
            .collect()
    }
}

pub struct DomainBlocking;
impl BlockingStrategy for DomainBlocking {
    fn generate_block_key(&self, entity: &NormalizedEntity) -> Vec<String> {
        fn extract_domain(value: &str) -> Option<String> {
            if let Some(at_pos) = value.rfind('@') {
                Some(value[at_pos + 1..].to_lowercase())
            } else if value.contains('.') {
                let s = value.strip_prefix("https://").or_else(|| value.strip_prefix("http://")).unwrap_or(value);
                let s = s.strip_prefix("www.").unwrap_or(s);
                Some(s.split('/').next().unwrap_or(s).to_lowercase())
            } else {
                None
            }
        }

        let mut keys = Vec::new();
        for field in ["email", "website"] {
            if let Some(val) = entity.data.get(field) {
                if let Some(domain) = extract_domain(val) {
                    keys.push(format!("domain:{}", domain));
                }
            }
        }
        keys
    }
}

pub struct SortedNeighborhoodBlocking {
    pub sort_fields: Vec<String>,
}

impl BlockingStrategy for SortedNeighborhoodBlocking {
    fn generate_block_key(&self, entity: &NormalizedEntity) -> Vec<String> {
        let key_parts: Vec<&str> = self.sort_fields.iter()
            .filter_map(|f| entity.data.get(f).map(|v| v.as_str()))
            .collect();
        if key_parts.is_empty() {
            return vec![];
        }
        vec![format!("sorted_neighborhood:{}", key_parts.join(":"))]
    }
}

/// Shared blocking key computation. Single source of truth for both batch and realtime.
///
/// Takes blocking key definitions from the identity plan and a flat data map,
/// returns composite key strings suitable for GIN-indexed array overlap queries.
pub fn compute_blocking_keys(
    key_defs: &[cannon_common::ir::BlockingKey],
    data: &std::collections::HashMap<String, String>,
) -> Vec<String> {
    use cannon_common::ir::BlockingTransformation;
    use rphonetic::{Soundex, DoubleMetaphone, Encoder};

    let mut block_keys = Vec::new();

    for key_def in key_defs {
         let mut candidates = vec![String::new()];
         let mut valid_key = true;

         for (i, field) in key_def.fields.iter().enumerate() {
             if let Some(val) = data.get(field).map(|v| v.as_str()) {
                 let mut field_values = vec![val.to_string()];

                 for t in &key_def.transformations {
                      let mut next_values = Vec::new();
                      for v in field_values {
                          match t {
                              BlockingTransformation::Lowercase => next_values.push(v.to_lowercase()),
                              BlockingTransformation::FirstInitial => {
                                  next_values.push(v.chars().next().map(|c| c.to_string()).unwrap_or_default());
                              },
                              BlockingTransformation::Soundex => {
                                  let encoder = Soundex::default();
                                  next_values.push(encoder.encode(&v));
                              },
                              BlockingTransformation::DoubleMetaphone => {
                                  let encoder = DoubleMetaphone::default();
                                  next_values.push(encoder.encode(&v));
                              },
                              BlockingTransformation::AreaCode => {
                                  let digits: String = v.chars().filter(|c| c.is_ascii_digit()).collect();
                                  let area_digits = if digits.len() >= 11 && digits.starts_with('1') {
                                      &digits[1..]
                                  } else {
                                      &digits
                                  };
                                  if area_digits.len() >= 3 {
                                      next_values.push(area_digits[0..3].to_string());
                                  } else {
                                      next_values.push(v);
                                  }
                              },
                              BlockingTransformation::Last4 => {
                                   let digits: String = v.chars().filter(|c| c.is_ascii_digit()).collect();
                                   if digits.len() >= 4 {
                                       next_values.push(digits[digits.len()-4..].to_string());
                                   } else {
                                       next_values.push(v);
                                   }
                              },
                              BlockingTransformation::Trigrams => {
                                  let chars: Vec<char> = v.chars().collect();
                                  if chars.len() < 3 {
                                      next_values.push(v);
                                  } else {
                                      for window in chars.windows(3) {
                                          next_values.push(window.iter().collect());
                                      }
                                  }
                              }
                              BlockingTransformation::NormalizePhone => {
                                  use super::normalization::{PhoneNormalizer, Normalizer, NormalizationResult};
                                  match PhoneNormalizer.normalize(&v) {
                                      NormalizationResult::Normalized(n) => next_values.push(n),
                                      _ => next_values.push(v),
                                  }
                              }
                              BlockingTransformation::NormalizeEmail => {
                                  use super::normalization::{EmailNormalizer, Normalizer, NormalizationResult};
                                  match EmailNormalizer.normalize(&v) {
                                      NormalizationResult::Normalized(n) => next_values.push(n),
                                      _ => next_values.push(v),
                                  }
                              }
                              BlockingTransformation::NormalizeName => {
                                  use super::normalization::{NameNormalizer, Normalizer, NormalizationResult};
                                  match NameNormalizer.normalize(&v) {
                                      NormalizationResult::Normalized(n) => next_values.push(n),
                                      _ => next_values.push(v),
                                  }
                              }
                              BlockingTransformation::NormalizeNickname => {
                                  use super::normalization::{NicknameNormalizer, Normalizer, NormalizationResult};
                                  match NicknameNormalizer.normalize(&v) {
                                      NormalizationResult::Normalized(n) => next_values.push(n),
                                      _ => next_values.push(v),
                                  }
                              }
                              BlockingTransformation::NormalizeDomain => {
                                  use super::normalization::{DomainNormalizer, Normalizer, NormalizationResult};
                                  match DomainNormalizer.normalize(&v) {
                                      NormalizationResult::Normalized(n) => next_values.push(n),
                                      _ => next_values.push(v),
                                  }
                              }
                              BlockingTransformation::NormalizeBarcode => {
                                  use super::normalization::{BarcodeNormalizer, Normalizer, NormalizationResult};
                                  match BarcodeNormalizer.normalize(&v) {
                                      NormalizationResult::Normalized(n) => next_values.push(n),
                                      _ => next_values.push(v),
                                  }
                              }
                              BlockingTransformation::NormalizeMpn => {
                                  use super::normalization::{MpnNormalizer, Normalizer, NormalizationResult};
                                  match MpnNormalizer.normalize(&v) {
                                      NormalizationResult::Normalized(n) => next_values.push(n),
                                      _ => next_values.push(v),
                                  }
                              }
                              BlockingTransformation::NormalizeBrand => {
                                  use super::normalization::{BrandNormalizer, Normalizer, NormalizationResult};
                                  match BrandNormalizer.normalize(&v) {
                                      NormalizationResult::Normalized(n) => next_values.push(n),
                                      _ => next_values.push(v),
                                  }
                              }
                              BlockingTransformation::Geohash => {
                                  // Expect "lat,lon" format. Encode as precision-6 geohash (~1.2km cells).
                                  let parts: Vec<&str> = v.split(',').collect();
                                  if parts.len() == 2 {
                                      if let (Ok(lat), Ok(lon)) = (parts[0].trim().parse::<f64>(), parts[1].trim().parse::<f64>()) {
                                          next_values.push(super::geospatial::encode_geohash(lat, lon, 6));
                                      } else {
                                          next_values.push(v);
                                      }
                                  } else {
                                      next_values.push(v);
                                  }
                              }
                              BlockingTransformation::GridCell => {
                                  // Expect "lat,lon" format. Truncate to 3 decimal places (~100m cells).
                                  let parts: Vec<&str> = v.split(',').collect();
                                  if parts.len() >= 2 {
                                      if let (Ok(lat), Ok(lon)) = (parts[0].trim().parse::<f64>(), parts[1].trim().parse::<f64>()) {
                                          let lat_cell = (lat * 1000.0).trunc() / 1000.0;
                                          let lon_cell = (lon * 1000.0).trunc() / 1000.0;
                                          next_values.push(format!("{:.3},{:.3}", lat_cell, lon_cell));
                                      } else {
                                          next_values.push(v);
                                      }
                                  } else {
                                      next_values.push(v);
                                  }
                              }
                              BlockingTransformation::TokenizeProduct => {
                                  let lowered = v.to_lowercase();
                                  let stopwords = [
                                      "the", "a", "an", "and", "or", "for", "with", "in",
                                      "of", "by", "on", "to", "is", "it", "at", "as",
                                  ];
                                  // Color/finish words stripped so model and variant records
                                  // produce the same blocking key. "Galaxy S24 Onyx Black"
                                  // and "Galaxy S24" both become "galaxy_s24".
                                  let colors = [
                                      "black", "white", "silver", "gray", "grey", "gold",
                                      "blue", "red", "green", "pink", "purple", "orange",
                                      "yellow", "brown", "cream", "beige", "navy", "olive",
                                      "copper", "bronze", "titanium", "graphite", "platinum",
                                      "midnight", "starlight", "charcoal", "sand", "coral",
                                      "lavender", "mint", "burgundy", "teal", "ivory",
                                      "rose", "violet", "indigo", "aqua", "crimson",
                                      // Abbreviated colors (supplier feeds)
                                      "blk", "wht", "slv", "gry", "blu", "grn", "pnk",
                                      "gld", "ylw", "org", "brn", "crm", "nvy",
                                      // Marketing color prefixes
                                      "phantom", "onyx", "cobalt", "marble", "amber",
                                      "cosmic", "volcanic", "arctic", "lunar", "storm",
                                      "eclipse", "titan", "desert", "ember", "frost",
                                      "ocean", "forest", "sunset", "sunrise",
                                  ];
                                  // SEO filler stripped so ecommerce titles block with
                                  // clean PIM model names.
                                  let seo_noise = [
                                      "smartphone", "cell", "phone", "cellphone",
                                      "laptop", "notebook", "computer", "pc",
                                      "tablet", "reader", "ereader",
                                      "headphones", "earbuds", "earphones", "headset",
                                      "speaker", "soundbar", "subwoofer",
                                      "monitor", "display", "screen",
                                      "camera", "camcorder", "mirrorless", "dslr",
                                      "smartwatch", "tracker", "fitness", "wearable",
                                      "console", "controller", "gamepad", "joystick",
                                      "tv", "television",
                                      "wireless", "bluetooth", "wifi",
                                      "unlocked", "factory", "refurbished", "renewed",
                                      "new", "latest", "model", "edition", "version",
                                      "dual", "sim", "esim",
                                      "touchscreen", "convertible",
                                      "portable", "waterproof", "dustproof",
                                      "noise", "canceling", "cancelling", "active",
                                      "rgb", "mechanical", "gaming", "streaming",
                                      "smart", "digital", "hd", "fhd", "uhd", "qhd",
                                      "hdr", "ips", "oled", "qled", "led",
                                      "usb", "type",
                                  ];
                                  let mut tokens: Vec<&str> = lowered
                                      .split(|c: char| {
                                          c.is_whitespace()
                                          || c == '-' || c == '/' || c == '|' || c == ','
                                          || c == '(' || c == ')' || c == '[' || c == ']'
                                          || c == '{' || c == '}'
                                      })
                                      .filter(|t| {
                                          !t.is_empty()
                                          && !stopwords.contains(t)
                                          && !colors.contains(t)
                                          && !seo_noise.contains(t)
                                      })
                                      .collect();
                                  tokens.sort_unstable();
                                  next_values.push(tokens.join("_"));
                              }
                          }
                      }
                      field_values = next_values;
                 }

                 let mut new_candidates = Vec::new();
                 for c in candidates {
                     for fv in &field_values {
                         let mut new_c = c.clone();
                         if i > 0 { new_c.push('|'); }
                         new_c.push_str(fv);
                         new_candidates.push(new_c);
                     }
                 }
                 candidates = new_candidates;
             } else {
                 valid_key = false;
                 break;
             }

             if candidates.len() > 100 {
                 candidates.truncate(100);
                 break;
             }
         }

         if valid_key {
             block_keys.extend(candidates);
         }
    }

    block_keys
}

pub struct PlanBlocking {
    pub keys: Vec<cannon_common::ir::BlockingKey>,
}

impl BlockingStrategy for PlanBlocking {
    fn generate_block_key(&self, entity: &NormalizedEntity) -> Vec<String> {
        compute_blocking_keys(&self.keys, &entity.data)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::collections::HashMap;
    use cannon_common::ir::{BlockingKey, BlockingTransformation};

    #[test]
    fn test_compute_blocking_keys_empty_defs() {
        let data: HashMap<String, String> = [("email".into(), "a@b.com".into())].into();
        let keys = compute_blocking_keys(&[], &data);
        assert!(keys.is_empty(), "Empty key_defs should produce no keys");
    }

    #[test]
    fn test_compute_blocking_keys_single_field_lowercase() {
        let defs = vec![BlockingKey {
            fields: vec!["email".to_string()],
            transformations: vec![BlockingTransformation::Lowercase],
        }];
        let data: HashMap<String, String> = [("email".into(), "Alice@EXAMPLE.COM".into())].into();
        let keys = compute_blocking_keys(&defs, &data);
        assert_eq!(keys, vec!["alice@example.com"]);
    }

    #[test]
    fn test_compute_blocking_keys_multi_field_composite() {
        let defs = vec![BlockingKey {
            fields: vec!["first_name".to_string(), "last_name".to_string()],
            transformations: vec![BlockingTransformation::Lowercase],
        }];
        let data: HashMap<String, String> = [
            ("first_name".into(), "Bob".into()),
            ("last_name".into(), "Smith".into()),
        ].into();
        let keys = compute_blocking_keys(&defs, &data);
        assert_eq!(keys.len(), 1);
        assert_eq!(keys[0], "bob|smith");
    }

    #[test]
    fn test_compute_blocking_keys_missing_field_skips_key() {
        let defs = vec![BlockingKey {
            fields: vec!["first_name".to_string(), "last_name".to_string()],
            transformations: vec![BlockingTransformation::Lowercase],
        }];
        // Only first_name, missing last_name
        let data: HashMap<String, String> = [("first_name".into(), "Bob".into())].into();
        let keys = compute_blocking_keys(&defs, &data);
        assert!(keys.is_empty(), "Missing required field should skip key");
    }

    #[test]
    fn test_compute_blocking_keys_multiple_defs() {
        let defs = vec![
            BlockingKey {
                fields: vec!["email".to_string()],
                transformations: vec![BlockingTransformation::Lowercase],
            },
            BlockingKey {
                fields: vec!["phone".to_string()],
                transformations: vec![BlockingTransformation::NormalizePhone],
            },
        ];
        let data: HashMap<String, String> = [
            ("email".into(), "A@B.COM".into()),
            ("phone".into(), "+1-555-123-4567".into()),
        ].into();
        let keys = compute_blocking_keys(&defs, &data);
        assert_eq!(keys.len(), 2, "Should produce one key per def");
        assert!(keys.iter().any(|k| k == "a@b.com"), "Should have email key");
        // Phone key should be normalized (digits only, E.164 style)
        assert!(keys.iter().any(|k| k.chars().all(|c| c.is_ascii_digit() || c == '+')),
            "Phone key should be normalized digits: {:?}", keys);
    }

    #[test]
    fn test_compute_blocking_keys_empty_data() {
        let defs = vec![BlockingKey {
            fields: vec!["email".to_string()],
            transformations: vec![BlockingTransformation::Lowercase],
        }];
        let data: HashMap<String, String> = HashMap::new();
        let keys = compute_blocking_keys(&defs, &data);
        assert!(keys.is_empty(), "Empty data should produce no keys");
    }

    #[test]
    fn test_compute_blocking_keys_soundex() {
        let defs = vec![BlockingKey {
            fields: vec!["last_name".to_string()],
            transformations: vec![BlockingTransformation::Soundex],
        }];
        let data_a: HashMap<String, String> = [("last_name".into(), "Smith".into())].into();
        let data_b: HashMap<String, String> = [("last_name".into(), "Smyth".into())].into();
        let keys_a = compute_blocking_keys(&defs, &data_a);
        let keys_b = compute_blocking_keys(&defs, &data_b);
        // Smith and Smyth should produce the same soundex code
        assert_eq!(keys_a, keys_b, "Smith and Smyth should have same soundex key");
    }

    #[test]
    fn test_compute_blocking_keys_first_initial() {
        let defs = vec![BlockingKey {
            fields: vec!["first_name".to_string()],
            transformations: vec![BlockingTransformation::FirstInitial],
        }];
        let data: HashMap<String, String> = [("first_name".into(), "Robert".into())].into();
        let keys = compute_blocking_keys(&defs, &data);
        assert_eq!(keys, vec!["R"]);
    }

    #[test]
    fn test_compute_blocking_keys_area_code() {
        let defs = vec![BlockingKey {
            fields: vec!["phone".to_string()],
            transformations: vec![BlockingTransformation::AreaCode],
        }];
        let data: HashMap<String, String> = [("phone".into(), "15551234567".into())].into();
        let keys = compute_blocking_keys(&defs, &data);
        assert_eq!(keys, vec!["555"], "Should extract area code from 11-digit number");
    }

    #[test]
    fn test_compute_blocking_keys_last4() {
        let defs = vec![BlockingKey {
            fields: vec!["phone".to_string()],
            transformations: vec![BlockingTransformation::Last4],
        }];
        let data: HashMap<String, String> = [("phone".into(), "5551234567".into())].into();
        let keys = compute_blocking_keys(&defs, &data);
        assert_eq!(keys, vec!["4567"], "Should extract last 4 digits");
    }

    #[test]
    fn test_compute_blocking_keys_trigrams() {
        let defs = vec![BlockingKey {
            fields: vec!["name".to_string()],
            transformations: vec![BlockingTransformation::Trigrams],
        }];
        let data: HashMap<String, String> = [("name".into(), "hello".into())].into();
        let keys = compute_blocking_keys(&defs, &data);
        assert_eq!(keys, vec!["hel", "ell", "llo"], "Should produce 3-char windows");
    }

    #[test]
    fn test_compute_blocking_keys_trigrams_short_string() {
        let defs = vec![BlockingKey {
            fields: vec!["name".to_string()],
            transformations: vec![BlockingTransformation::Trigrams],
        }];
        let data: HashMap<String, String> = [("name".into(), "ab".into())].into();
        let keys = compute_blocking_keys(&defs, &data);
        assert_eq!(keys, vec!["ab"], "Short string should pass through unchanged");
    }

    #[test]
    fn test_compute_blocking_keys_normalize_barcode() {
        let defs = vec![BlockingKey {
            fields: vec!["barcode".to_string()],
            transformations: vec![BlockingTransformation::NormalizeBarcode],
        }];
        let data: HashMap<String, String> = [("barcode".into(), "012345678905".into())].into();
        let keys = compute_blocking_keys(&defs, &data);
        assert!(!keys.is_empty(), "Should produce a barcode key");
        assert_eq!(keys.len(), 1);
    }

    #[test]
    fn test_compute_blocking_keys_tokenize_product() {
        let defs = vec![BlockingKey {
            fields: vec!["title".to_string()],
            transformations: vec![BlockingTransformation::TokenizeProduct],
        }];
        // Colors and SEO noise should be stripped
        let data_a: HashMap<String, String> =
            [("title".into(), "Samsung Galaxy S24 Ultra Onyx Black Smartphone".into())].into();
        let data_b: HashMap<String, String> =
            [("title".into(), "Samsung Galaxy S24 Ultra Silver".into())].into();
        let keys_a = compute_blocking_keys(&defs, &data_a);
        let keys_b = compute_blocking_keys(&defs, &data_b);
        assert_eq!(keys_a, keys_b,
            "Product titles differing only by color/SEO should produce same key.\n  a={:?}\n  b={:?}",
            keys_a, keys_b);
    }

    #[test]
    fn test_compute_blocking_keys_candidate_truncation() {
        // Trigrams on a long string can produce many candidates
        let defs = vec![BlockingKey {
            fields: vec!["text".to_string()],
            transformations: vec![BlockingTransformation::Trigrams],
        }];
        // 200 chars produces 198 trigrams, but should be capped at 100
        let long_text: String = (0..200).map(|i| ((i % 26) as u8 + b'a') as char).collect();
        let data: HashMap<String, String> = [("text".into(), long_text)].into();
        let keys = compute_blocking_keys(&defs, &data);
        assert!(keys.len() <= 100, "Should cap at 100 keys, got {}", keys.len());
    }

    #[test]
    fn test_plan_blocking_delegates_to_compute() {
        let plan = PlanBlocking {
            keys: vec![BlockingKey {
                fields: vec!["email".to_string()],
                transformations: vec![BlockingTransformation::Lowercase],
            }],
        };
        let entity = NormalizedEntity {
            id: uuid::Uuid::nil(),
            tenant_id: uuid::Uuid::nil(),
            source_name: "test".to_string(),
            external_id: "1".to_string(),
            entity_type: "person".to_string(),
            data: [("email".into(), "Test@Example.com".into())].into(),
            valid_from: None,
            valid_to: None,
            last_updated: chrono::Utc::now(),
        };

        let via_trait = plan.generate_block_key(&entity);
        let via_fn = compute_blocking_keys(&plan.keys, &entity.data);
        assert_eq!(via_trait, via_fn, "PlanBlocking should delegate identically");
        assert_eq!(via_trait, vec!["test@example.com"]);
    }
}
