//! SSRF target guard: classify internal / non-public IP ranges and refuse to
//! connect to a target that resolves into one.
//!
//! Split out of `execute.rs` to keep that module under the Santh modularity
//! limit. These two functions form a self-contained unit: `is_internal_ip`
//! decides whether an address is non-public, and `ensure_external_target`
//! resolves a URL's host and rejects the request if *any* resolved address is
//! internal. Keeping them together makes the SSRF policy reviewable in one
//! place.

use secir::{Error, Result};
use std::collections::HashSet;
use std::net::IpAddr;
use url::Url;

/// Returns `true` if `ip` falls in a loopback, private, link-local, CGNAT,
/// documentation, benchmark, ULA, multicast, or IPv4-mapped-internal range,
/// i.e. any address an external scan target must not resolve to.
pub fn is_internal_ip(ip: IpAddr) -> bool {
    match ip {
        IpAddr::V4(ip) => {
            ip.is_loopback()                                                    // 127.0.0.0/8
                || ip.octets()[0] == 0                                          // 0.0.0.0/8
                || ip.octets()[0] == 10                                         // 10.0.0.0/8
                || (ip.octets()[0] == 100 && (64..=127).contains(&ip.octets()[1])) // 100.64.0.0/10 (CGN)
                || (ip.octets()[0] == 169 && ip.octets()[1] == 254)             // 169.254.0.0/16 (link-local, cloud metadata)
                || (ip.octets()[0] == 172 && (16..=31).contains(&ip.octets()[1])) // 172.16.0.0/12
                || (ip.octets()[0] == 192 && ip.octets()[1] == 168)             // 192.168.0.0/16
                || (ip.octets()[0] == 198 && matches!(ip.octets()[1], 18 | 19)) // 198.18.0.0/15 (benchmark)
        }
        IpAddr::V6(ip) => {
            ip.is_loopback()                                           // ::1
                || ip.is_unspecified()                                 // ::
                || ((ip.segments()[0] & 0xfe00) == 0xfc00)             // fc00::/7 (ULA)
                || ((ip.segments()[0] & 0xffc0) == 0xfe80)             // fe80::/10 (link-local)
                || (ip.segments()[0] == 0 && ip.segments()[1] == 0
                    && ip.segments()[2] == 0 && ip.segments()[3] == 0
                    && ip.segments()[4] == 0 && ip.segments()[5] == 0xffff) // ::ffff:0:0/96 (IPv4-mapped)
                || (ip.segments()[0] == 0x0064 && ip.segments()[1] == 0xff9b) // 64:ff9b::/96 (NAT64)
                || (ip.segments()[0] == 0x2001 && ip.segments()[1] == 0x0db8) // 2001:db8::/32 (documentation)
                || ((ip.segments()[0] & 0xff00) == 0xff00)             // ff00::/8 (multicast)
                || {
                    // IPv4-mapped: check if the embedded IPv4 is internal
                    if ip.segments()[5] == 0xffff && ip.segments()[..5] == [0, 0, 0, 0, 0] {
                        let v4 = std::net::Ipv4Addr::new(
                            (ip.segments()[6] >> 8) as u8,
                            ip.segments()[6] as u8,
                            (ip.segments()[7] >> 8) as u8,
                            ip.segments()[7] as u8,
                        );
                        is_internal_ip(IpAddr::V4(v4))
                    } else {
                        false
                    }
                }
        }
    }
}

/// Resolve `url`'s host and reject the request if any resolved address is
/// internal (SSRF guard). Returns the original transport error on a malformed
/// URL or a resolution failure.
pub(crate) async fn ensure_external_target(url: &str) -> Result<()> {
    let parsed = Url::parse(url).map_err(|e| Error::Transport {
        url: url.to_string(),
        message: format!("invalid URL: {e}"),
        source: Some(Box::new(e)),
    })?;
    let host = parsed.host_str().ok_or_else(|| Error::Transport {
        url: url.to_string(),
        message: "URL missing host".to_string(),
        source: None,
    })?;
    let port = parsed
        .port_or_known_default()
        .ok_or_else(|| Error::Transport {
            url: url.to_string(),
            message: "URL missing port".to_string(),
            source: None,
        })?;

    let resolved_ips = if let Ok(ip) = host.parse::<IpAddr>() {
        vec![ip]
    } else {
        tokio::net::lookup_host((host, port))
            .await
            .map_err(|e| Error::Transport {
                url: url.to_string(),
                message: format!("failed to resolve host: {e}"),
                source: Some(Box::new(e)),
            })?
            .map(|addr| addr.ip())
            .collect::<HashSet<_>>()
            .into_iter()
            .collect::<Vec<_>>()
    };

    if resolved_ips.iter().copied().any(is_internal_ip) {
        let rendered_ips = resolved_ips
            .iter()
            .map(ToString::to_string)
            .collect::<Vec<_>>()
            .join(", ");
        let message = format!("blocked internal address resolution for {host}: {rendered_ips}");
        tracing::warn!(
            url = url,
            host = host,
            resolved_ips = rendered_ips,
            "{message}"
        );
        return Err(Error::Transport {
            url: url.to_string(),
            message,
            source: None,
        });
    }

    Ok(())
}
