//! Domain validation and SSRF protection utilities

use std::collections::HashSet;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};

use once_cell::sync::Lazy;

use crate::error::{Result, SeerError};

/// TLD allowlist loaded from the `SEER_DOMAIN_ALLOWLIST` environment variable.
/// When set (e.g., `SEER_DOMAIN_ALLOWLIST="com,org,net"`), only domains with
/// matching TLDs are permitted. When unset, all TLDs are allowed.
static DOMAIN_ALLOWLIST: Lazy<Option<HashSet<String>>> = Lazy::new(|| {
    let set: HashSet<String> = std::env::var("SEER_DOMAIN_ALLOWLIST")
        .ok()?
        .split(',')
        .map(|s| s.trim().to_lowercase())
        .filter(|s| !s.is_empty())
        .collect();

    if set.is_empty() {
        None
    } else {
        Some(set)
    }
});

/// Normalizes and validates a domain name.
///
/// This function:
/// - Removes http:// and https:// prefixes
/// - Removes www. prefix
/// - Removes trailing slashes and paths
/// - Converts to lowercase
/// - Converts internationalized domain names (IDN) to Punycode (ASCII)
/// - Validates format (must contain dots, only alphanumeric/hyphens/dots)
/// - Does NOT perform SSRF checks (use `validate_domain_safe` for network operations)
pub fn normalize_domain(domain: &str) -> Result<String> {
    let domain = domain.trim().to_lowercase();

    // Remove protocol
    let domain = domain
        .strip_prefix("http://")
        .or_else(|| domain.strip_prefix("https://"))
        .unwrap_or(&domain);

    // Remove trailing slash, path, query parameters, and fragments
    let domain = domain.split('/').next().unwrap_or(domain);
    let domain = domain.split('?').next().unwrap_or(domain);
    let domain = domain.split('#').next().unwrap_or(domain);

    // Remove www. prefix
    let domain = domain.strip_prefix("www.").unwrap_or(domain);

    // Validate domain format
    if domain.is_empty() || !domain.contains('.') {
        return Err(SeerError::InvalidDomain(domain.to_string()));
    }

    // Convert internationalized domain names (IDN) to ASCII/Punycode
    let domain = if !domain.is_ascii() {
        domain_to_ascii(domain)?
    } else {
        domain.to_string()
    };

    // Basic validation - alphanumeric, hyphens, dots, and underscores
    // Underscores are valid in DNS names (RFC 8552) and required for service
    // records like _dmarc., _domainkey., _sip._tcp., etc.
    let valid = domain
        .chars()
        .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_');
    if !valid {
        return Err(SeerError::InvalidDomain(domain.to_string()));
    }

    // Check for consecutive dots or dots at start/end
    if domain.contains("..") || domain.starts_with('.') || domain.ends_with('.') {
        return Err(SeerError::InvalidDomain(domain.to_string()));
    }

    // RFC 1035: total domain name length ≤ 253 characters
    if domain.len() > 253 {
        return Err(SeerError::InvalidDomain(domain.to_string()));
    }

    // Check label constraints
    for label in domain.split('.') {
        // Labels must be non-empty and not start/end with hyphens
        if label.is_empty() || label.starts_with('-') || label.ends_with('-') {
            return Err(SeerError::InvalidDomain(domain.to_string()));
        }
        // RFC 1035: each label ≤ 63 characters
        if label.len() > 63 {
            return Err(SeerError::InvalidDomain(domain.to_string()));
        }
    }

    // Check TLD against allowlist (if configured)
    if let Some(ref allowlist) = *DOMAIN_ALLOWLIST {
        if let Some(tld) = domain.rsplit('.').next() {
            if !allowlist.contains(tld) {
                return Err(SeerError::DomainNotAllowed {
                    domain: domain.to_string(),
                    tld: tld.to_string(),
                });
            }
        }
    }

    Ok(domain.to_string())
}

/// Converts an internationalized domain name to ASCII (Punycode).
fn domain_to_ascii(domain: &str) -> Result<String> {
    idna::domain_to_ascii(domain).map_err(|_| {
        SeerError::InvalidDomain(format!("invalid internationalized domain: {}", domain))
    })
}

/// Checks if an IP address is in a private or reserved range.
///
/// This includes:
/// - Private networks (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
/// - Loopback (127.0.0.0/8, ::1/128)
/// - Link-local (169.254.0.0/16, fe80::/10)
/// - Cloud metadata (169.254.169.254)
/// - Unique local addresses (fc00::/7)
/// - Documentation ranges (192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24)
/// - Multicast and broadcast
pub fn is_private_or_reserved_ip(ip: &IpAddr) -> bool {
    match ip {
        IpAddr::V4(ipv4) => is_private_or_reserved_ipv4(ipv4),
        IpAddr::V6(ipv6) => is_private_or_reserved_ipv6(ipv6),
    }
}

/// Checks if an IPv4 address is private or reserved.
fn is_private_or_reserved_ipv4(ip: &Ipv4Addr) -> bool {
    // Standard private/loopback/link-local checks
    if ip.is_private() || ip.is_loopback() || ip.is_link_local() {
        return true;
    }

    let octets = ip.octets();

    // Cloud metadata service (169.254.169.254)
    if octets[0] == 169 && octets[1] == 254 && octets[2] == 169 && octets[3] == 254 {
        return true;
    }

    // Broader link-local range (169.254.0.0/16) - already covered by is_link_local()
    // But explicitly check cloud metadata range
    if octets[0] == 169 && octets[1] == 254 {
        return true;
    }

    // Documentation ranges
    // 192.0.2.0/24 (TEST-NET-1)
    if octets[0] == 192 && octets[1] == 0 && octets[2] == 2 {
        return true;
    }
    // 198.51.100.0/24 (TEST-NET-2)
    if octets[0] == 198 && octets[1] == 51 && octets[2] == 100 {
        return true;
    }
    // 203.0.113.0/24 (TEST-NET-3)
    if octets[0] == 203 && octets[1] == 0 && octets[2] == 113 {
        return true;
    }

    // Broadcast
    if ip.is_broadcast() {
        return true;
    }

    // Unspecified (0.0.0.0)
    if ip.is_unspecified() {
        return true;
    }

    // Multicast (224.0.0.0/4)
    if octets[0] >= 224 && octets[0] <= 239 {
        return true;
    }

    // Reserved (240.0.0.0/4)
    if octets[0] >= 240 {
        return true;
    }

    false
}

/// Checks if an IPv6 address is private or reserved.
fn is_private_or_reserved_ipv6(ip: &Ipv6Addr) -> bool {
    // Loopback (::1)
    if ip.is_loopback() {
        return true;
    }

    // Unspecified (::)
    if ip.is_unspecified() {
        return true;
    }

    let segments = ip.segments();

    // Unique local addresses (fc00::/7)
    if (segments[0] & 0xfe00) == 0xfc00 {
        return true;
    }

    // Link-local (fe80::/10)
    if (segments[0] & 0xffc0) == 0xfe80 {
        return true;
    }

    // Multicast (ff00::/8)
    if segments[0] >> 8 == 0xff {
        return true;
    }

    // IPv4-mapped IPv6 addresses (::ffff:0:0/96)
    // Check if it maps to a private IPv4
    if ip
        .to_ipv4_mapped()
        .is_some_and(|ipv4| is_private_or_reserved_ipv4(&ipv4))
    {
        return true;
    }

    false
}

/// Returns a human-readable reason why an IP is blocked, or `None` if it is
/// safe.  Intended for error messages — callers should still use
/// [`is_private_or_reserved_ip`] for the fast boolean check.
pub fn describe_reserved_ip(ip: &IpAddr) -> Option<&'static str> {
    match ip {
        IpAddr::V4(v4) => {
            if v4.is_unspecified() {
                return Some("unspecified address (0.0.0.0) — domain has no routable IP");
            }
            if v4.is_loopback() {
                return Some("loopback address (127.0.0.0/8)");
            }
            if v4.is_private() {
                return Some("private network (RFC 1918)");
            }
            if v4.is_link_local() {
                return Some("link-local address (169.254.0.0/16)");
            }
            let o = v4.octets();
            if o[0] == 169 && o[1] == 254 && o[2] == 169 && o[3] == 254 {
                return Some("cloud metadata endpoint (169.254.169.254)");
            }
            if o[0] == 169 && o[1] == 254 {
                return Some("link-local address (169.254.0.0/16)");
            }
            if (o[0] == 192 && o[1] == 0 && o[2] == 2)
                || (o[0] == 198 && o[1] == 51 && o[2] == 100)
                || (o[0] == 203 && o[1] == 0 && o[2] == 113)
            {
                return Some("documentation/test range (RFC 5737)");
            }
            if v4.is_broadcast() {
                return Some("broadcast address (255.255.255.255)");
            }
            if o[0] >= 224 && o[0] <= 239 {
                return Some("multicast address (224.0.0.0/4)");
            }
            if o[0] >= 240 {
                return Some("reserved address (240.0.0.0/4)");
            }
            None
        }
        IpAddr::V6(v6) => {
            if v6.is_loopback() {
                return Some("IPv6 loopback (::1)");
            }
            if v6.is_unspecified() {
                return Some("IPv6 unspecified address (::) — domain has no routable IP");
            }
            let seg = v6.segments();
            if (seg[0] & 0xfe00) == 0xfc00 {
                return Some("IPv6 unique local address (fc00::/7)");
            }
            if (seg[0] & 0xffc0) == 0xfe80 {
                return Some("IPv6 link-local address (fe80::/10)");
            }
            if seg[0] >> 8 == 0xff {
                return Some("IPv6 multicast (ff00::/8)");
            }
            if let Some(v4) = v6.to_ipv4_mapped() {
                if is_private_or_reserved_ipv4(&v4) {
                    return Some("IPv4-mapped IPv6 address in private/reserved range");
                }
            }
            None
        }
    }
}

/// Validates that a domain is safe to query (SSRF protection).
///
/// This function:
/// 1. Normalizes the domain
/// 2. Resolves it to IP addresses
/// 3. Checks that none of the IPs are in private/reserved ranges
///
/// Use this before making HTTP/TLS connections to user-supplied domains.
pub async fn validate_domain_safe(domain: &str) -> Result<String> {
    // First normalize the domain
    let normalized = normalize_domain(domain)?;

    // Resolve the domain to IP addresses
    let addr = format!("{}:443", normalized);
    let socket_addrs = tokio::net::lookup_host(&addr)
        .await
        .map_err(|e| SeerError::InvalidDomain(format!("failed to resolve domain: {}", e)))?;

    // Check all resolved IPs
    for socket_addr in socket_addrs {
        let ip = socket_addr.ip();
        if let Some(reason) = describe_reserved_ip(&ip) {
            return Err(SeerError::InvalidDomain(format!(
                "cannot connect to '{}': {} — {}",
                normalized, ip, reason
            )));
        }
    }

    Ok(normalized)
}

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

    #[test]
    fn test_normalize_domain() {
        assert_eq!(normalize_domain("example.com").unwrap(), "example.com");
        assert_eq!(normalize_domain("EXAMPLE.COM").unwrap(), "example.com");
        assert_eq!(
            normalize_domain("https://www.example.com/path").unwrap(),
            "example.com"
        );
        assert_eq!(
            normalize_domain("http://example.com/").unwrap(),
            "example.com"
        );
        assert_eq!(
            normalize_domain("  WWW.EXAMPLE.COM  ").unwrap(),
            "example.com"
        );

        // Query parameters and fragments
        assert_eq!(
            normalize_domain("example.com?query=1").unwrap(),
            "example.com"
        );
        assert_eq!(
            normalize_domain("example.com#section").unwrap(),
            "example.com"
        );
        assert_eq!(
            normalize_domain("https://example.com/path?q=1#frag").unwrap(),
            "example.com"
        );

        // Underscore domains (DNS service records)
        assert_eq!(
            normalize_domain("_dmarc.example.com").unwrap(),
            "_dmarc.example.com"
        );
        assert_eq!(
            normalize_domain("selector1._domainkey.example.com").unwrap(),
            "selector1._domainkey.example.com"
        );
        assert_eq!(
            normalize_domain("_sip._tcp.example.com").unwrap(),
            "_sip._tcp.example.com"
        );

        // Invalid domains
        assert!(normalize_domain("").is_err());
        assert!(normalize_domain("nodots").is_err());
        assert!(normalize_domain("example..com").is_err());
        assert!(normalize_domain(".example.com").is_err());
        assert!(normalize_domain("example.com.").is_err());
        assert!(normalize_domain("-example.com").is_err());
        assert!(normalize_domain("example-.com").is_err());
    }

    #[test]
    fn test_normalize_idn_domain() {
        // German: münchen.de -> xn--mnchen-3ya.de
        let result = normalize_domain("münchen.de").unwrap();
        assert_eq!(result, "xn--mnchen-3ya.de");

        // Japanese: 例え.jp -> xn--r8jz45g.jp
        let result = normalize_domain("例え.jp").unwrap();
        assert_eq!(result, "xn--r8jz45g.jp");

        // Chinese: 中文.com -> xn--fiq228c.com
        let result = normalize_domain("中文.com").unwrap();
        assert_eq!(result, "xn--fiq228c.com");

        // With protocol prefix
        let result = normalize_domain("https://münchen.de/path").unwrap();
        assert_eq!(result, "xn--mnchen-3ya.de");
    }

    #[test]
    fn test_allowlist_not_set_allows_all() {
        // When SEER_DOMAIN_ALLOWLIST is not set, all domains pass
        // This test verifies the default behavior (no env var)
        assert!(normalize_domain("example.com").is_ok());
        assert!(normalize_domain("example.xyz").is_ok());
        assert!(normalize_domain("example.co.uk").is_ok());
    }

    #[test]
    fn test_is_private_or_reserved_ipv4() {
        // Private networks
        assert!(is_private_or_reserved_ip(&IpAddr::V4(Ipv4Addr::new(
            10, 0, 0, 1
        ))));
        assert!(is_private_or_reserved_ip(&IpAddr::V4(Ipv4Addr::new(
            172, 16, 0, 1
        ))));
        assert!(is_private_or_reserved_ip(&IpAddr::V4(Ipv4Addr::new(
            192, 168, 1, 1
        ))));

        // Loopback
        assert!(is_private_or_reserved_ip(&IpAddr::V4(Ipv4Addr::new(
            127, 0, 0, 1
        ))));

        // Link-local
        assert!(is_private_or_reserved_ip(&IpAddr::V4(Ipv4Addr::new(
            169, 254, 1, 1
        ))));

        // Cloud metadata
        assert!(is_private_or_reserved_ip(&IpAddr::V4(Ipv4Addr::new(
            169, 254, 169, 254
        ))));

        // Public IP (should not be blocked)
        assert!(!is_private_or_reserved_ip(&IpAddr::V4(Ipv4Addr::new(
            8, 8, 8, 8
        ))));
        assert!(!is_private_or_reserved_ip(&IpAddr::V4(Ipv4Addr::new(
            1, 1, 1, 1
        ))));
    }

    #[test]
    fn test_is_private_or_reserved_ipv6() {
        // Loopback
        assert!(is_private_or_reserved_ip(&IpAddr::V6(Ipv6Addr::new(
            0, 0, 0, 0, 0, 0, 0, 1
        ))));

        // Unique local
        assert!(is_private_or_reserved_ip(&IpAddr::V6(Ipv6Addr::new(
            0xfc00, 0, 0, 0, 0, 0, 0, 1
        ))));

        // Link-local
        assert!(is_private_or_reserved_ip(&IpAddr::V6(Ipv6Addr::new(
            0xfe80, 0, 0, 0, 0, 0, 0, 1
        ))));

        // Public IPv6 (should not be blocked)
        assert!(!is_private_or_reserved_ip(&IpAddr::V6(Ipv6Addr::new(
            0x2001, 0x4860, 0x4860, 0, 0, 0, 0, 0x8888
        ))));
    }
}
