//
// Syd: rock-solid application kernel
// src/proc.rs: DNS utilities
//
// Copyright (c) 2024, 2025 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

use std::{
    ffi::CStr,
    net::{IpAddr, SocketAddr, SocketAddrV4, SocketAddrV6, ToSocketAddrs},
    ptr,
};

use ahash::RandomState;
use indexmap::IndexSet;
use libc::{
    c_char, getnameinfo, socklen_t, AF_INET, AF_INET6, AF_UNSPEC, EAI_AGAIN, EAI_BADFLAGS,
    EAI_FAIL, EAI_FAMILY, EAI_MEMORY, EAI_NONAME, EAI_SERVICE, EAI_SOCKTYPE, EAI_SYSTEM,
    NI_MAXHOST, NI_NAMEREQD, NI_NUMERICSERV,
};
use nix::{
    errno::Errno,
    sys::socket::{SockaddrLike, SockaddrStorage},
};

use crate::{err::err2no, fs::fillrandom};

/// Resolves a hostname to a single IP address.
/// In case of multiple responses, an IP is selected randomly.
/// Randomness is provided by getrandom(2).
pub fn resolve_rand(name: &str, family: Option<i32>) -> Result<IpAddr, Errno> {
    // Read random bytes with getrandom(2) and convert to usize.
    // Note, getrandom(2) is soon to be in the VDSO!
    let mut buf = [0u8; 4];
    if fillrandom(&mut buf).is_err() {
        return Err(Errno::EIO); // Input/output error.
    }
    // Convert bytes to a usize.
    let cookie = usize::try_from(u32::from_ne_bytes(buf)).unwrap_or(usize::MAX);

    // Resolve hostname.
    let addrs = resolve_host(name, family)?;

    // Select a random IP address from the list.
    // Calculate random index within the bounds of the addresses vector.
    #[allow(clippy::arithmetic_side_effects)]
    Ok(addrs[cookie.wrapping_rem(addrs.len())])
}

/// Resolves a hostname using the system DNS resolver.
pub fn resolve_host(name: &str, family: Option<i32>) -> Result<Vec<IpAddr>, Errno> {
    let ai_family = match family {
        Some(AF_INET) => AF_INET,
        Some(AF_INET6) => AF_INET6,
        Some(_) => return Err(Errno::EINVAL),
        None => AF_UNSPEC, // Allow IPv4 or IPv6.
    };

    // Create an IndexSet to store unique IPs
    // while preserving insertion order.
    let addrs: IndexSet<IpAddr, RandomState> = IndexSet::from_iter(
        (name, 22)
            .to_socket_addrs()
            .map_err(|err| err2no(&err))?
            .filter(|addr| {
                matches!(
                    (ai_family, addr),
                    (AF_UNSPEC, _) | (AF_INET, SocketAddr::V4(_)) | (AF_INET6, SocketAddr::V6(_))
                )
            })
            .map(|addr| addr.ip()),
    );

    if addrs.is_empty() {
        // No addresses were found.
        Err(Errno::ENOENT)
    } else {
        Ok(addrs.iter().copied().collect())
    }
}

/// Performs a reverse DNS lookup for the given IP address, returning a hostname or an error.
#[allow(clippy::cast_possible_truncation)]
pub fn lookup_addr(addr: IpAddr) -> Result<String, Errno> {
    let addr = match addr {
        IpAddr::V4(v4) => SockaddrStorage::from(SocketAddrV4::new(v4, 0)),
        IpAddr::V6(v6) => SockaddrStorage::from(SocketAddrV6::new(v6, 0, 0, 0)),
    };
    let mut host_buf = [0 as c_char; NI_MAXHOST as usize];

    // SAFETY: We call a system function (getnameinfo) with valid pointers for the address
    // and buffer, and we check the return value to ensure success before using `host_buf`.
    let ret = unsafe {
        getnameinfo(
            addr.as_ptr(),
            addr.len(),
            host_buf.as_mut_ptr(),
            host_buf.len() as socklen_t,
            ptr::null_mut(),
            0,
            NI_NAMEREQD | NI_NUMERICSERV,
        )
    };

    if ret != 0 {
        if ret == EAI_SYSTEM {
            return Err(Errno::last());
        } else {
            let e = match ret {
                EAI_AGAIN => Errno::EAGAIN,
                EAI_BADFLAGS => Errno::EINVAL,
                EAI_FAIL => Errno::EIO,
                EAI_FAMILY => Errno::EAFNOSUPPORT,
                EAI_MEMORY => Errno::ENOMEM,
                EAI_NONAME => Errno::ENOENT,
                EAI_SERVICE => Errno::EPROTONOSUPPORT,
                EAI_SOCKTYPE => Errno::ESOCKTNOSUPPORT,
                _ => Errno::EIO,
            };
            return Err(e);
        }
    }

    // SAFETY: On success, `host_buf` contains a valid null-terminated string.
    let cstr = unsafe { CStr::from_ptr(host_buf.as_ptr()) };
    let name = cstr.to_string_lossy().into_owned();
    Ok(name)
}
