//! Host discovery before port scan. Skipped with `-Pn` / `--no-ping`.
//!
//! Default probes: **ICMP echo** (parallel with TCP) plus **TCP 443 and 80** using **raw SYN** when
//! privileged ([`crate::syn::parallel_syn_scan_ipv4`] / `ipv6`), otherwise TCP connect fallback.
//!
//! **`-PS`**: raw half-open SYN; **`-PA`**: raw TCP ACK (same `syn` stack as `-PS`); both fall back to
//! TCP connect if raw sockets fail.
//!
//! **UDP `-PU`**: UDP datagram; reply or ICMP-unreachable-style socket errors → “up”.
//!
//! **`-PY`**: SCTP **INIT** (same engines as `-sY`); INIT-ACK / ABORT ⇒ host responded (up).
//!
//! **`-PO`**: IP protocol probes (same engines as `-sO`); ICMP **protocol unreachable** or IPv6
//! Parameter Problem ⇒ “closed”; any **open**/**closed** result ⇒ host up.
//!
//! **`-PP`** / **`-PM`**: legacy ICMP **timestamp** and **address mask** (IPv4 only; Unix raw ICMP).

use std::collections::HashSet;
use std::io::ErrorKind as IoErrorKind;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use std::time::Duration;

use anyhow::{anyhow, bail, Result};
use futures::stream::{self, StreamExt};
use tokio::io::ErrorKind as TokioErrorKind;
use tokio::net::{TcpStream, UdpSocket};

use crate::cli::Args;
use crate::ip_proto::MAX_IP_PROTO_PARALLEL_SHARDS;
use crate::ping::ping_hosts;
use crate::ports::parse_port_spec;
use crate::scan::PortLine;
use crate::sctp::MAX_SCTP_PARALLEL_SHARDS;
use crate::syn::MAX_SYN_PARALLEL_SHARDS;

/// TCP ports for default discovery (Nmap: SYN 443, ACK 80).
const DEFAULT_TCP_DISCOVERY_PORTS: &[u16] = &[443, 80];

const TCP_SYN_DEFAULT: u16 = 80;
const TCP_ACK_DEFAULT: u16 = 80;
const UDP_PING_DEFAULT: u16 = 40_125;
/// Nmap default SCTP ping port (`-PY` with no port list).
const SCTP_PING_DEFAULT: u16 = 80;

fn has_explicit_discovery_flags(args: &Args) -> bool {
    args.ping_echo
        || args.ping_syn.is_some()
        || args.ping_ack.is_some()
        || args.ping_udp.is_some()
        || args.ping_sctp.is_some()
        || args.ping_timestamp
        || args.ping_mask
        || args.ping_ip_proto.is_some()
}

fn has_implemented_explicit_probes(args: &Args) -> bool {
    args.ping_echo
        || args.ping_syn.is_some()
        || args.ping_ack.is_some()
        || args.ping_udp.is_some()
        || args.ping_sctp.is_some()
        || args.ping_ip_proto.is_some()
        || args.ping_timestamp
        || args.ping_mask
}

fn ports_from_ping_tcp(opt: &Option<Option<String>>, default: u16) -> Result<Vec<u16>> {
    match opt {
        None => Ok(vec![]),
        Some(None) => Ok(vec![default]),
        Some(Some(s)) => parse_port_spec(s.trim()).map_err(|e| anyhow!("{e}")),
    }
}

fn ports_from_ping_udp(opt: &Option<Option<String>>) -> Result<Vec<u16>> {
    match opt {
        None => Ok(vec![]),
        Some(None) => Ok(vec![UDP_PING_DEFAULT]),
        Some(Some(s)) => parse_port_spec(s.trim()).map_err(|e| anyhow!("{e}")),
    }
}

fn ports_from_ping_sctp(opt: &Option<Option<String>>) -> Result<Vec<u16>> {
    match opt {
        None => Ok(vec![]),
        Some(None) => Ok(vec![SCTP_PING_DEFAULT]),
        Some(Some(s)) => parse_port_spec(s.trim()).map_err(|e| anyhow!("{e}")),
    }
}

/// Parse `-PO` / `--ping-ip-proto` protocol list (comma-separated 0–255). Empty or omitted list uses
/// Nmap’s default **1,2,4** (ICMP, IGMP, IP-in-IP).
fn parse_ping_ip_proto_list(s: &str) -> Result<Vec<u16>> {
    let s = s.trim();
    if s.is_empty() {
        return Ok(vec![1, 2, 4]);
    }
    let mut out = Vec::new();
    for part in s.split(',') {
        let p = part.trim();
        if p.is_empty() {
            continue;
        }
        let n: u16 = p
            .parse()
            .map_err(|_| anyhow!("invalid protocol number in -PO: {p}"))?;
        if n > 255 {
            bail!("protocol number in -PO must be 0..=255, got {n}");
        }
        out.push(n);
    }
    if out.is_empty() {
        bail!("-PO protocol list is empty");
    }
    out.sort_unstable();
    out.dedup();
    Ok(out)
}

fn port_lines_to_alive_hosts(lines: Vec<PortLine>) -> HashSet<IpAddr> {
    lines
        .into_iter()
        .filter(|l| l.state == "open" || l.state == "closed")
        .map(|l| l.host)
        .collect()
}

/// Raw SYN discovery: RST or SYN-ACK ⇒ host responded (up).
async fn syn_raw_discovery_collect(
    hosts: &[IpAddr],
    ports: &[u16],
    skip: Option<&HashSet<IpAddr>>,
    timeout: Duration,
    max_shards: usize,
) -> std::io::Result<HashSet<IpAddr>> {
    let mut v4: Vec<(Ipv4Addr, u16)> = Vec::new();
    let mut v6: Vec<(Ipv6Addr, u16)> = Vec::new();
    for &h in hosts {
        if skip.map(|s| s.contains(&h)).unwrap_or(false) {
            continue;
        }
        for &p in ports {
            match h {
                IpAddr::V4(a) => v4.push((a, p)),
                IpAddr::V6(a) => v6.push((a, p)),
            }
        }
    }

    let v4_fut = async {
        if v4.is_empty() {
            return Ok::<HashSet<IpAddr>, std::io::Error>(HashSet::new());
        }
        tokio::task::spawn_blocking(move || {
            let lines = crate::syn::parallel_syn_scan_ipv4(
                v4, timeout, None, None, None, None, None, 0, max_shards,
            )?;
            Ok(port_lines_to_alive_hosts(lines))
        })
        .await
        .map_err(|e| std::io::Error::other(format!("{e}")))?
    };

    let v6_fut = async {
        if v6.is_empty() {
            return Ok::<HashSet<IpAddr>, std::io::Error>(HashSet::new());
        }
        tokio::task::spawn_blocking(move || {
            let lines = crate::syn::parallel_syn_scan_ipv6(
                v6, timeout, None, None, None, None, None, 0, max_shards,
            )?;
            Ok(port_lines_to_alive_hosts(lines))
        })
        .await
        .map_err(|e| std::io::Error::other(format!("{e}")))?
    };

    let (r4, r6) = tokio::join!(v4_fut, v6_fut);
    let mut out = r4?;
    out.extend(r6?);
    Ok(out)
}

/// Raw ACK discovery (`-PA`): RST or SYN-ACK ⇒ host responded.
async fn ack_raw_discovery_collect(
    hosts: &[IpAddr],
    ports: &[u16],
    skip: Option<&HashSet<IpAddr>>,
    timeout: Duration,
    max_shards: usize,
) -> std::io::Result<HashSet<IpAddr>> {
    let mut v4: Vec<(Ipv4Addr, u16)> = Vec::new();
    let mut v6: Vec<(Ipv6Addr, u16)> = Vec::new();
    for &h in hosts {
        if skip.map(|s| s.contains(&h)).unwrap_or(false) {
            continue;
        }
        for &p in ports {
            match h {
                IpAddr::V4(a) => v4.push((a, p)),
                IpAddr::V6(a) => v6.push((a, p)),
            }
        }
    }

    let v4_fut = async {
        if v4.is_empty() {
            return Ok::<HashSet<IpAddr>, std::io::Error>(HashSet::new());
        }
        tokio::task::spawn_blocking(move || {
            let lines = crate::syn::parallel_ack_ping_scan_ipv4(
                v4, timeout, None, None, None, None, None, 0, max_shards,
            )?;
            Ok(port_lines_to_alive_hosts(lines))
        })
        .await
        .map_err(|e| std::io::Error::other(format!("{e}")))?
    };

    let v6_fut = async {
        if v6.is_empty() {
            return Ok::<HashSet<IpAddr>, std::io::Error>(HashSet::new());
        }
        tokio::task::spawn_blocking(move || {
            let lines = crate::syn::parallel_ack_ping_scan_ipv6(
                v6, timeout, None, None, None, None, None, 0, max_shards,
            )?;
            Ok(port_lines_to_alive_hosts(lines))
        })
        .await
        .map_err(|e| std::io::Error::other(format!("{e}")))?
    };

    let (r4, r6) = tokio::join!(v4_fut, v6_fut);
    let mut out = r4?;
    out.extend(r6?);
    Ok(out)
}

/// Raw SCTP INIT discovery (`-PY`): INIT-ACK or ABORT ⇒ host responded.
/// IPv4 / IPv6 are merged independently; one family may warn and yield empty while the other succeeds.
async fn sctp_raw_discovery_collect(
    hosts: &[IpAddr],
    ports: &[u16],
    skip: Option<&HashSet<IpAddr>>,
    timeout: Duration,
    max_shards: usize,
) -> HashSet<IpAddr> {
    let mut v4: Vec<(Ipv4Addr, u16)> = Vec::new();
    let mut v6: Vec<(Ipv6Addr, u16)> = Vec::new();
    for &h in hosts {
        if skip.map(|s| s.contains(&h)).unwrap_or(false) {
            continue;
        }
        for &p in ports {
            match h {
                IpAddr::V4(a) => v4.push((a, p)),
                IpAddr::V6(a) => v6.push((a, p)),
            }
        }
    }

    let v4_fut = async {
        if v4.is_empty() {
            return Ok(HashSet::new());
        }
        let r = tokio::task::spawn_blocking(move || {
            crate::sctp::parallel_sctp_scan_ipv4(
                v4,
                crate::sctp::SctpProbeKind::Init,
                timeout,
                None,
                None,
                None,
                None,
                None,
                0,
                max_shards,
            )
            .map(port_lines_to_alive_hosts)
        })
        .await;
        match r {
            Ok(Ok(s)) => Ok(s),
            Ok(Err(e)) => Err(e),
            Err(e) => Err(std::io::Error::other(format!("{e}"))),
        }
    };

    let v6_fut = async {
        if v6.is_empty() {
            return Ok(HashSet::new());
        }
        let r = tokio::task::spawn_blocking(move || {
            crate::sctp::parallel_sctp_scan_ipv6(
                v6,
                crate::sctp::SctpProbeKind::Init,
                timeout,
                None,
                None,
                None,
                None,
                None,
                0,
                max_shards,
            )
            .map(port_lines_to_alive_hosts)
        })
        .await;
        match r {
            Ok(Ok(s)) => Ok(s),
            Ok(Err(e)) => Err(e),
            Err(e) => Err(std::io::Error::other(format!("{e}"))),
        }
    };

    let (r4, r6) = tokio::join!(v4_fut, v6_fut);
    let mut out = HashSet::new();
    match r4 {
        Ok(s) => out.extend(s),
        Err(e) => tracing::warn!(error = %e, "SCTP discovery IPv4 (-PY) failed"),
    }
    match r6 {
        Ok(s) => out.extend(s),
        Err(e) => tracing::warn!(error = %e, "SCTP discovery IPv6 (-PY) failed"),
    }
    out
}

/// Raw IP-protocol discovery (`-PO`): ICMP unreachable / IPv6 Parameter Problem ⇒ “closed”; same
/// engines as `-sO`. IPv4 / IPv6 merged independently on partial failure.
async fn ip_proto_ping_discovery_collect(
    hosts: &[IpAddr],
    protos: &[u16],
    skip: Option<&HashSet<IpAddr>>,
    timeout: Duration,
    max_shards: usize,
) -> HashSet<IpAddr> {
    let mut v4: Vec<(Ipv4Addr, u16)> = Vec::new();
    let mut v6: Vec<(Ipv6Addr, u16)> = Vec::new();
    for &h in hosts {
        if skip.map(|s| s.contains(&h)).unwrap_or(false) {
            continue;
        }
        for &p in protos {
            match h {
                IpAddr::V4(a) => v4.push((a, p)),
                IpAddr::V6(a) => v6.push((a, p)),
            }
        }
    }

    let v4_fut = async {
        if v4.is_empty() {
            return Ok(HashSet::new());
        }
        let r = tokio::task::spawn_blocking(move || {
            crate::ip_proto::parallel_ip_proto_scan_ipv4(
                v4, timeout, None, None, None, None, None, 0, max_shards,
            )
            .map(port_lines_to_alive_hosts)
        })
        .await;
        match r {
            Ok(Ok(s)) => Ok(s),
            Ok(Err(e)) => Err(e),
            Err(e) => Err(std::io::Error::other(format!("{e}"))),
        }
    };

    let v6_fut = async {
        if v6.is_empty() {
            return Ok(HashSet::new());
        }
        let r = tokio::task::spawn_blocking(move || {
            crate::ip_proto::parallel_ip_proto_scan_ipv6(
                v6, timeout, None, None, None, None, None, 0, max_shards,
            )
            .map(port_lines_to_alive_hosts)
        })
        .await;
        match r {
            Ok(Ok(s)) => Ok(s),
            Ok(Err(e)) => Err(e),
            Err(e) => Err(std::io::Error::other(format!("{e}"))),
        }
    };

    let (r4, r6) = tokio::join!(v4_fut, v6_fut);
    let mut out = HashSet::new();
    match r4 {
        Ok(s) => out.extend(s),
        Err(e) => tracing::warn!(error = %e, "IP protocol discovery IPv4 (-PO) failed"),
    }
    match r6 {
        Ok(s) => out.extend(s),
        Err(e) => tracing::warn!(error = %e, "IP protocol discovery IPv6 (-PO) failed"),
    }
    out
}

async fn ip_proto_ping_discovery_merge(
    hosts: &[IpAddr],
    protos: &[u16],
    alive: &mut HashSet<IpAddr>,
    connect_timeout: Duration,
    max_shards: usize,
) {
    let s =
        ip_proto_ping_discovery_collect(hosts, protos, Some(&*alive), connect_timeout, max_shards)
            .await;
    alive.extend(s);
}

async fn sctp_discovery_merge(
    hosts: &[IpAddr],
    ports: &[u16],
    alive: &mut HashSet<IpAddr>,
    connect_timeout: Duration,
    max_shards: usize,
) {
    let s =
        sctp_raw_discovery_collect(hosts, ports, Some(&*alive), connect_timeout, max_shards).await;
    alive.extend(s);
}

/// `-PS` / default TCP ports: raw SYN when possible, else TCP connect.
async fn tcp_ps_discovery_or_connect(
    hosts: &[IpAddr],
    ports: &[u16],
    concurrency: usize,
    skip: Option<&HashSet<IpAddr>>,
    connect_timeout: Duration,
    max_shards: usize,
) -> HashSet<IpAddr> {
    match syn_raw_discovery_collect(hosts, ports, skip, connect_timeout, max_shards).await {
        Ok(s) => s,
        Err(e) => {
            tracing::warn!(error = %e, "raw TCP SYN discovery failed; using TCP connect");
            tcp_connect_discovery_collect(hosts, ports, concurrency, skip, connect_timeout).await
        }
    }
}

async fn tcp_ps_discovery_merge(
    hosts: &[IpAddr],
    ports: &[u16],
    concurrency: usize,
    alive: &mut HashSet<IpAddr>,
    connect_timeout: Duration,
    max_shards: usize,
) {
    let new_up = tcp_ps_discovery_or_connect(
        hosts,
        ports,
        concurrency,
        Some(&*alive),
        connect_timeout,
        max_shards,
    )
    .await;
    alive.extend(new_up);
}

/// `-PA`: raw ACK when possible, else TCP connect.
async fn tcp_pa_discovery_or_connect(
    hosts: &[IpAddr],
    ports: &[u16],
    concurrency: usize,
    skip: Option<&HashSet<IpAddr>>,
    connect_timeout: Duration,
    max_shards: usize,
) -> HashSet<IpAddr> {
    match ack_raw_discovery_collect(hosts, ports, skip, connect_timeout, max_shards).await {
        Ok(s) => s,
        Err(e) => {
            tracing::warn!(error = %e, "raw TCP ACK discovery failed; using TCP connect");
            tcp_connect_discovery_collect(hosts, ports, concurrency, skip, connect_timeout).await
        }
    }
}

async fn tcp_pa_discovery_merge(
    hosts: &[IpAddr],
    ports: &[u16],
    concurrency: usize,
    alive: &mut HashSet<IpAddr>,
    connect_timeout: Duration,
    max_shards: usize,
) {
    let new_up = tcp_pa_discovery_or_connect(
        hosts,
        ports,
        concurrency,
        Some(&*alive),
        connect_timeout,
        max_shards,
    )
    .await;
    alive.extend(new_up);
}

async fn tcp_probe(host: IpAddr, port: u16, timeout: Duration) -> bool {
    let addr = SocketAddr::new(host, port);
    match tokio::time::timeout(timeout, TcpStream::connect(addr)).await {
        Ok(Ok(_)) => true,
        Ok(Err(e)) => e.kind() == TokioErrorKind::ConnectionRefused,
        Err(_) => false,
    }
}

async fn tcp_connect_discovery_collect(
    hosts: &[IpAddr],
    ports: &[u16],
    concurrency: usize,
    skip: Option<&HashSet<IpAddr>>,
    connect_timeout: Duration,
) -> HashSet<IpAddr> {
    let pairs: Vec<(IpAddr, u16)> = hosts
        .iter()
        .copied()
        .filter(|h| !skip.map(|s| s.contains(h)).unwrap_or(false))
        .flat_map(|h| ports.iter().map(move |&p| (h, p)))
        .collect();

    let results: Vec<(IpAddr, bool)> = stream::iter(pairs)
        .map(|(host, port)| {
            let t = connect_timeout;
            async move { (host, tcp_probe(host, port, t).await) }
        })
        .buffer_unordered(concurrency)
        .collect()
        .await;

    results
        .into_iter()
        .filter(|(_, ok)| *ok)
        .map(|(h, _)| h)
        .collect()
}

async fn udp_ping_probe(host: IpAddr, port: u16, wait: Duration) -> bool {
    let bind_addr: SocketAddr = match host {
        IpAddr::V4(_) => "0.0.0.0:0".parse().unwrap(),
        IpAddr::V6(_) => "[::]:0".parse().unwrap(),
    };
    let Ok(socket) = UdpSocket::bind(bind_addr).await else {
        return false;
    };
    let dst = SocketAddr::new(host, port);
    if socket.send_to(&[0u8], dst).await.is_err() {
        return false;
    }
    let mut buf = [0u8; 2048];
    match tokio::time::timeout(wait, socket.recv_from(&mut buf)).await {
        Ok(Ok((n, _))) => n > 0,
        Ok(Err(e)) => matches!(
            e.kind(),
            IoErrorKind::ConnectionRefused
                | IoErrorKind::NetworkUnreachable
                | IoErrorKind::HostUnreachable
        ),
        Err(_) => false,
    }
}

async fn udp_discovery_collect(
    hosts: &[IpAddr],
    ports: &[u16],
    concurrency: usize,
    skip: Option<&HashSet<IpAddr>>,
    icmp_wait: Duration,
) -> HashSet<IpAddr> {
    let pairs: Vec<(IpAddr, u16)> = hosts
        .iter()
        .copied()
        .filter(|h| !skip.map(|s| s.contains(h)).unwrap_or(false))
        .flat_map(|h| ports.iter().map(move |&p| (h, p)))
        .collect();

    let results: Vec<(IpAddr, bool)> = stream::iter(pairs)
        .map(|(host, port)| {
            let w = icmp_wait;
            async move { (host, udp_ping_probe(host, port, w).await) }
        })
        .buffer_unordered(concurrency)
        .collect()
        .await;

    results
        .into_iter()
        .filter(|(_, ok)| *ok)
        .map(|(h, _)| h)
        .collect()
}

async fn udp_discovery(
    hosts: &[IpAddr],
    ports: &[u16],
    concurrency: usize,
    alive: &mut HashSet<IpAddr>,
    icmp_wait: Duration,
) {
    let new_up = udp_discovery_collect(hosts, ports, concurrency, Some(&*alive), icmp_wait).await;
    alive.extend(new_up);
}

#[cfg(unix)]
async fn icmp_timestamp_discovery_merge(
    hosts: &[IpAddr],
    alive: &mut HashSet<IpAddr>,
    timeout: Duration,
    concurrency: usize,
) {
    let v4: Vec<Ipv4Addr> = hosts
        .iter()
        .copied()
        .filter(|h| !alive.contains(h))
        .filter_map(|h| match h {
            IpAddr::V4(a) => Some(a),
            IpAddr::V6(_) => None,
        })
        .collect();
    if v4.is_empty() {
        return;
    }
    let results: Vec<(IpAddr, bool)> = stream::iter(v4)
        .map(|dst| async move {
            let ok = tokio::task::spawn_blocking(move || {
                crate::icmp_ping::icmp_timestamp_probe_v4(dst, timeout)
            })
            .await
            .unwrap_or(false);
            (IpAddr::V4(dst), ok)
        })
        .buffer_unordered(concurrency)
        .collect()
        .await;
    for (ip, ok) in results {
        if ok {
            alive.insert(ip);
        }
    }
}

#[cfg(unix)]
async fn icmp_mask_discovery_merge(
    hosts: &[IpAddr],
    alive: &mut HashSet<IpAddr>,
    timeout: Duration,
    concurrency: usize,
) {
    let v4: Vec<Ipv4Addr> = hosts
        .iter()
        .copied()
        .filter(|h| !alive.contains(h))
        .filter_map(|h| match h {
            IpAddr::V4(a) => Some(a),
            IpAddr::V6(_) => None,
        })
        .collect();
    if v4.is_empty() {
        return;
    }
    let results: Vec<(IpAddr, bool)> = stream::iter(v4)
        .map(|dst| async move {
            let ok = tokio::task::spawn_blocking(move || {
                crate::icmp_ping::icmp_address_mask_probe_v4(dst, timeout)
            })
            .await
            .unwrap_or(false);
            (IpAddr::V4(dst), ok)
        })
        .buffer_unordered(concurrency)
        .collect()
        .await;
    for (ip, ok) in results {
        if ok {
            alive.insert(ip);
        }
    }
}

/// Determine which IPv4 targets are on local subnets (ARP-reachable) by comparing
/// against local interface addresses.
fn local_subnet_v4_hosts(hosts: &[IpAddr]) -> Vec<Ipv4Addr> {
    let ifaces = match if_addrs::get_if_addrs() {
        Ok(v) => v,
        Err(_) => return Vec::new(),
    };
    let mut locals: Vec<(Ipv4Addr, u32)> = Vec::new();
    for iface in &ifaces {
        if let if_addrs::IfAddr::V4(ref v4) = iface.addr {
            let ip = v4.ip;
            let mask = v4.netmask;
            let mask_u32 = u32::from(mask);
            locals.push((ip, mask_u32));
        }
    }
    let mut out = Vec::new();
    for h in hosts {
        if let IpAddr::V4(target) = h {
            let t = u32::from(*target);
            for &(local_ip, mask) in &locals {
                let l = u32::from(local_ip);
                if (t & mask) == (l & mask) {
                    out.push(*target);
                    break;
                }
            }
        }
    }
    out
}

/// ARP ping for local-subnet IPv4 hosts. Sends ARP requests and listens for replies.
/// Returns set of hosts that replied within the timeout.
#[cfg(unix)]
fn arp_ping_hosts(
    targets: &[Ipv4Addr],
    timeout: Duration,
    spoof_mac: Option<[u8; 6]>,
) -> HashSet<IpAddr> {
    use pnet::datalink::{self, Channel};
    use pnet::packet::arp::{ArpHardwareTypes, ArpOperations, ArpPacket, MutableArpPacket};
    use pnet::packet::ethernet::{EtherTypes, MutableEthernetPacket};
    use pnet::packet::Packet;
    use pnet::util::MacAddr;

    let mut alive = HashSet::new();
    if targets.is_empty() {
        return alive;
    }

    // Find the default interface.
    let interfaces = datalink::interfaces();
    let iface = match interfaces
        .iter()
        .find(|i| i.is_up() && !i.is_loopback() && i.ips.iter().any(|ip| ip.is_ipv4()))
    {
        Some(i) => i.clone(),
        None => return alive,
    };

    let src_mac = if let Some(mac) = spoof_mac {
        MacAddr::new(mac[0], mac[1], mac[2], mac[3], mac[4], mac[5])
    } else {
        match iface.mac {
            Some(m) => m,
            None => return alive,
        }
    };
    let src_ip = match iface.ips.iter().find_map(|ip| match ip.ip() {
        IpAddr::V4(a) => Some(a),
        _ => None,
    }) {
        Some(ip) => ip,
        None => return alive,
    };

    let (mut tx, mut rx) = match datalink::channel(&iface, Default::default()) {
        Ok(Channel::Ethernet(tx, rx)) => (tx, rx),
        _ => return alive,
    };

    // Send ARP requests for all targets.
    let target_set: HashSet<Ipv4Addr> = targets.iter().copied().collect();
    for &target in targets {
        let mut eth_buf = vec![0u8; 42]; // 14 (eth) + 28 (arp)
        if let Some(mut eth) = MutableEthernetPacket::new(&mut eth_buf) {
            eth.set_destination(MacAddr::broadcast());
            eth.set_source(src_mac);
            eth.set_ethertype(EtherTypes::Arp);

            let mut arp_buf = vec![0u8; 28];
            if let Some(mut arp) = MutableArpPacket::new(&mut arp_buf) {
                arp.set_hardware_type(ArpHardwareTypes::Ethernet);
                arp.set_protocol_type(EtherTypes::Ipv4);
                arp.set_hw_addr_len(6);
                arp.set_proto_addr_len(4);
                arp.set_operation(ArpOperations::Request);
                arp.set_sender_hw_addr(src_mac);
                arp.set_sender_proto_addr(src_ip);
                arp.set_target_hw_addr(MacAddr::zero());
                arp.set_target_proto_addr(target);
                eth.set_payload(arp.packet());
            }
            let _ = tx.send_to(eth.packet(), None);
        }
    }

    // Listen for ARP replies.
    let deadline = std::time::Instant::now() + timeout;
    while std::time::Instant::now() < deadline {
        match rx.next() {
            Ok(frame) => {
                if let Some(eth) = pnet::packet::ethernet::EthernetPacket::new(frame) {
                    if eth.get_ethertype() == EtherTypes::Arp {
                        if let Some(arp) = ArpPacket::new(eth.payload()) {
                            if arp.get_operation() == ArpOperations::Reply {
                                let sender = arp.get_sender_proto_addr();
                                if target_set.contains(&sender) {
                                    alive.insert(IpAddr::V4(sender));
                                    if alive.len() == target_set.len() {
                                        break;
                                    }
                                }
                            }
                        }
                    }
                }
            }
            Err(_) => break,
        }
    }
    alive
}

#[cfg(not(unix))]
fn arp_ping_hosts(
    _targets: &[Ipv4Addr],
    _timeout: Duration,
    _spoof_mac: Option<[u8; 6]>,
) -> HashSet<IpAddr> {
    HashSet::new()
}

/// Filter `hosts` to targets that respond to at least one discovery probe.
pub async fn hosts_after_discovery(
    hosts: Vec<IpAddr>,
    args: &Args,
    concurrency: usize,
    connect_timeout: Duration,
    spoof_mac: Option<[u8; 6]>,
) -> Result<Vec<IpAddr>> {
    if args.no_ping {
        return Ok(hosts);
    }
    if hosts.is_empty() {
        return Ok(hosts);
    }

    let c = concurrency.max(1);
    let max_shards = c.clamp(1, MAX_SYN_PARALLEL_SHARDS);
    let max_sctp_shards = c.clamp(1, MAX_SCTP_PARALLEL_SHARDS);
    let max_ip_proto_shards = c.clamp(1, MAX_IP_PROTO_PARALLEL_SHARDS);

    if (args.ping_timestamp || args.ping_mask) && hosts.iter().any(|h| matches!(h, IpAddr::V6(_))) {
        let n = hosts.iter().filter(|h| matches!(h, IpAddr::V6(_))).count();
        tracing::warn!(
            count = n,
            "ICMP timestamp (-PP) and netmask (-PM) are IPv4-only; skipping IPv6 targets"
        );
    }
    if args.ping_timestamp && cfg!(not(unix)) {
        tracing::warn!("-PP (ICMP timestamp) requires Unix raw ICMP; skipping");
    }
    if args.ping_mask && cfg!(not(unix)) {
        tracing::warn!("-PM (ICMP netmask) requires Unix raw ICMP; skipping");
    }

    let explicit = has_explicit_discovery_flags(args);
    let implemented = has_implemented_explicit_probes(args);

    if explicit && !implemented {
        tracing::warn!(
            "only unimplemented -P* discovery probes were given; using default discovery (ICMP + TCP {})",
            DEFAULT_TCP_DISCOVERY_PORTS
                .iter()
                .map(|p| p.to_string())
                .collect::<Vec<_>>()
                .join(",")
        );
    }

    let mut alive: HashSet<IpAddr> = HashSet::new();

    // ARP discovery for local-subnet IPv4 hosts (nmap default behavior, skip with --disable-arp-ping).
    if !args.disable_arp_ping {
        let local_v4 = local_subnet_v4_hosts(&hosts);
        if !local_v4.is_empty() {
            let arp_alive = tokio::task::spawn_blocking({
                let local_v4 = local_v4.clone();
                let ct = connect_timeout;
                move || arp_ping_hosts(&local_v4, ct.min(Duration::from_secs(2)), spoof_mac)
            })
            .await
            .unwrap_or_default();
            alive.extend(arp_alive);
        }
    }

    let use_default = !(explicit && implemented);

    if use_default {
        let hosts_icmp = hosts.clone();
        let hosts_tcp = hosts.clone();
        let ct = connect_timeout;
        let (icmp_alive, tcp_alive) = tokio::join!(
            async move {
                let mut s = HashSet::new();
                for o in ping_hosts(&hosts_icmp, c).await {
                    if o.up {
                        s.insert(o.host);
                    }
                }
                s
            },
            async move {
                tcp_ps_discovery_or_connect(
                    &hosts_tcp,
                    DEFAULT_TCP_DISCOVERY_PORTS,
                    c,
                    None,
                    ct,
                    max_shards,
                )
                .await
            }
        );
        alive.extend(icmp_alive);
        alive.extend(tcp_alive);
    } else {
        if args.ping_echo {
            for o in ping_hosts(&hosts, c).await {
                if o.up {
                    alive.insert(o.host);
                }
            }
        }
        if args.ping_syn.is_some() {
            let ports = ports_from_ping_tcp(&args.ping_syn, TCP_SYN_DEFAULT)?;
            tcp_ps_discovery_merge(&hosts, &ports, c, &mut alive, connect_timeout, max_shards)
                .await;
        }
        if args.ping_ack.is_some() {
            let ports = ports_from_ping_tcp(&args.ping_ack, TCP_ACK_DEFAULT)?;
            tcp_pa_discovery_merge(&hosts, &ports, c, &mut alive, connect_timeout, max_shards)
                .await;
        }
        if args.ping_udp.is_some() {
            let ports = ports_from_ping_udp(&args.ping_udp)?;
            udp_discovery(&hosts, &ports, c, &mut alive, connect_timeout).await;
        }
        if args.ping_sctp.is_some() {
            let ports = ports_from_ping_sctp(&args.ping_sctp)?;
            sctp_discovery_merge(&hosts, &ports, &mut alive, connect_timeout, max_sctp_shards)
                .await;
        }
        if let Some(opt) = &args.ping_ip_proto {
            let protos = parse_ping_ip_proto_list(opt.as_deref().unwrap_or(""))?;
            ip_proto_ping_discovery_merge(
                &hosts,
                &protos,
                &mut alive,
                connect_timeout,
                max_ip_proto_shards,
            )
            .await;
        }
        if args.ping_timestamp {
            #[cfg(unix)]
            icmp_timestamp_discovery_merge(&hosts, &mut alive, connect_timeout, c).await;
        }
        if args.ping_mask {
            #[cfg(unix)]
            icmp_mask_discovery_merge(&hosts, &mut alive, connect_timeout, c).await;
        }
    }

    let out: Vec<IpAddr> = hosts.into_iter().filter(|h| alive.contains(h)).collect();
    if out.is_empty() {
        bail!("no hosts responded to discovery (try -Pn to skip host discovery, or adjust -P* probes)");
    }
    Ok(out)
}

#[cfg(test)]
mod tests {
    use clap::Parser;

    use super::*;

    #[test]
    fn ports_from_ping_tcp_variants() {
        assert_eq!(ports_from_ping_tcp(&Some(None), 80).unwrap(), vec![80]);
        assert_eq!(
            ports_from_ping_tcp(&Some(Some("443".into())), 80).unwrap(),
            vec![443]
        );
        assert_eq!(
            ports_from_ping_tcp(&Some(Some("80,443".into())), 80).unwrap(),
            vec![80, 443]
        );
        assert!(ports_from_ping_tcp(&None, 80).unwrap().is_empty());
    }

    #[test]
    fn ports_from_ping_udp_variants() {
        assert_eq!(ports_from_ping_udp(&Some(None)).unwrap(), vec![40_125]);
        assert_eq!(
            ports_from_ping_udp(&Some(Some("53".into()))).unwrap(),
            vec![53]
        );
        assert!(ports_from_ping_udp(&None).unwrap().is_empty());
    }

    #[test]
    fn ports_from_ping_sctp_variants() {
        assert_eq!(ports_from_ping_sctp(&Some(None)).unwrap(), vec![80]);
        assert_eq!(
            ports_from_ping_sctp(&Some(Some("38412".into()))).unwrap(),
            vec![38412]
        );
        assert!(ports_from_ping_sctp(&None).unwrap().is_empty());
    }

    #[test]
    fn parse_ping_ip_proto_list_variants() {
        assert_eq!(parse_ping_ip_proto_list("").unwrap(), vec![1, 2, 4]);
        assert_eq!(parse_ping_ip_proto_list("  ").unwrap(), vec![1, 2, 4]);
        assert_eq!(parse_ping_ip_proto_list("6").unwrap(), vec![6]);
        assert_eq!(parse_ping_ip_proto_list("17,6").unwrap(), vec![6, 17]);
        assert!(parse_ping_ip_proto_list("256").is_err());
        assert!(parse_ping_ip_proto_list(",,").is_err());
    }

    #[test]
    fn port_lines_to_alive_filters_timeouts() {
        let lines = vec![
            PortLine::new(
                IpAddr::V4(std::net::Ipv4Addr::new(10, 0, 0, 1)),
                80,
                "tcp",
                "closed",
                crate::scan::PortReason::ConnRefused,
                None,
            ),
            PortLine::new(
                IpAddr::V4(std::net::Ipv4Addr::new(10, 0, 0, 2)),
                80,
                "tcp",
                "filtered",
                crate::scan::PortReason::Timeout,
                None,
            ),
        ];
        let a = port_lines_to_alive_hosts(lines);
        assert_eq!(a.len(), 1);
        assert!(a.contains(&IpAddr::V4(std::net::Ipv4Addr::new(10, 0, 0, 1))));
    }

    #[test]
    fn has_explicit_discovery_flags_false_by_default() {
        let args = crate::cli::Args::try_parse_from(["nmaprs", "-p", "80", "127.0.0.1"]).unwrap();
        assert!(!has_explicit_discovery_flags(&args));
    }

    #[test]
    fn has_explicit_discovery_flags_ping_echo() {
        let args =
            crate::cli::Args::try_parse_from(["nmaprs", "-p", "80", "--ping-E", "127.0.0.1"])
                .unwrap();
        assert!(has_explicit_discovery_flags(&args));
        assert!(has_implemented_explicit_probes(&args));
    }

    #[test]
    fn has_explicit_discovery_flags_ping_syn_some() {
        let args = crate::cli::Args::try_parse_from([
            "nmaprs",
            "-p",
            "80",
            "--ping-S",
            "443",
            "127.0.0.1",
        ])
        .unwrap();
        assert!(has_explicit_discovery_flags(&args));
    }

    #[test]
    fn has_explicit_discovery_flags_ip_proto_without_timestamp_mask_gap() {
        let args = crate::cli::Args::try_parse_from([
            "nmaprs",
            "-p",
            "80",
            "--ping-ip-proto",
            "6",
            "127.0.0.1",
        ])
        .unwrap();
        assert!(has_explicit_discovery_flags(&args));
        assert!(has_implemented_explicit_probes(&args));
    }

    #[test]
    fn parse_ping_ip_proto_list_sorts_and_dedupes() {
        assert_eq!(
            parse_ping_ip_proto_list("17,1,17,6").unwrap(),
            vec![1, 6, 17]
        );
    }

    #[test]
    fn port_lines_to_alive_includes_open_and_closed() {
        let ip = IpAddr::V4(std::net::Ipv4Addr::new(10, 0, 0, 5));
        let open = PortLine::new(
            ip,
            443,
            "tcp",
            "open",
            crate::scan::PortReason::SynAck,
            None,
        );
        let closed = PortLine::new(
            ip,
            444,
            "tcp",
            "closed",
            crate::scan::PortReason::ConnRefused,
            None,
        );
        let filtered = PortLine::new(
            ip,
            445,
            "tcp",
            "filtered",
            crate::scan::PortReason::Timeout,
            None,
        );
        let a = port_lines_to_alive_hosts(vec![open, closed, filtered]);
        assert_eq!(a.len(), 1);
        assert!(a.contains(&ip));
    }

    #[test]
    fn has_explicit_discovery_flags_false_for_no_ping_only() {
        let args =
            crate::cli::Args::try_parse_from(["nmaprs", "--no-ping", "-p", "80", "127.0.0.1"])
                .unwrap();
        assert!(!has_explicit_discovery_flags(&args));
    }

    #[test]
    fn ports_from_ping_tcp_empty_some_none_uses_default_port() {
        assert_eq!(ports_from_ping_tcp(&Some(None), 1234).unwrap(), vec![1234]);
    }

    #[test]
    fn parse_ping_ip_proto_list_rejects_non_numeric() {
        assert!(parse_ping_ip_proto_list("1,foo").is_err());
    }

    #[test]
    fn port_lines_to_alive_empty_input() {
        assert!(port_lines_to_alive_hosts(vec![]).is_empty());
    }

    #[test]
    fn has_implemented_explicit_probes_false_when_only_no_ping() {
        let args =
            crate::cli::Args::try_parse_from(["nmaprs", "--no-ping", "-p", "80", "127.0.0.1"])
                .unwrap();
        assert!(!has_implemented_explicit_probes(&args));
    }

    #[test]
    fn ports_from_ping_sctp_default_port_80() {
        assert_eq!(ports_from_ping_sctp(&Some(None)).unwrap(), vec![80]);
    }

    #[test]
    fn ports_from_ping_ack_uses_tcp_helper_with_default() {
        use super::TCP_ACK_DEFAULT;
        assert_eq!(
            ports_from_ping_tcp(&Some(None), TCP_ACK_DEFAULT).unwrap(),
            vec![TCP_ACK_DEFAULT]
        );
    }

    #[test]
    fn parse_ping_ip_proto_list_single_zero() {
        assert_eq!(parse_ping_ip_proto_list("0").unwrap(), vec![0]);
    }

    #[test]
    fn port_lines_to_alive_excludes_only_filtered() {
        let ip = IpAddr::V4(std::net::Ipv4Addr::new(10, 0, 0, 8));
        let filtered = PortLine::new(
            ip,
            1,
            "tcp",
            "filtered",
            crate::scan::PortReason::Timeout,
            None,
        );
        assert!(port_lines_to_alive_hosts(vec![filtered]).is_empty());
    }

    #[test]
    fn has_implemented_explicit_probes_ping_timestamp() {
        let args =
            crate::cli::Args::try_parse_from(["nmaprs", "-p", "80", "--ping-P", "127.0.0.1"])
                .unwrap();
        assert!(has_implemented_explicit_probes(&args));
    }

    #[test]
    fn has_implemented_explicit_probes_ping_mask() {
        let args =
            crate::cli::Args::try_parse_from(["nmaprs", "-p", "80", "--ping-M", "127.0.0.1"])
                .unwrap();
        assert!(has_implemented_explicit_probes(&args));
    }

    #[test]
    fn parse_ping_ip_proto_list_dedupes() {
        assert_eq!(parse_ping_ip_proto_list("1,1,2,2").unwrap(), vec![1, 2]);
    }

    #[test]
    fn ports_from_ping_udp_default_port_40125() {
        assert_eq!(ports_from_ping_udp(&Some(None)).unwrap(), vec![40_125]);
    }

    #[test]
    fn port_lines_to_alive_dedupes_same_host() {
        let ip = IpAddr::V4(std::net::Ipv4Addr::new(10, 0, 0, 9));
        let a = PortLine::new(
            ip,
            80,
            "tcp",
            "closed",
            crate::scan::PortReason::ConnRefused,
            None,
        );
        let b = PortLine::new(
            ip,
            443,
            "tcp",
            "open",
            crate::scan::PortReason::SynAck,
            None,
        );
        let alive = port_lines_to_alive_hosts(vec![a, b]);
        assert_eq!(alive.len(), 1);
        assert!(alive.contains(&ip));
    }

    #[test]
    fn has_explicit_discovery_flags_ping_syn_short_flag() {
        let args =
            crate::cli::Args::try_parse_from(["nmaprs", "-p", "80", "--ping-S", "127.0.0.1"])
                .unwrap();
        assert!(has_explicit_discovery_flags(&args));
    }

    #[test]
    fn has_explicit_discovery_flags_ping_ip_proto() {
        let args = crate::cli::Args::try_parse_from([
            "nmaprs",
            "-p",
            "80",
            "--ping-ip-proto",
            "1,6",
            "127.0.0.1",
        ])
        .unwrap();
        assert!(has_explicit_discovery_flags(&args));
    }

    #[test]
    fn parse_ping_ip_proto_list_whitespace_only_defaults() {
        assert_eq!(parse_ping_ip_proto_list("   ").unwrap(), vec![1, 2, 4]);
    }

    #[test]
    fn parse_ping_ip_proto_list_max_255_ok() {
        assert_eq!(parse_ping_ip_proto_list("255").unwrap(), vec![255]);
    }

    #[test]
    fn ports_from_ping_tcp_invalid_spec_errors() {
        assert!(ports_from_ping_tcp(&Some(Some("abc".into())), 80).is_err());
    }

    #[test]
    fn ports_from_ping_udp_invalid_spec_errors() {
        assert!(ports_from_ping_udp(&Some(Some("99999".into()))).is_err());
    }

    #[test]
    fn port_lines_to_alive_open_only_counts() {
        let ip = IpAddr::V4(std::net::Ipv4Addr::new(10, 0, 0, 20));
        let open = PortLine::new(ip, 80, "tcp", "open", crate::scan::PortReason::SynAck, None);
        let alive = port_lines_to_alive_hosts(vec![open]);
        assert_eq!(alive.len(), 1);
    }

    #[test]
    fn has_implemented_explicit_probes_ping_echo() {
        let args =
            crate::cli::Args::try_parse_from(["nmaprs", "-p", "80", "--ping-E", "127.0.0.1"])
                .unwrap();
        assert!(has_implemented_explicit_probes(&args));
    }

    #[test]
    fn has_implemented_explicit_probes_ping_udp() {
        let args =
            crate::cli::Args::try_parse_from(["nmaprs", "-p", "80", "--ping-U", "127.0.0.1"])
                .unwrap();
        assert!(has_implemented_explicit_probes(&args));
    }

    #[test]
    fn parse_ping_ip_proto_list_sorts_unsorted_input() {
        assert_eq!(parse_ping_ip_proto_list("17,6,1").unwrap(), vec![1, 6, 17]);
    }

    #[test]
    fn parse_ping_ip_proto_list_256_errors() {
        assert!(parse_ping_ip_proto_list("256").is_err());
    }

    #[test]
    fn has_explicit_discovery_flags_no_ping_syn() {
        let args =
            crate::cli::Args::try_parse_from(["nmaprs", "-p", "80", "--no-ping", "127.0.0.1"])
                .unwrap();
        assert!(!has_explicit_discovery_flags(&args));
    }

    #[test]
    fn port_lines_to_alive_closed_state_counts_as_up() {
        let ip = IpAddr::V4(std::net::Ipv4Addr::new(10, 0, 0, 31));
        let closed = PortLine::new(
            ip,
            80,
            "tcp",
            "closed",
            crate::scan::PortReason::ConnRefused,
            None,
        );
        let alive = port_lines_to_alive_hosts(vec![closed]);
        assert_eq!(alive.len(), 1);
        assert!(alive.contains(&ip));
    }

    #[test]
    fn has_implemented_explicit_probes_ping_ip_proto() {
        let args = crate::cli::Args::try_parse_from([
            "nmaprs",
            "-p",
            "80",
            "--ping-ip-proto",
            "1",
            "127.0.0.1",
        ])
        .unwrap();
        assert!(has_implemented_explicit_probes(&args));
    }

    #[test]
    fn has_implemented_explicit_probes_ping_ack() {
        let args =
            crate::cli::Args::try_parse_from(["nmaprs", "-p", "80", "--ping-A", "127.0.0.1"])
                .unwrap();
        assert!(has_implemented_explicit_probes(&args));
    }

    #[test]
    fn has_implemented_explicit_probes_ping_sctp() {
        let args =
            crate::cli::Args::try_parse_from(["nmaprs", "-p", "80", "--ping-Y", "127.0.0.1"])
                .unwrap();
        assert!(has_implemented_explicit_probes(&args));
    }

    #[test]
    fn has_explicit_discovery_flags_ping_ack() {
        let args = crate::cli::Args::try_parse_from([
            "nmaprs",
            "-p",
            "80",
            "--ping-A",
            "443",
            "127.0.0.1",
        ])
        .unwrap();
        assert!(has_explicit_discovery_flags(&args));
    }

    #[test]
    fn ports_from_ping_tcp_range_spec() {
        assert_eq!(
            ports_from_ping_tcp(&Some(Some("22-24".into())), 80).unwrap(),
            vec![22, 23, 24]
        );
    }

    #[test]
    fn ports_from_ping_sctp_multi_port() {
        assert_eq!(
            ports_from_ping_sctp(&Some(Some("80,443".into()))).unwrap(),
            vec![80, 443]
        );
    }

    #[test]
    fn parse_ping_ip_proto_list_leading_comma_skips_empty_token() {
        assert_eq!(parse_ping_ip_proto_list(",1").unwrap(), vec![1]);
    }

    #[test]
    fn port_lines_to_alive_ipv6_host() {
        let ip: IpAddr = "2001:db8::1".parse().unwrap();
        let open = PortLine::new(
            ip,
            443,
            "tcp",
            "open",
            crate::scan::PortReason::SynAck,
            None,
        );
        let alive = port_lines_to_alive_hosts(vec![open]);
        assert!(alive.contains(&ip));
    }

    #[test]
    fn local_subnet_v4_includes_loopback_when_on_subnet() {
        let hosts = vec![IpAddr::V4(Ipv4Addr::LOCALHOST)];
        let local = local_subnet_v4_hosts(&hosts);
        assert!(local.contains(&Ipv4Addr::LOCALHOST) || local.is_empty());
    }
}
