// Copyright 2018-2026 the Deno authors. MIT license.

use std::borrow::Cow;
use std::cell::RefCell;
use std::net::Ipv4Addr;
use std::net::Ipv6Addr;
use std::net::SocketAddr;
use std::rc::Rc;
use std::str::FromStr;

use deno_core::CancelFuture;
use deno_core::CancelHandle;
use deno_core::JsBuffer;
use deno_core::OpState;
use deno_core::RcRef;
use deno_core::Resource;
use deno_core::ResourceId;
use deno_core::op2;
use deno_permissions::PermissionsContainer;
use socket2::Domain;
use socket2::Protocol;
use socket2::Socket;
use socket2::Type;
use tokio::net::UdpSocket;

#[derive(Debug, thiserror::Error, deno_error::JsError)]
pub enum NodeUdpError {
  #[class(inherit)]
  #[error("{0}")]
  Io(#[from] std::io::Error),
  #[class(generic)]
  #[error("{0}")]
  AddrParse(#[from] std::net::AddrParseError),
  #[class(inherit)]
  #[error("{0}")]
  Resource(#[from] deno_core::error::ResourceError),
  #[class(inherit)]
  #[error("{0}")]
  Canceled(#[from] deno_core::Canceled),
  #[class(generic)]
  #[error("No resolved address found")]
  NoResolvedAddress,
  #[class(type)]
  #[error("Invalid hostname: '{0}'")]
  InvalidHostname(String),
  #[class(inherit)]
  #[error(transparent)]
  Permission(#[from] deno_permissions::PermissionCheckError),
}

pub struct NodeUdpSocketResource {
  pub socket: UdpSocket,
  pub cancel: CancelHandle,
}

impl Resource for NodeUdpSocketResource {
  fn name(&self) -> Cow<'_, str> {
    "nodeUdpSocket".into()
  }

  fn close(self: Rc<Self>) {
    self.cancel.cancel()
  }
}

#[op2]
#[serde]
pub fn op_node_udp_bind(
  state: &mut OpState,
  #[string] hostname: &str,
  #[smi] port: u16,
  reuse_address: bool,
  ipv6_only: bool,
) -> Result<(ResourceId, String, u16), NodeUdpError> {
  state
    .borrow_mut::<PermissionsContainer>()
    .check_net(&(hostname, Some(port)), "dgram.createSocket()")?;

  let addr = deno_net::resolve_addr::resolve_addr_sync(hostname, port)?
    .next()
    .ok_or(NodeUdpError::NoResolvedAddress)?;
  state
    .borrow_mut::<PermissionsContainer>()
    .check_net_resolved(&addr.ip(), addr.port(), "dgram.createSocket()")?;

  let domain = if addr.is_ipv4() {
    Domain::IPV4
  } else {
    Domain::IPV6
  };
  let sock = Socket::new(domain, Type::DGRAM, Some(Protocol::UDP))?;
  if reuse_address {
    #[cfg(any(
      target_os = "windows",
      target_os = "android",
      target_os = "linux"
    ))]
    sock.set_reuse_address(true)?;
    #[cfg(all(unix, not(any(target_os = "android", target_os = "linux"))))]
    sock.set_reuse_port(true)?;
  }
  if addr.is_ipv6() && ipv6_only {
    sock.set_only_v6(true)?;
  }
  let socket_addr = socket2::SockAddr::from(addr);
  sock.bind(&socket_addr)?;
  sock.set_nonblocking(true)?;

  let std_socket: std::net::UdpSocket = sock.into();
  let socket = UdpSocket::from_std(std_socket)?;
  let local_addr = socket.local_addr()?;

  let resource = NodeUdpSocketResource {
    socket,
    cancel: Default::default(),
  };
  let rid = state.resource_table.add(resource);

  Ok((rid, local_addr.ip().to_string(), local_addr.port()))
}

#[op2]
pub fn op_node_udp_join_multi_v4(
  state: &mut OpState,
  #[smi] rid: ResourceId,
  #[string] address: &str,
  #[string] multi_iface: Option<String>,
) -> Result<(), NodeUdpError> {
  let resource = state.resource_table.get::<NodeUdpSocketResource>(rid)?;

  let addr = Ipv4Addr::from_str(address)?;
  let iface = multi_iface
    .as_deref()
    .map(Ipv4Addr::from_str)
    .transpose()?
    .unwrap_or(Ipv4Addr::UNSPECIFIED);

  resource.socket.join_multicast_v4(addr, iface)?;
  Ok(())
}

#[op2]
pub fn op_node_udp_leave_multi_v4(
  state: &mut OpState,
  #[smi] rid: ResourceId,
  #[string] address: &str,
  #[string] multi_iface: Option<String>,
) -> Result<(), NodeUdpError> {
  let resource = state.resource_table.get::<NodeUdpSocketResource>(rid)?;

  let addr = Ipv4Addr::from_str(address)?;
  let iface = multi_iface
    .as_deref()
    .map(Ipv4Addr::from_str)
    .transpose()?
    .unwrap_or(Ipv4Addr::UNSPECIFIED);

  resource.socket.leave_multicast_v4(addr, iface)?;
  Ok(())
}

/// Resolve an IPv6 interface address to an interface index.
///
/// This mirrors libuv's `uv_ip6_addr()`, which Node.js uses to translate the
/// `multicastInterface` argument of `addMembership()`/`dropMembership()` into
/// an interface index (the `sin6_scope_id` of the parsed address):
///
/// - The interface is selected by the zone id following `%`. On Windows the
///   zone is a numeric index (`atoi`), while on Unix it is an interface *name*
///   resolved via `if_nametoindex()`.
/// - An unknown zone id resolves to index `0`, which the kernel treats as
///   "use the default interface". Node.js silently accepts such values rather
///   than failing, so `"::%12"` (no interface literally named `12`) joins on
///   the default interface instead of raising `EINVAL`.
///
/// As an extension over libuv, when no zone id is supplied we additionally try
/// to map a bare interface address (e.g. `"fe80::1"`) to its index by scanning
/// the local interfaces, falling back to the default interface.
fn resolve_ipv6_interface(
  interface_addr: Option<&str>,
) -> Result<u32, NodeUdpError> {
  let Some(addr_str) = interface_addr else {
    return Ok(0);
  };

  // Check if the address contains a zone ID (e.g. "fe80::1%eth0" or "::1%12")
  if let Some(zone_idx) = addr_str.find('%') {
    // The address part must be a valid IPv6 address (libuv calls
    // `uv_inet_pton` and reports `EINVAL` when it fails to parse).
    Ipv6Addr::from_str(&addr_str[..zone_idx])?;

    let zone_id = &addr_str[zone_idx + 1..];
    #[cfg(windows)]
    {
      // `atoi`-style parsing: a non-numeric zone yields the default interface.
      return Ok(zone_id.parse::<u32>().unwrap_or(0));
    }
    #[cfg(unix)]
    {
      use std::ffi::CString;
      // Resolve the zone as an interface name. `if_nametoindex` returns 0 for
      // unknown interfaces (including numeric strings that aren't names),
      // which is silently treated as the default interface.
      let idx = CString::new(zone_id)
        .ok()
        // SAFETY: if_nametoindex is safe to call with a valid C string
        .map(|name| unsafe { libc::if_nametoindex(name.as_ptr()) })
        .unwrap_or(0);
      return Ok(idx);
    }
    #[cfg(not(any(unix, windows)))]
    return Ok(0);
  }

  // No zone id was provided. Try to find interface by matching the address.
  #[cfg(unix)]
  {
    use std::ffi::CStr;
    let target_addr =
      Ipv6Addr::from_str(addr_str.split('%').next().unwrap_or(addr_str))?;

    // Get all interfaces and find one with matching address
    let mut addrs: *mut libc::ifaddrs = std::ptr::null_mut();
    // SAFETY: getifaddrs is safe to call with a valid pointer
    if unsafe { libc::getifaddrs(&mut addrs) } == 0 {
      let mut current = addrs;
      while !current.is_null() {
        // SAFETY: we checked current is not null
        let ifa = unsafe { &*current };
        if !ifa.ifa_addr.is_null() {
          // SAFETY: we checked ifa_addr is not null
          let family = unsafe { (*ifa.ifa_addr).sa_family };
          if family == libc::AF_INET6 as libc::sa_family_t {
            // SAFETY: we verified this is AF_INET6
            let sockaddr_in6 =
              unsafe { &*(ifa.ifa_addr as *const libc::sockaddr_in6) };
            let addr = Ipv6Addr::from(sockaddr_in6.sin6_addr.s6_addr);
            if addr == target_addr {
              // SAFETY: ifa_name is a valid C string
              let name = unsafe { CStr::from_ptr(ifa.ifa_name) };
              if let Ok(name_str) = name.to_str()
                && let Ok(c_name) = std::ffi::CString::new(name_str)
              {
                // SAFETY: if_nametoindex is safe with valid C string
                let idx = unsafe { libc::if_nametoindex(c_name.as_ptr()) };
                // SAFETY: freeifaddrs is safe to call with the pointer from getifaddrs
                unsafe { libc::freeifaddrs(addrs) };
                if idx != 0 {
                  return Ok(idx);
                }
              }
            }
          }
        }
        current = ifa.ifa_next;
      }
      // SAFETY: freeifaddrs is safe to call with the pointer from getifaddrs
      unsafe { libc::freeifaddrs(addrs) };
    }
  }

  // Default to 0 if we couldn't resolve
  Ok(0)
}

#[op2]
pub fn op_node_udp_join_multi_v6(
  state: &mut OpState,
  #[smi] rid: ResourceId,
  #[string] address: &str,
  #[string] interface_addr: Option<String>,
) -> Result<(), NodeUdpError> {
  let resource = state.resource_table.get::<NodeUdpSocketResource>(rid)?;

  let addr = Ipv6Addr::from_str(address)?;
  let iface = resolve_ipv6_interface(interface_addr.as_deref())?;
  resource.socket.join_multicast_v6(&addr, iface)?;
  Ok(())
}

#[op2]
pub fn op_node_udp_leave_multi_v6(
  state: &mut OpState,
  #[smi] rid: ResourceId,
  #[string] address: &str,
  #[string] interface_addr: Option<String>,
) -> Result<(), NodeUdpError> {
  let resource = state.resource_table.get::<NodeUdpSocketResource>(rid)?;

  let addr = Ipv6Addr::from_str(address)?;
  let iface = resolve_ipv6_interface(interface_addr.as_deref())?;
  resource.socket.leave_multicast_v6(&addr, iface)?;
  Ok(())
}

#[op2(fast)]
pub fn op_node_udp_set_broadcast(
  state: &mut OpState,
  #[smi] rid: ResourceId,
  on: bool,
) -> Result<(), NodeUdpError> {
  let resource = state.resource_table.get::<NodeUdpSocketResource>(rid)?;
  resource.socket.set_broadcast(on)?;
  Ok(())
}

#[op2(fast)]
pub fn op_node_udp_set_multicast_loopback(
  state: &mut OpState,
  #[smi] rid: ResourceId,
  is_v4: bool,
  on: bool,
) -> Result<(), NodeUdpError> {
  let resource = state.resource_table.get::<NodeUdpSocketResource>(rid)?;
  if is_v4 {
    resource.socket.set_multicast_loop_v4(on)?;
  } else {
    resource.socket.set_multicast_loop_v6(on)?;
  }
  Ok(())
}

#[op2(fast)]
pub fn op_node_udp_set_multicast_ttl(
  state: &mut OpState,
  #[smi] rid: ResourceId,
  #[smi] ttl: u32,
) -> Result<(), NodeUdpError> {
  let resource = state.resource_table.get::<NodeUdpSocketResource>(rid)?;
  resource.socket.set_multicast_ttl_v4(ttl)?;
  Ok(())
}

#[op2(fast)]
pub fn op_node_udp_set_ttl(
  state: &mut OpState,
  #[smi] rid: ResourceId,
  #[smi] ttl: u32,
) -> Result<(), NodeUdpError> {
  let resource = state.resource_table.get::<NodeUdpSocketResource>(rid)?;
  let sock_ref = socket2::SockRef::from(&resource.socket);
  sock_ref.set_ttl(ttl)?;
  Ok(())
}

#[op2(fast)]
pub fn op_node_udp_set_multicast_interface(
  state: &mut OpState,
  #[smi] rid: ResourceId,
  is_ipv6: bool,
  #[string] interface_address: &str,
) -> Result<(), NodeUdpError> {
  let resource = state.resource_table.get::<NodeUdpSocketResource>(rid)?;
  let sock_ref = socket2::SockRef::from(&resource.socket);
  if is_ipv6 {
    let index = ipv6_interface_index(interface_address)?;
    sock_ref.set_multicast_if_v6(index)?;
  } else {
    let addr: Ipv4Addr = interface_address.parse().map_err(|_| {
      NodeUdpError::Io(std::io::Error::new(
        std::io::ErrorKind::InvalidInput,
        "invalid IPv4 address",
      ))
    })?;
    sock_ref.set_multicast_if_v4(&addr)?;
  }
  Ok(())
}

/// Parse an IPv6 interface address string to a network interface index.
/// Matches libuv's `uv__udp_set_multicast_interface6` behavior:
/// - Parses IPv6 address strings like `::%lo0`, `::%1`, `::`
/// - Extracts scope_id and resolves interface names via if_nametoindex
/// - Returns EINVAL for empty or unparseable addresses
fn ipv6_interface_index(interface_address: &str) -> Result<u32, NodeUdpError> {
  let einval =
    || NodeUdpError::Io(std::io::Error::from_raw_os_error(libc::EINVAL));

  if interface_address.is_empty() {
    return Err(einval());
  }

  // Check for scope ID separator
  if let Some(pos) = interface_address.rfind('%') {
    // Validate the address part before % is a valid IPv6 address
    let addr_part = &interface_address[..pos];
    if addr_part.parse::<Ipv6Addr>().is_err() {
      return Err(einval());
    }

    let scope_id = &interface_address[pos + 1..];
    if scope_id.is_empty() {
      return Err(einval());
    }

    // Try numeric scope ID first
    if let Ok(index) = scope_id.parse::<u32>() {
      return Ok(index);
    }

    // Resolve interface name to index
    #[cfg(unix)]
    {
      let name = std::ffi::CString::new(scope_id).map_err(|_| einval())?;
      // SAFETY: name is a valid CString
      let index = unsafe { libc::if_nametoindex(name.as_ptr()) };
      // if_nametoindex returns 0 for unknown interfaces, which is
      // acceptable as "default selection" (matches libuv behavior)
      return Ok(index);
    }
    #[cfg(windows)]
    {
      let name = std::ffi::CString::new(scope_id).map_err(|_| einval())?;
      // SAFETY: name is a valid CString
      let index = unsafe {
        windows_sys::Win32::NetworkManagement::IpHelper::if_nametoindex(
          name.as_ptr() as *const u8,
        )
      };
      return Ok(index);
    }
    #[cfg(not(any(unix, windows)))]
    return Ok(0);
  }

  // No scope ID separator — try parsing as plain IPv6 address
  if interface_address.parse::<Ipv6Addr>().is_ok() {
    return Ok(0);
  }

  Err(einval())
}

fn source_specific_multicast(
  socket: &UdpSocket,
  source_addr: Ipv4Addr,
  group_addr: Ipv4Addr,
  interface_addr: Ipv4Addr,
  option: i32,
) -> Result<(), NodeUdpError> {
  #[cfg(unix)]
  {
    let mreq = libc::ip_mreq_source {
      imr_multiaddr: libc::in_addr {
        s_addr: u32::from(group_addr).to_be(),
      },
      imr_sourceaddr: libc::in_addr {
        s_addr: u32::from(source_addr).to_be(),
      },
      imr_interface: libc::in_addr {
        s_addr: u32::from(interface_addr).to_be(),
      },
    };

    // SAFETY: We pass a valid socket fd, level, option, and correctly-sized struct.
    let ret = unsafe {
      libc::setsockopt(
        std::os::fd::AsRawFd::as_raw_fd(socket),
        libc::IPPROTO_IP,
        option,
        &mreq as *const libc::ip_mreq_source as *const libc::c_void,
        std::mem::size_of::<libc::ip_mreq_source>() as libc::socklen_t,
      )
    };
    if ret != 0 {
      return Err(std::io::Error::last_os_error().into());
    }
  }

  #[cfg(windows)]
  {
    use std::os::windows::io::AsRawSocket;

    #[repr(C)]
    struct IpMreqSource {
      imr_multiaddr: u32,
      imr_sourceaddr: u32,
      imr_interface: u32,
    }

    let mreq = IpMreqSource {
      imr_multiaddr: u32::from(group_addr).to_be(),
      imr_sourceaddr: u32::from(source_addr).to_be(),
      imr_interface: u32::from(interface_addr).to_be(),
    };

    // SAFETY: We pass a valid socket, level, option, and correctly-sized struct.
    let ret = unsafe {
      windows_sys::Win32::Networking::WinSock::setsockopt(
        socket.as_raw_socket() as usize,
        windows_sys::Win32::Networking::WinSock::IPPROTO_IP,
        option,
        &mreq as *const IpMreqSource as *const u8,
        std::mem::size_of::<IpMreqSource>() as i32,
      )
    };
    if ret != 0 {
      return Err(std::io::Error::last_os_error().into());
    }
  }

  Ok(())
}

#[op2(fast)]
pub fn op_node_udp_join_source_specific(
  state: &mut OpState,
  #[smi] rid: ResourceId,
  #[string] source_address: &str,
  #[string] group_address: &str,
  #[string] interface_address: &str,
) -> Result<(), NodeUdpError> {
  let resource = state.resource_table.get::<NodeUdpSocketResource>(rid)?;

  let source_addr = Ipv4Addr::from_str(source_address)?;
  let group_addr = Ipv4Addr::from_str(group_address)?;
  let interface_addr = Ipv4Addr::from_str(interface_address)?;

  #[cfg(unix)]
  let option = libc::IP_ADD_SOURCE_MEMBERSHIP;
  #[cfg(windows)]
  let option =
    windows_sys::Win32::Networking::WinSock::IP_ADD_SOURCE_MEMBERSHIP;

  source_specific_multicast(
    &resource.socket,
    source_addr,
    group_addr,
    interface_addr,
    option,
  )
}

#[op2(fast)]
pub fn op_node_udp_leave_source_specific(
  state: &mut OpState,
  #[smi] rid: ResourceId,
  #[string] source_address: &str,
  #[string] group_address: &str,
  #[string] interface_address: &str,
) -> Result<(), NodeUdpError> {
  let resource = state.resource_table.get::<NodeUdpSocketResource>(rid)?;

  let source_addr = Ipv4Addr::from_str(source_address)?;
  let group_addr = Ipv4Addr::from_str(group_address)?;
  let interface_addr = Ipv4Addr::from_str(interface_address)?;

  #[cfg(unix)]
  let option = libc::IP_DROP_SOURCE_MEMBERSHIP;
  #[cfg(windows)]
  let option =
    windows_sys::Win32::Networking::WinSock::IP_DROP_SOURCE_MEMBERSHIP;

  source_specific_multicast(
    &resource.socket,
    source_addr,
    group_addr,
    interface_addr,
    option,
  )
}

#[op2]
#[smi]
pub async fn op_node_udp_send(
  state: Rc<RefCell<OpState>>,
  #[smi] rid: ResourceId,
  #[buffer] buf: JsBuffer,
  #[string] hostname: String,
  #[smi] port: u16,
) -> Result<usize, NodeUdpError> {
  {
    state
      .borrow_mut()
      .borrow_mut::<PermissionsContainer>()
      .check_net(&(&hostname, Some(port)), "socket.send()")?;
  }

  let resource = state
    .borrow()
    .resource_table
    .get::<NodeUdpSocketResource>(rid)?;

  let addr: SocketAddr =
    deno_net::resolve_addr::resolve_addr_sync(&hostname, port)?
      .next()
      .ok_or(NodeUdpError::NoResolvedAddress)?;
  {
    state
      .borrow_mut()
      .borrow_mut::<PermissionsContainer>()
      .check_net_resolved(&addr.ip(), addr.port(), "socket.send()")?;
  }

  let cancel = RcRef::map(&resource, |r| &r.cancel);
  let nwritten = resource
    .socket
    .send_to(&buf, &addr)
    .or_cancel(cancel)
    .await??;

  Ok(nwritten)
}

#[derive(serde::Serialize)]
pub struct RecvResult {
  pub nread: usize,
  pub hostname: String,
  pub port: u16,
}

#[op2]
#[serde]
pub async fn op_node_udp_recv(
  state: Rc<RefCell<OpState>>,
  #[smi] rid: ResourceId,
  #[buffer] mut buf: JsBuffer,
) -> Result<RecvResult, NodeUdpError> {
  let resource = state
    .borrow()
    .resource_table
    .get::<NodeUdpSocketResource>(rid)?;

  let cancel = RcRef::map(&resource, |r| &r.cancel);
  let (nread, remote_addr) = resource
    .socket
    .recv_from(&mut buf)
    .or_cancel(cancel)
    .await??;

  Ok(RecvResult {
    nread,
    hostname: remote_addr.ip().to_string(),
    port: remote_addr.port(),
  })
}

/// Return an owned dup of the bound UDP socket's file descriptor, for use as
/// the payload of an SCM_RIGHTS cmsg on an IPC channel.  The caller owns the
/// returned fd and must close it after `sendmsg` has attached it to the IPC
/// message.  Returns -1 on platforms that don't support fd-passing (Windows).
#[op2(fast)]
#[smi]
pub fn op_node_udp_fd_for_ipc(
  state: &mut OpState,
  #[smi] rid: ResourceId,
) -> Result<i32, NodeUdpError> {
  let resource = state.resource_table.get::<NodeUdpSocketResource>(rid)?;
  #[cfg(unix)]
  {
    use std::os::unix::io::AsRawFd;
    let fd = resource.socket.as_raw_fd();
    if fd < 0 {
      return Ok(-1);
    }
    // SAFETY: fd is a valid open file descriptor. F_DUPFD_CLOEXEC
    // atomically dups and sets CLOEXEC, avoiding a race window.
    let dup = unsafe { libc::fcntl(fd, libc::F_DUPFD_CLOEXEC, 0) };
    Ok(dup)
  }
  #[cfg(not(unix))]
  {
    let _ = resource;
    Ok(-1)
  }
}

/// Adopt an existing file descriptor as a UDP socket resource.  Used on the
/// receiving side of IPC handle passing.
#[op2]
#[serde]
pub fn op_node_udp_open(
  state: &mut OpState,
  #[smi] fd: i32,
) -> Result<(ResourceId, String, u16), NodeUdpError> {
  #[cfg(unix)]
  {
    use std::os::unix::io::FromRawFd;
    // SAFETY: The fd was received via SCM_RIGHTS and is a valid, open socket.
    let std_socket = unsafe { std::net::UdpSocket::from_raw_fd(fd) };
    std_socket.set_nonblocking(true)?;
    let local_addr = std_socket.local_addr()?;
    let socket = UdpSocket::from_std(std_socket)?;
    let resource = NodeUdpSocketResource {
      socket,
      cancel: Default::default(),
    };
    let rid = state.resource_table.add(resource);
    Ok((rid, local_addr.ip().to_string(), local_addr.port()))
  }
  #[cfg(not(unix))]
  {
    let _ = (state, fd);
    Err(NodeUdpError::Io(std::io::Error::new(
      std::io::ErrorKind::Unsupported,
      "UDP socket IPC handle passing is not supported on this platform",
    )))
  }
}
