use crate::device::device::{DeviceTag, TagType};
use ip_in_subnet::iface_in_subnet;
use lazy_static::lazy_static;
use log::{debug, error, trace};
use phonenumber::country;
use phonenumber::country::Id;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
lazy_static! {
static ref PHONE_CLEANER: Regex = Regex::new(r"(?:\+|<sip:)?([^>:]+)(?::.*|>.*)?").unwrap();
static ref REQUEST_PLUS: Regex = Regex::new(r"\+\[request\.(\w+)\]").unwrap();
static ref APP_PLUS: Regex = Regex::new(r"\+\[app\.(\w+)\]").unwrap();
static ref REQUEST: Regex = Regex::new(r"\[request\.(\w+)\]").unwrap();
static ref APP: Regex = Regex::new(r"\[app\.(\w+)\]").unwrap();
}
#[derive(Serialize, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum NumberFormat {
#[serde(rename = "national")]
National,
#[serde(rename = "e164")]
E164,
#[serde(rename = "e164p")]
E164Plus,
}
pub fn clean_str_parse_country(country_code: &str, str: &str) -> String {
trace!("Cleaning phone number '{}' with country code '{}'", str, country_code);
let id = match country_code.parse() {
Ok(id) => id,
Err(e) => {
debug!("Invalid country code '{}', defaulting to GB: {}", country_code, e);
country::GB
}
};
clean_str(id, str)
}
pub fn clean_str(country: Id, str: &str) -> String {
trace!("Cleaning phone number '{}' for country {:?}", str, country);
let clean = match PHONE_CLEANER.captures(str) {
Some(caps) => {
caps.get(1).map_or(str, |m| m.as_str())
},
None => {
debug!("Regex pattern didn't match for '{}', using as-is", str);
str
}
};
trace!("Extracted number part: '{}'", clean);
parse_number(country, clean.to_string())
}
pub fn format_number(str: &str, country_code: &str, format: &NumberFormat) -> String {
trace!("Formatting '{}' with country code '{}' using format {:?}", str, country_code, format);
let clean_str = clean_str_parse_country(country_code, str);
match format {
NumberFormat::National => {
let result = national_number(&clean_str, country_code);
debug!("Formatted '{}' to national format: '{}'", str, result);
result
},
NumberFormat::E164 => {
debug!("Formatted '{}' to E164 format: '{}'", str, clean_str);
clean_str
},
NumberFormat::E164Plus => {
let result = format!("+{}", clean_str);
debug!("Formatted '{}' to E164Plus format: '{}'", str, result);
result
},
}
}
fn national_number(str: &str, country_code: &str) -> String {
trace!("Converting '{}' to national format for country '{}'", str, country_code);
let id = match country_code.parse() {
Ok(id) => id,
Err(e) => {
debug!("Invalid country code '{}', defaulting to GB: {}", country_code, e);
country::GB
}
};
parse_number(id, str.to_string())
}
fn parse_number(country: Id, clean: String) -> String {
trace!("Parsing '{}' for country {:?}", clean, country);
match phonenumber::parse(Some(country), &clean) {
Ok(number) => {
let valid = phonenumber::is_valid(&number);
if valid {
let result = format!("{}{}", number.code().value(), number.national().value());
trace!("Successfully parsed number: '{}'", result);
result
} else {
debug!("Number '{}' parsed but not valid, returning as-is", clean);
clean
}
}
Err(e) => {
debug!("Failed to parse '{}': {}", clean, e);
clean
}
}
}
pub fn replace_placeholders_with_formatting(
input: &str,
device_tags: &HashMap<String, DeviceTag>,
country_code: &str,
format: &NumberFormat
) -> String {
debug!("Replacing placeholders in '{}' with country_code '{}', format {:?}",
input, country_code, format);
let mut result = input.to_string();
let replacements = find_tag_replacements(
&result,
device_tags,
country_code,
format,
true
);
for (placeholder, value) in replacements {
result = result.replace(&placeholder, &value);
}
let replacements = find_tag_replacements(
&result,
device_tags,
country_code,
format,
false
);
for (placeholder, value) in replacements {
result = result.replace(&placeholder, &value);
}
debug!("Replacement result: '{}'", result);
result
}
fn find_tag_replacements(
input: &str,
device_tags: &HashMap<String, DeviceTag>,
country_code: &str,
format: &NumberFormat,
with_plus: bool
) -> Vec<(String, String)> {
let placeholder_type = if with_plus { "plus-prefixed" } else { "standard" };
trace!("Finding {} placeholders in '{}'", placeholder_type, input);
let mut replacements = Vec::new();
if with_plus {
for cap in REQUEST_PLUS.captures_iter(input) {
let placeholder = cap[0].to_string();
let tag_name = &cap[1];
let key = format!("request.{}", tag_name);
if let Some(tag) = device_tags.get(&key) {
if tag.tag_type == TagType::Session {
let formatted_value = format_number(&tag.value, country_code, format);
trace!("Found request+ tag: '{}' → '{}'", placeholder, formatted_value);
replacements.push((placeholder, formatted_value));
}
}
}
} else {
for cap in REQUEST.captures_iter(input) {
let placeholder = cap[0].to_string();
let tag_name = &cap[1];
let key = format!("request.{}", tag_name);
if let Some(tag) = device_tags.get(&key) {
if tag.tag_type == TagType::Session {
let formatted_value = format_number(&tag.value, country_code, format);
trace!("Found request tag: '{}' → '{}'", placeholder, formatted_value);
replacements.push((placeholder, formatted_value));
}
}
}
}
if with_plus {
for cap in APP_PLUS.captures_iter(input) {
let placeholder = cap[0].to_string();
let tag_name = &cap[1];
let key = format!("app.{}", tag_name);
if let Some(tag) = device_tags.get(&key) {
if tag.tag_type == TagType::Global {
let formatted_value = format_number(&tag.value, country_code, format);
trace!("Found app+ tag: '{}' → '{}'", placeholder, formatted_value);
replacements.push((placeholder, formatted_value));
}
}
}
} else {
for cap in APP.captures_iter(input) {
let placeholder = cap[0].to_string();
let tag_name = &cap[1];
let key = format!("app.{}", tag_name);
if let Some(tag) = device_tags.get(&key) {
if tag.tag_type == TagType::Global {
let formatted_value = format_number(&tag.value, country_code, format);
trace!("Found app tag: '{}' → '{}'", placeholder, formatted_value);
replacements.push((placeholder, formatted_value));
}
}
}
}
replacements
}
pub fn ip_in_subnet(ip: &str, subnet: &str) -> bool {
trace!("Checking if IP '{}' is in subnet '{}'", ip, subnet);
if ip == subnet {
debug!("IP '{}' matches subnet '{}' directly", ip, subnet);
return true;
}
match iface_in_subnet(ip, subnet) {
Ok(result) => {
debug!("IP '{}' in subnet '{}': {}", ip, subnet, result);
result
},
Err(e) => {
error!("Failed to check if IP '{}' is in subnet '{}': {}", ip, subnet, e);
false
}
}
}