//! Configuration types received from the TypeScript control plane.
//!
//! TypeScript loads config from `.nogit/config.json` and sends it to the
//! proxy engine via the `configure` command. These types mirror the TS interfaces.

use serde::Deserialize;
use sip_proto::message::SipMessage;
use std::net::SocketAddr;

/// Network endpoint.
#[derive(Debug, Clone, Deserialize)]
pub struct Endpoint {
    pub address: String,
    pub port: u16,
}

impl Endpoint {
    /// Resolve to a SocketAddr. Handles both IP addresses and hostnames.
    pub fn to_socket_addr(&self) -> Option<SocketAddr> {
        // Try direct parse first (IP address).
        if let Ok(addr) = format!("{}:{}", self.address, self.port).parse() {
            return Some(addr);
        }
        // DNS resolution for hostnames.
        use std::net::ToSocketAddrs;
        format!("{}:{}", self.address, self.port)
            .to_socket_addrs()
            .ok()
            .and_then(|mut addrs| addrs.next())
    }
}

/// Provider quirks for codec/protocol workarounds.
//
// Deserialized from provider config for TS parity. Early-media silence
// injection and related workarounds are not yet ported to the Rust engine,
// so every field is populated by serde but not yet consumed.
#[allow(dead_code)]
#[derive(Debug, Clone, Deserialize)]
pub struct Quirks {
    #[serde(rename = "earlyMediaSilence")]
    pub early_media_silence: bool,
    #[serde(rename = "silencePayloadType")]
    pub silence_payload_type: Option<u8>,
    #[serde(rename = "silenceMaxPackets")]
    pub silence_max_packets: Option<u32>,
}

/// A SIP trunk provider configuration.
#[derive(Debug, Clone, Deserialize)]
pub struct ProviderConfig {
    pub id: String,
    // UI label — populated by serde for parity with the TS config, not
    // consumed at runtime.
    #[allow(dead_code)]
    #[serde(rename = "displayName")]
    pub display_name: String,
    pub domain: String,
    #[serde(rename = "outboundProxy")]
    pub outbound_proxy: Endpoint,
    pub username: String,
    pub password: String,
    #[serde(rename = "registerIntervalSec")]
    pub register_interval_sec: u32,
    pub codecs: Vec<u8>,
    // Workaround knobs populated by serde but not yet acted upon — see Quirks.
    #[allow(dead_code)]
    pub quirks: Quirks,
}

/// A SIP device (phone) configuration.
#[derive(Debug, Clone, Deserialize)]
pub struct DeviceConfig {
    pub id: String,
    #[serde(rename = "displayName")]
    pub display_name: String,
    #[serde(rename = "expectedAddress")]
    pub expected_address: String,
    pub extension: String,
}

/// Route match criteria.
#[derive(Debug, Clone, Deserialize)]
pub struct RouteMatch {
    pub direction: String, // "inbound" | "outbound"
    #[serde(rename = "numberPattern")]
    pub number_pattern: Option<String>,
    #[serde(rename = "callerPattern")]
    pub caller_pattern: Option<String>,
    #[serde(rename = "sourceProvider")]
    pub source_provider: Option<String>,
    #[serde(rename = "sourceDevice")]
    pub source_device: Option<String>,
}

/// Route action.
#[derive(Debug, Clone, Deserialize)]
// Several fields (voicemail_box, ivr_menu_id, no_answer_timeout) are read
// by resolve_inbound_route but not yet honored downstream — see the
// multi-target TODO in CallManager::create_inbound_call.
#[allow(dead_code)]
pub struct RouteAction {
    pub targets: Option<Vec<String>>,
    #[serde(rename = "ringBrowsers")]
    pub ring_browsers: Option<bool>,
    #[serde(rename = "voicemailBox")]
    pub voicemail_box: Option<String>,
    #[serde(rename = "faxBox")]
    pub fax_box: Option<String>,
    #[serde(rename = "ivrMenuId")]
    pub ivr_menu_id: Option<String>,
    #[serde(rename = "noAnswerTimeout")]
    pub no_answer_timeout: Option<u32>,
    pub provider: Option<String>,
    #[serde(rename = "failoverProviders")]
    pub failover_providers: Option<Vec<String>>,
    #[serde(rename = "stripPrefix")]
    pub strip_prefix: Option<String>,
    #[serde(rename = "prependPrefix")]
    pub prepend_prefix: Option<String>,
}

/// A routing rule.
#[derive(Debug, Clone, Deserialize)]
pub struct Route {
    // `id` and `name` are UI identifiers, populated by serde but not
    // consumed by the resolvers.
    #[allow(dead_code)]
    pub id: String,
    #[allow(dead_code)]
    pub name: String,
    pub priority: i32,
    pub enabled: bool,
    #[serde(rename = "match")]
    pub match_criteria: RouteMatch,
    pub action: RouteAction,
}

/// Proxy network settings.
#[derive(Debug, Clone, Deserialize)]
pub struct ProxyConfig {
    #[serde(rename = "lanIp")]
    pub lan_ip: String,
    #[serde(rename = "lanPort")]
    pub lan_port: u16,
    #[serde(rename = "publicIpSeed")]
    pub public_ip_seed: Option<String>,
    #[serde(rename = "rtpPortRange")]
    pub rtp_port_range: RtpPortRange,
}

#[derive(Debug, Clone, Deserialize)]
pub struct RtpPortRange {
    pub min: u16,
    pub max: u16,
}

/// Full application config pushed from TypeScript.
#[derive(Debug, Clone, Deserialize)]
pub struct AppConfig {
    pub proxy: ProxyConfig,
    pub providers: Vec<ProviderConfig>,
    pub devices: Vec<DeviceConfig>,
    pub routing: RoutingConfig,
    #[serde(default)]
    pub faxboxes: Vec<FaxBoxConfig>,
    #[serde(default)]
    pub voiceboxes: Vec<VoiceboxConfig>,
    #[serde(default)]
    pub ivr: Option<IvrConfig>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct RoutingConfig {
    pub routes: Vec<Route>,
}

// ---------------------------------------------------------------------------
// Voicebox config
// ---------------------------------------------------------------------------

#[allow(dead_code)]
#[derive(Debug, Clone, Deserialize)]
pub struct VoiceboxConfig {
    pub id: String,
    #[serde(default)]
    pub enabled: bool,
    #[serde(rename = "greetingText")]
    pub greeting_text: Option<String>,
    #[serde(rename = "greetingVoice")]
    pub greeting_voice: Option<String>,
    #[serde(rename = "greetingWavPath")]
    pub greeting_wav_path: Option<String>,
    #[serde(rename = "maxRecordingSec")]
    pub max_recording_sec: Option<u32>,
}

#[allow(dead_code)]
#[derive(Debug, Clone, Deserialize)]
pub struct FaxBoxConfig {
    pub id: String,
    #[serde(default)]
    pub enabled: bool,
    #[serde(rename = "maxMessages")]
    pub max_messages: Option<u32>,
}

// ---------------------------------------------------------------------------
// IVR config
// ---------------------------------------------------------------------------

#[allow(dead_code)]
#[derive(Debug, Clone, Deserialize)]
pub struct IvrConfig {
    pub enabled: bool,
    pub menus: Vec<IvrMenuConfig>,
    #[serde(rename = "entryMenuId")]
    pub entry_menu_id: String,
}

#[derive(Debug, Clone, Deserialize)]
pub struct IvrMenuConfig {
    pub id: String,
    #[serde(rename = "promptText")]
    pub prompt_text: String,
    #[serde(rename = "promptVoice")]
    pub prompt_voice: Option<String>,
    pub entries: Vec<IvrMenuEntry>,
    #[serde(rename = "timeoutSec")]
    pub timeout_sec: Option<u32>,
}

#[allow(dead_code)]
#[derive(Debug, Clone, Deserialize)]
pub struct IvrMenuEntry {
    pub digit: String,
    pub action: String,
    pub target: Option<String>,
}

// ---------------------------------------------------------------------------
// Pattern matching (ported from ts/config.ts)
// ---------------------------------------------------------------------------

/// Extract the URI user part and normalize phone-like identities for routing.
///
/// This keeps inbound route matching stable across provider-specific URI shapes,
/// e.g. `sip:+49 421 219694@trunk.example` and `sip:0049421219694@trunk.example`
/// both normalize to `+49421219694`.
pub fn normalize_routing_identity(value: &str) -> String {
    let extracted = SipMessage::extract_uri_user(value).unwrap_or(value).trim();
    if extracted.is_empty() {
        return String::new();
    }

    let mut digits = String::new();
    let mut saw_plus = false;

    for (idx, ch) in extracted.chars().enumerate() {
        if ch.is_ascii_digit() {
            digits.push(ch);
            continue;
        }

        if ch == '+' && idx == 0 {
            saw_plus = true;
            continue;
        }

        if matches!(ch, ' ' | '\t' | '-' | '.' | '/' | '(' | ')') {
            continue;
        }

        return extracted.to_string();
    }

    if digits.is_empty() {
        return extracted.to_string();
    }
    if saw_plus {
        return format!("+{digits}");
    }
    if digits.starts_with("00") && digits.len() > 2 {
        return format!("+{}", &digits[2..]);
    }

    digits
}

fn looks_like_phone_identity(value: &str) -> bool {
    let digits = value.chars().filter(|c| c.is_ascii_digit()).count();
    digits >= 6 && value.chars().all(|c| c.is_ascii_digit() || c == '+')
}

/// Pick the best inbound called-number identity from common SIP headers.
///
/// Some providers deliver the DID in `To` / `P-Called-Party-ID` while the
/// request URI contains an account username. Prefer a phone-like identity when
/// present; otherwise fall back to the request URI user part.
pub fn extract_inbound_called_number(msg: &SipMessage) -> String {
    let request_uri = normalize_routing_identity(msg.request_uri().unwrap_or(""));
    if looks_like_phone_identity(&request_uri) {
        return request_uri;
    }

    for header_name in [
        "P-Called-Party-ID",
        "X-Called-Party-ID",
        "Diversion",
        "History-Info",
        "To",
    ] {
        let candidate = normalize_routing_identity(msg.get_header(header_name).unwrap_or(""));
        if looks_like_phone_identity(&candidate) {
            return candidate;
        }
    }

    request_uri
}

fn parse_numeric_range_value(value: &str) -> Option<(bool, &str)> {
    let trimmed = value.trim();
    if trimmed.is_empty() {
        return None;
    }

    let (has_plus, digits) = if let Some(rest) = trimmed.strip_prefix('+') {
        (true, rest)
    } else {
        (false, trimmed)
    };

    if digits.is_empty() || !digits.chars().all(|c| c.is_ascii_digit()) {
        return None;
    }

    Some((has_plus, digits))
}

fn matches_numeric_range_pattern(pattern: &str, value: &str) -> bool {
    let Some((start, end)) = pattern.split_once("..") else {
        return false;
    };

    let Some((start_plus, start_digits)) = parse_numeric_range_value(start) else {
        return false;
    };
    let Some((end_plus, end_digits)) = parse_numeric_range_value(end) else {
        return false;
    };
    let Some((value_plus, value_digits)) = parse_numeric_range_value(value) else {
        return false;
    };

    if start_plus != end_plus || value_plus != start_plus {
        return false;
    }
    if start_digits.len() != end_digits.len() || value_digits.len() != start_digits.len() {
        return false;
    }
    if start_digits > end_digits {
        return false;
    }

    value_digits >= start_digits && value_digits <= end_digits
}

/// Test a value against a pattern string.
/// - None/empty: matches everything (wildcard)
/// - `start..end`: numeric range match
/// - Trailing '*': prefix match
/// - Starts with '/': regex match
/// - Otherwise: exact match
pub fn matches_pattern(pattern: Option<&str>, value: &str) -> bool {
    let pattern = match pattern {
        None => return true,
        Some(p) if p.is_empty() => return true,
        Some(p) => p,
    };

    // Prefix match: "+49*"
    if pattern.ends_with('*') {
        return value.starts_with(&pattern[..pattern.len() - 1]);
    }

    if matches_numeric_range_pattern(pattern, value) {
        return true;
    }

    // Regex match: "/^\\+49/" or "/pattern/i"
    if pattern.starts_with('/') {
        if let Some(last_slash) = pattern[1..].rfind('/') {
            let re_str = &pattern[1..1 + last_slash];
            let flags = &pattern[2 + last_slash..];
            let case_insensitive = flags.contains('i');
            if let Ok(re) = if case_insensitive {
                regex_lite::Regex::new(&format!("(?i){re_str}"))
            } else {
                regex_lite::Regex::new(re_str)
            } {
                return re.is_match(value);
            }
        }
    }

    // Exact match.
    value == pattern
}

/// Result of resolving an outbound route.
pub struct OutboundRouteResult {
    pub provider: ProviderConfig,
    // TODO: prefix rewriting is unfinished — this is computed but the
    // caller ignores it and uses the raw dialed number.
    #[allow(dead_code)]
    pub transformed_number: String,
}

/// Result of resolving an inbound route.
//
// `device_ids`, `ring_all_devices`, and `ring_browsers` are consumed by
// create_inbound_call.
// The remaining fields (voicemail_box, ivr_menu_id, no_answer_timeout)
// are resolved but not yet acted upon — see the multi-target TODO.
#[allow(dead_code)]
pub struct InboundRouteResult {
    pub device_ids: Vec<String>,
    pub ring_all_devices: bool,
    pub ring_browsers: bool,
    pub voicemail_box: Option<String>,
    pub fax_box: Option<String>,
    pub ivr_menu_id: Option<String>,
    pub no_answer_timeout: Option<u32>,
}

impl AppConfig {
    /// Resolve which provider to use for an outbound call.
    pub fn resolve_outbound_route(
        &self,
        dialed_number: &str,
        source_device_id: Option<&str>,
        is_provider_registered: &dyn Fn(&str) -> bool,
    ) -> Option<OutboundRouteResult> {
        let mut routes: Vec<&Route> = self
            .routing
            .routes
            .iter()
            .filter(|r| r.enabled && r.match_criteria.direction == "outbound")
            .collect();
        routes.sort_by(|a, b| b.priority.cmp(&a.priority));

        for route in &routes {
            let m = &route.match_criteria;

            if !matches_pattern(m.number_pattern.as_deref(), dialed_number) {
                continue;
            }
            if let Some(sd) = &m.source_device {
                if source_device_id != Some(sd.as_str()) {
                    continue;
                }
            }

            // Find a registered provider.
            let mut candidates: Vec<&str> = Vec::new();
            if let Some(p) = &route.action.provider {
                candidates.push(p);
            }
            if let Some(fps) = &route.action.failover_providers {
                candidates.extend(fps.iter().map(|s| s.as_str()));
            }

            for pid in candidates {
                let provider = match self.providers.iter().find(|p| p.id == pid) {
                    Some(p) => p,
                    None => continue,
                };
                if !is_provider_registered(pid) {
                    continue;
                }

                let mut num = dialed_number.to_string();
                if let Some(strip) = &route.action.strip_prefix {
                    if num.starts_with(strip.as_str()) {
                        num = num[strip.len()..].to_string();
                    }
                }
                if let Some(prepend) = &route.action.prepend_prefix {
                    num = format!("{prepend}{num}");
                }

                return Some(OutboundRouteResult {
                    provider: provider.clone(),
                    transformed_number: num,
                });
            }
        }

        // Fallback: first provider.
        self.providers.first().map(|p| OutboundRouteResult {
            provider: p.clone(),
            transformed_number: dialed_number.to_string(),
        })
    }

    /// Resolve which devices to ring for an inbound call.
    pub fn resolve_inbound_route(
        &self,
        provider_id: &str,
        called_number: &str,
        caller_number: &str,
    ) -> Option<InboundRouteResult> {
        let mut routes: Vec<&Route> = self
            .routing
            .routes
            .iter()
            .filter(|r| r.enabled && r.match_criteria.direction == "inbound")
            .collect();
        routes.sort_by(|a, b| b.priority.cmp(&a.priority));

        for route in &routes {
            let m = &route.match_criteria;

            if let Some(sp) = &m.source_provider {
                if sp != provider_id {
                    continue;
                }
            }
            if !matches_pattern(m.number_pattern.as_deref(), called_number) {
                continue;
            }
            if !matches_pattern(m.caller_pattern.as_deref(), caller_number) {
                continue;
            }

            let explicit_targets = route.action.targets.clone();
            return Some(InboundRouteResult {
                device_ids: explicit_targets.clone().unwrap_or_default(),
                ring_all_devices: explicit_targets.is_none(),
                ring_browsers: route.action.ring_browsers.unwrap_or(false),
                voicemail_box: route.action.voicemail_box.clone(),
                fax_box: route.action.fax_box.clone(),
                ivr_menu_id: route.action.ivr_menu_id.clone(),
                no_answer_timeout: route.action.no_answer_timeout,
            });
        }

        None
    }
}

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

    fn test_app_config(routes: Vec<Route>) -> AppConfig {
        AppConfig {
            proxy: ProxyConfig {
                lan_ip: "127.0.0.1".to_string(),
                lan_port: 5070,
                public_ip_seed: None,
                rtp_port_range: RtpPortRange {
                    min: 20_000,
                    max: 20_100,
                },
            },
            providers: vec![ProviderConfig {
                id: "provider-a".to_string(),
                display_name: "Provider A".to_string(),
                domain: "example.com".to_string(),
                outbound_proxy: Endpoint {
                    address: "example.com".to_string(),
                    port: 5060,
                },
                username: "user".to_string(),
                password: "pass".to_string(),
                register_interval_sec: 300,
                codecs: vec![9],
                quirks: Quirks {
                    early_media_silence: false,
                    silence_payload_type: None,
                    silence_max_packets: None,
                },
            }],
            devices: vec![DeviceConfig {
                id: "desk".to_string(),
                display_name: "Desk".to_string(),
                expected_address: "127.0.0.1".to_string(),
                extension: "100".to_string(),
            }],
            routing: RoutingConfig { routes },
            faxboxes: vec![],
            voiceboxes: vec![],
            ivr: None,
        }
    }

    #[test]
    fn normalize_routing_identity_extracts_uri_user_and_phone_number() {
        assert_eq!(
            normalize_routing_identity("sip:0049 421 219694@voip.easybell.de"),
            "+49421219694"
        );
        assert_eq!(
            normalize_routing_identity("<tel:+49 (421) 219694>"),
            "+49421219694"
        );
        assert_eq!(normalize_routing_identity("sip:100@pbx.local"), "100");
        assert_eq!(normalize_routing_identity("sip:alice@pbx.local"), "alice");
    }

    #[test]
    fn resolve_inbound_route_requires_explicit_match() {
        let cfg = test_app_config(vec![]);
        assert!(cfg
            .resolve_inbound_route("provider-a", "+49421219694", "+491701234567")
            .is_none());
    }

    #[test]
    fn resolve_inbound_route_matches_per_number_on_shared_provider() {
        let cfg = test_app_config(vec![
            Route {
                id: "main".to_string(),
                name: "Main DID".to_string(),
                priority: 200,
                enabled: true,
                match_criteria: RouteMatch {
                    direction: "inbound".to_string(),
                    number_pattern: Some("+49421219694".to_string()),
                    caller_pattern: None,
                    source_provider: Some("provider-a".to_string()),
                    source_device: None,
                },
                action: RouteAction {
                    targets: Some(vec!["desk".to_string()]),
                    ring_browsers: Some(true),
                    voicemail_box: None,
                    fax_box: None,
                    ivr_menu_id: None,
                    no_answer_timeout: None,
                    provider: None,
                    failover_providers: None,
                    strip_prefix: None,
                    prepend_prefix: None,
                },
            },
            Route {
                id: "support".to_string(),
                name: "Support DID".to_string(),
                priority: 100,
                enabled: true,
                match_criteria: RouteMatch {
                    direction: "inbound".to_string(),
                    number_pattern: Some("+49421219695".to_string()),
                    caller_pattern: None,
                    source_provider: Some("provider-a".to_string()),
                    source_device: None,
                },
                action: RouteAction {
                    targets: None,
                    ring_browsers: Some(false),
                    voicemail_box: Some("support-box".to_string()),
                    fax_box: None,
                    ivr_menu_id: None,
                    no_answer_timeout: Some(20),
                    provider: None,
                    failover_providers: None,
                    strip_prefix: None,
                    prepend_prefix: None,
                },
            },
        ]);

        let main = cfg
            .resolve_inbound_route("provider-a", "+49421219694", "+491701234567")
            .expect("main DID should match");
        assert_eq!(main.device_ids, vec!["desk".to_string()]);
        assert!(main.ring_browsers);

        let support = cfg
            .resolve_inbound_route("provider-a", "+49421219695", "+491701234567")
            .expect("support DID should match");
        assert_eq!(support.voicemail_box.as_deref(), Some("support-box"));
        assert_eq!(support.no_answer_timeout, Some(20));
        assert!(!support.ring_browsers);
    }

    #[test]
    fn extract_inbound_called_number_prefers_did_headers_over_username_ruri() {
        let raw = b"INVITE sip:2830573e1@proxy.example SIP/2.0\r\nTo: <sip:+4942116767548@proxy.example>\r\nFrom: <sip:+491701234567@provider.example>;tag=abc\r\nCall-ID: test-1\r\nCSeq: 1 INVITE\r\nContent-Length: 0\r\n\r\n";
        let msg = SipMessage::parse(raw).expect("invite should parse");
        assert_eq!(extract_inbound_called_number(&msg), "+4942116767548");
    }

    #[test]
    fn extract_inbound_called_number_keeps_phone_ruri_when_already_present() {
        let raw = b"INVITE sip:042116767548@proxy.example SIP/2.0\r\nTo: <sip:2830573e1@proxy.example>\r\nFrom: <sip:+491701234567@provider.example>;tag=abc\r\nCall-ID: test-2\r\nCSeq: 1 INVITE\r\nContent-Length: 0\r\n\r\n";
        let msg = SipMessage::parse(raw).expect("invite should parse");
        assert_eq!(extract_inbound_called_number(&msg), "042116767548");
    }

    #[test]
    fn matches_pattern_supports_numeric_ranges() {
        assert!(matches_pattern(
            Some("042116767546..042116767548"),
            "042116767547"
        ));
        assert!(!matches_pattern(
            Some("042116767546..042116767548"),
            "042116767549"
        ));
        assert!(matches_pattern(
            Some("+4942116767546..+4942116767548"),
            "+4942116767547"
        ));
        assert!(!matches_pattern(
            Some("+4942116767546..+4942116767548"),
            "042116767547"
        ));
    }
}
