//! Network interface binding utilities for socket creation
//!
//! Provides cross-platform interface validation and socket binding.
//! - Linux: Uses SO_BINDTODEVICE via socket2::bind_device()
//! - macOS/FreeBSD: Uses IP_BOUND_IF via socket2::bind_device_by_index()

use anyhow::{Result, anyhow};
use pnet::datalink;
use socket2::Socket;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};

/// Check if an IPv6 address is link-local (fe80::/10)
///
/// Link-local addresses have the first 10 bits set to 1111111010,
/// which means the first segment is in the range 0xfe80-0xfebf.
pub fn is_link_local_ipv6(addr: &Ipv6Addr) -> bool {
    let first_seg = addr.segments()[0];
    (0xfe80..=0xfebf).contains(&first_seg)
}

/// Validated interface information
#[derive(Debug, Clone)]
pub struct InterfaceInfo {
    /// Interface name (e.g., "eth0", "wlan0")
    pub name: String,
    /// Interface index (used for macOS binding)
    #[allow(dead_code)]
    pub index: u32,
    /// First IPv4 address on the interface (if any)
    pub ipv4: Option<Ipv4Addr>,
    /// First IPv6 address on the interface (if any)
    pub ipv6: Option<Ipv6Addr>,
    /// Default gateway IPv4 address (if detected)
    pub gateway_ipv4: Option<Ipv4Addr>,
    /// Default gateway IPv6 address (if detected)
    pub gateway_ipv6: Option<Ipv6Addr>,
}

// Gateway detection is only available on Linux and macOS.
// FreeBSD is excluded because getifs uses macOS-specific APIs (NET_RT_IFLIST2, rt_msghdr)
// that don't exist in FreeBSD's libc bindings.
#[cfg(any(target_os = "linux", target_os = "macos"))]
mod gateway_impl {
    use super::*;
    use std::sync::OnceLock;
    use std::time::Duration;

    /// Timeout for gateway detection (protects against large routing tables)
    const GATEWAY_TIMEOUT: Duration = Duration::from_millis(500);

    /// Cached gateway addresses (fetched once, reused for all targets)
    static GATEWAY_CACHE: OnceLock<Option<Vec<(u32, IpAddr, String)>>> = OnceLock::new();

    /// Fetch gateway addresses with timeout protection and caching
    ///
    /// On systems with very large routing tables (e.g., DFZ routers with millions of routes),
    /// gateway detection could be slow. This wrapper ensures we don't hang.
    /// Gateway info is nice-to-have for display, not critical for operation.
    ///
    /// Results are cached so multi-target runs only pay the cost once.
    fn fetch_gateways_with_timeout() -> Option<&'static Vec<(u32, IpAddr, String)>> {
        GATEWAY_CACHE
            .get_or_init(|| {
                let (tx, rx) = std::sync::mpsc::channel();

                std::thread::spawn(move || {
                    let result = getifs::gateway_addrs().ok().map(|gws| {
                        gws.into_iter()
                            .filter_map(|gw| {
                                let name = gw.name().ok()?;
                                Some((gw.index(), gw.addr(), name.to_string()))
                            })
                            .collect::<Vec<_>>()
                    });
                    // Ignore send error if receiver timed out
                    let _ = tx.send(result);
                });

                // Wait with timeout - if it takes too long, gateway info is unavailable
                rx.recv_timeout(GATEWAY_TIMEOUT).ok().flatten()
            })
            .as_ref()
    }

    /// Detect the default IPv4 gateway for an interface using kernel APIs
    ///
    /// Uses getifs which queries netlink (Linux) or sysctl (macOS) directly,
    /// avoiding subprocess calls that can hang on systems with large routing tables.
    pub fn detect_gateway_ipv4(interface: &str) -> Option<Ipv4Addr> {
        // Use cached gateway fetch
        fetch_gateways_with_timeout()?
            .iter()
            .find(|(_, _, name)| name == interface)
            .and_then(|(_, addr, _)| match addr {
                IpAddr::V4(v4) => Some(*v4),
                _ => None,
            })
    }

    /// Detect the default IPv6 gateway for an interface using kernel APIs
    pub fn detect_gateway_ipv6(interface: &str) -> Option<Ipv6Addr> {
        fetch_gateways_with_timeout()?
            .iter()
            .find(|(_, _, name)| name == interface)
            .and_then(|(_, addr, _)| match addr {
                IpAddr::V6(v6) => Some(*v6),
                _ => None,
            })
    }

    /// Detect default gateway without specifying an interface
    ///
    /// Useful when no --interface flag is provided but we still want to show
    /// which gateway will be used.
    pub fn detect_default_gateway(ipv6: bool) -> Option<IpAddr> {
        let gateways = fetch_gateways_with_timeout()?;

        if ipv6 {
            gateways
                .iter()
                .find(|(_, addr, _)| addr.is_ipv6())
                .map(|(_, addr, _)| *addr)
        } else {
            gateways
                .iter()
                .find(|(_, addr, _)| addr.is_ipv4())
                .map(|(_, addr, _)| *addr)
        }
    }
}

// Re-export gateway functions on supported platforms
#[allow(unused_imports)] // pub use re-export for external crates
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub use gateway_impl::detect_default_gateway;
#[cfg(any(target_os = "linux", target_os = "macos"))]
use gateway_impl::{detect_gateway_ipv4, detect_gateway_ipv6};

// Stub implementations for FreeBSD and other platforms
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
fn detect_gateway_ipv4(_interface: &str) -> Option<Ipv4Addr> {
    None
}

#[cfg(not(any(target_os = "linux", target_os = "macos")))]
fn detect_gateway_ipv6(_interface: &str) -> Option<Ipv6Addr> {
    None
}

/// Detect default gateway without specifying an interface
///
/// On FreeBSD and other unsupported platforms, this always returns None.
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
pub fn detect_default_gateway(_ipv6: bool) -> Option<IpAddr> {
    None
}

/// Validate that an interface exists and get its information
///
/// Returns error if:
/// - Interface does not exist
/// - Interface has no usable IP addresses (link-local only on non-loopback)
pub fn validate_interface(name: &str) -> Result<InterfaceInfo> {
    for iface in datalink::interfaces() {
        if iface.name == name {
            let mut ipv4 = None;
            let mut ipv6 = None;
            let is_loopback = iface.is_loopback();

            for addr in &iface.ips {
                match addr.ip() {
                    IpAddr::V4(v4) if ipv4.is_none() && !v4.is_loopback() => {
                        ipv4 = Some(v4);
                    }
                    // Skip link-local addresses for non-loopback interfaces
                    // (they require scope IDs and can't reach Internet targets)
                    IpAddr::V6(v6)
                        if ipv6.is_none() && !v6.is_loopback() && !is_link_local_ipv6(&v6) =>
                    {
                        ipv6 = Some(v6);
                    }
                    _ => {}
                }
            }

            // For loopback interface only, allow loopback and link-local addresses
            if is_loopback && ipv4.is_none() && ipv6.is_none() {
                for addr in &iface.ips {
                    match addr.ip() {
                        IpAddr::V4(v4) if ipv4.is_none() => ipv4 = Some(v4),
                        IpAddr::V6(v6) if ipv6.is_none() => ipv6 = Some(v6),
                        _ => {}
                    }
                }
            }

            // Reject non-loopback interfaces with only link-local addresses
            if !is_loopback && ipv4.is_none() && ipv6.is_none() {
                // Check if there are any addresses at all (vs link-local only)
                let has_any_addr = iface.ips.iter().any(|a| match a.ip() {
                    IpAddr::V4(_) => true,
                    IpAddr::V6(v6) => !v6.is_loopback(),
                });
                if has_any_addr {
                    return Err(anyhow!(
                        "Interface '{}' has only link-local IPv6 addresses. \
                         Link-local addresses cannot reach Internet targets. \
                         Assign a global IPv4 or IPv6 address to this interface.",
                        name
                    ));
                }
            }

            // Detect gateway addresses for this interface
            let gateway_ipv4 = detect_gateway_ipv4(name);
            let gateway_ipv6 = detect_gateway_ipv6(name);

            return Ok(InterfaceInfo {
                name: name.to_string(),
                index: iface.index,
                ipv4,
                ipv6,
                gateway_ipv4,
                gateway_ipv6,
            });
        }
    }

    // Interface not found - list available interfaces
    let available: Vec<_> = datalink::interfaces()
        .iter()
        .filter(|i| !i.ips.is_empty())
        .map(|i| i.name.clone())
        .collect();

    Err(anyhow!(
        "Interface '{}' not found. Available interfaces: {}",
        name,
        if available.is_empty() {
            "(none with IP addresses)".to_string()
        } else {
            available.join(", ")
        }
    ))
}

/// Bind a socket to a specific network interface
///
/// On Linux, uses SO_BINDTODEVICE which requires CAP_NET_RAW or root.
/// On macOS, uses IP_BOUND_IF (IPv4) or IPV6_BOUND_IF (IPv6) with the interface index.
/// FreeBSD does not support interface binding (no SO_BINDTODEVICE or IP_BOUND_IF).
pub fn bind_socket_to_interface(socket: &Socket, info: &InterfaceInfo, ipv6: bool) -> Result<()> {
    #[cfg(target_os = "linux")]
    {
        let _ = ipv6; // SO_BINDTODEVICE works for both IPv4 and IPv6
        socket.bind_device(Some(info.name.as_bytes())).map_err(|e| {
            anyhow!(
                "Failed to bind socket to interface '{}': {}. \
                 This requires CAP_NET_RAW capability or root privileges.",
                info.name,
                e
            )
        })
    }

    #[cfg(target_os = "macos")]
    {
        use std::num::NonZeroU32;
        // macOS uses interface index binding
        // IPv4: IP_BOUND_IF, IPv6: IPV6_BOUND_IF
        let idx = NonZeroU32::new(info.index);
        let result = if ipv6 {
            socket.bind_device_by_index_v6(idx)
        } else {
            socket.bind_device_by_index_v4(idx)
        };
        result.map_err(|e| {
            anyhow!(
                "Failed to bind socket to interface '{}' (index {}): {}",
                info.name,
                info.index,
                e
            )
        })
    }

    #[cfg(any(target_os = "freebsd", target_os = "netbsd"))]
    {
        let _ = (socket, info, ipv6); // Suppress unused warnings
        Err(anyhow!(
            "Interface binding (-i) is not supported on FreeBSD/NetBSD. \
             These platforms lack SO_BINDTODEVICE and IP_BOUND_IF socket options."
        ))
    }

    #[cfg(not(any(
        target_os = "linux",
        target_os = "macos",
        target_os = "freebsd",
        target_os = "netbsd"
    )))]
    {
        let _ = (socket, info, ipv6); // Suppress unused warnings
        Err(anyhow!(
            "Interface binding is not supported on this platform. \
             It is only available on Linux and macOS."
        ))
    }
}

/// Get the source IP address from an interface for a given IP family
///
/// Returns the first address of the requested family, or an error if none exists.
#[allow(dead_code)]
pub fn get_interface_source_ip(info: &InterfaceInfo, ipv6: bool) -> Result<IpAddr> {
    if ipv6 {
        info.ipv6.map(IpAddr::V6).ok_or_else(|| {
            anyhow!(
                "Interface '{}' has no IPv6 address. Use -4 to force IPv4.",
                info.name
            )
        })
    } else {
        info.ipv4.map(IpAddr::V4).ok_or_else(|| {
            anyhow!(
                "Interface '{}' has no IPv4 address. Use -6 to force IPv6.",
                info.name
            )
        })
    }
}

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

    #[test]
    fn test_nonexistent_interface() {
        let result = validate_interface("nonexistent_interface_12345");
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("not found"));
    }

    #[test]
    #[cfg(target_os = "linux")]
    fn test_loopback_interface() {
        let interfaces = datalink::interfaces();
        let loopback_name = match interfaces.iter().find(|iface| iface.is_loopback()) {
            Some(iface) => iface.name.clone(),
            None => {
                eprintln!("Skipping loopback interface test: no loopback interface visible.");
                return;
            }
        };

        let result = validate_interface(&loopback_name);
        assert!(result.is_ok());

        let info = result.unwrap();
        assert_eq!(info.name, loopback_name);
        assert!(info.ipv4.is_some() || info.ipv6.is_some());
    }

    #[test]
    fn test_ipv6_link_local_detection() {
        // Test the shared is_link_local_ipv6 function
        // Link-local addresses (fe80::/10) should be detected
        let link_local: Ipv6Addr = "fe80::1".parse().unwrap();
        assert!(is_link_local_ipv6(&link_local));

        let link_local_2: Ipv6Addr = "fe80::dead:beef:cafe:1234".parse().unwrap();
        assert!(is_link_local_ipv6(&link_local_2));

        // Edge of link-local range (febf::)
        let link_local_edge: Ipv6Addr = "febf::1".parse().unwrap();
        assert!(is_link_local_ipv6(&link_local_edge));

        // Global unicast (2000::/3) should NOT be link-local
        let global: Ipv6Addr = "2001:db8::1".parse().unwrap();
        assert!(!is_link_local_ipv6(&global));

        let google_dns: Ipv6Addr = "2001:4860:4860::8888".parse().unwrap();
        assert!(!is_link_local_ipv6(&google_dns));

        // Unique local (fc00::/7) should NOT be link-local
        let ula: Ipv6Addr = "fd00::1".parse().unwrap();
        assert!(!is_link_local_ipv6(&ula));

        // Loopback should NOT be link-local
        let loopback: Ipv6Addr = "::1".parse().unwrap();
        assert!(!is_link_local_ipv6(&loopback));

        // Just outside link-local range (fe7f and fec0)
        let below_range: Ipv6Addr = "fe7f::1".parse().unwrap();
        assert!(!is_link_local_ipv6(&below_range));

        let above_range: Ipv6Addr = "fec0::1".parse().unwrap();
        assert!(!is_link_local_ipv6(&above_range));
    }
}
