use std::collections::HashMap;
use std::sync::{LazyLock, Mutex};
use phonenumber::country::Id;
use regex::Regex;
use super::{builtin, Candidate, Detector, P_PHONE};
use crate::kind::kinds::PHONE;
use crate::rules::RulesError;
static PATTERN: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"[+(]?\d[\d .()/\-]{5,22}\d").expect("valid phone regex"));
static DATE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^(?:\d{4}[-/.]\d{1,2}[-/.]\d{1,2}|\d{1,2}[-/.]\d{1,2}[-/.]\d{2,4})$").expect("valid date regex")
});
static DATE_PREFIX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^\d{4}-\d{2}-\d{2}(?:[ T]|$)").expect("valid date regex"));
pub(super) struct Phone {
regions: Vec<Id>,
seen: Mutex<HashMap<String, bool>>,
}
const SEEN_MAX: usize = 4096;
impl Phone {
pub fn new(regions: &[String]) -> Result<Self, RulesError> {
let regions = regions
.iter()
.map(|r| r.to_uppercase().parse::<Id>().map_err(|_| RulesError::Region(r.clone())))
.collect::<Result<_, _>>()?;
Ok(Phone { regions, seen: Mutex::default() })
}
fn valid(&self, s: &str) -> bool {
if let Some(&v) = self.seen.lock().unwrap_or_else(|e| e.into_inner()).get(s) {
return v;
}
let v = self.check(s);
let mut seen = self.seen.lock().unwrap_or_else(|e| e.into_inner());
if seen.len() >= SEEN_MAX {
seen.clear();
}
seen.insert(s.to_owned(), v);
v
}
fn check(&self, s: &str) -> bool {
let digits = s.bytes().filter(u8::is_ascii_digit).count();
if !(7..=15).contains(&digits) {
return false;
}
if DATE.is_match(s)
|| DATE_PREFIX.is_match(s)
|| (s.bytes().filter(|&b| b == b'.').count() == 1 && s.bytes().all(|b| b.is_ascii_digit() || b == b'.'))
{
return false;
}
let international = s.starts_with('+') || s.starts_with("00");
let has_separator = s.bytes().any(|b| matches!(b, b' ' | b'-' | b'.' | b'(' | b')' | b'/'));
if !international && !has_separator && !s.starts_with('0') {
return false;
}
if international {
let normalised = match s.strip_prefix("00") {
Some(rest) => format!("+{rest}"),
None => s.to_owned(),
};
return phonenumber::parse(None, &normalised).is_ok_and(|n| phonenumber::is_valid(&n));
}
if s.contains('.') && s.split(['.', ' ', '-']).any(|g| g.len() == 1) {
return false;
}
let trunk = s.trim_start_matches('(').starts_with('0');
self.regions.iter().any(|&r| {
let plausible = match r {
Id::US | Id::CA => digits == 10 || (digits == 11 && s.trim_start_matches('(').starts_with('1')),
_ => trunk,
};
plausible && phonenumber::parse(Some(r), s).is_ok_and(|n| phonenumber::is_valid(&n))
})
}
}
impl Detector for Phone {
fn find(&self, text: &str, out: &mut Vec<Candidate>) {
for m in PATTERN.find_iter(text) {
let (mut start, end) = (m.start(), m.end());
let mut s = m.as_str();
if s.starts_with('(') && !s.contains(')') {
start += 1;
s = &s[1..];
}
let before = text[..start].chars().next_back();
let after = text[end..].chars().next();
if before.is_some_and(|c| c.is_alphanumeric() || matches!(c, '.' | '/' | '-' | '_' | '+'))
|| after.is_some_and(|c| c.is_alphanumeric() || c == '_')
|| (after == Some('.') && text[end + 1..].starts_with(|c: char| c.is_ascii_digit()))
{
continue;
}
if self.valid(s) {
out.push(builtin(start, end, PHONE, P_PHONE));
}
}
}
}