//
// Syd: rock-solid unikernel
// src/compat.rs: Compatibility code for different libcs
//
// Copyright (c) 2023, 2024 Ali Polatel <alip@chesswob.org>
// waitid and WaitStatus are based in part upon nix which is:
//     Copyright (c) nix authors
//     SPDX-License-Identifier: MIT
//
// SPDX-License-Identifier: GPL-3.0-or-later

#![allow(non_camel_case_types)]

// SAFETY: We want to allocate the getdents file-list on the heap, hence PathBuf.
#[allow(clippy::disallowed_types)]
use std::{
    ffi::{OsStr, OsString},
    os::{
        fd::{AsFd, AsRawFd, RawFd},
        unix::ffi::OsStrExt,
    },
    path::{Path, PathBuf},
};

use nix::{
    errno::Errno,
    fcntl::OFlag,
    sys::{
        epoll::{Epoll, EpollEvent},
        socket::SockaddrLike,
        wait::{Id, WaitPidFlag, WaitStatus as NixWaitStatus},
    },
    unistd::Pid,
    NixPath,
};
use nonempty::NonEmpty;
use serde::{ser::SerializeMap, Serialize, Serializer};

use crate::IoctlRequest;

#[repr(C)]
pub(crate) struct TimeSpec64 {
    pub(crate) tv_sec: i64,
    pub(crate) tv_nsec: i64,
}

#[repr(C)]
pub(crate) struct TimeSpec32 {
    pub(crate) tv_sec: i32,
    pub(crate) tv_nsec: i32,
}

/// Aligns the given length to the nearest 4-byte boundary.
///
/// This function is useful for ensuring that data structures in a 32-bit architecture
/// are correctly aligned to 4 bytes as required by many system interfaces for proper operation.
///
/// # Arguments
///
/// * `len` - The length that needs to be aligned.
///
/// # Returns
///
/// The smallest length that is a multiple of 4 bytes and is not less than `len`.
pub(crate) const fn cmsg_align_32(len: usize) -> usize {
    len.saturating_add(3) & !3
}

/// Calculates the total space required for a control message including data and padding on a 32-bit system.
///
/// This function is specifically tailored for 32-bit architectures where control message
/// headers and data need to be aligned to 4-byte boundaries.
///
/// # Arguments
///
/// * `length` - The length of the data in the control message.
///
/// # Returns
///
/// The total space in bytes required to store the control message, ensuring proper alignment.
pub(crate) const fn cmsg_space_32(length: u32) -> usize {
    cmsg_align_32((length as usize).saturating_add(cmsg_align_32(std::mem::size_of::<cmsghdr32>())))
}

/// Computes the byte length of a control message's header and data for a 32-bit system.
///
/// This function helps in determining the correct length for control messages where the
/// header and data must be aligned to 4-byte boundaries in a 32-bit architecture.
///
/// # Arguments
///
/// * `length` - The length of the data part of the control message.
///
/// # Returns
///
/// The combined length of the control message header and the data, aligned as required.
pub(crate) const fn cmsg_len_32(length: u32) -> usize {
    cmsg_align_32(std::mem::size_of::<cmsghdr32>()).saturating_add(length as usize)
}

/// This structure represents the Linux 32-bit data structure 'struct stat'
#[repr(C)]
pub struct stat32 {
    /// Device ID.
    pub st_dev: nix::libc::dev_t,

    /// Inode number (32-bit).
    pub st_ino: u32,

    /// Number of hard links.
    pub st_nlink: nix::libc::nlink_t,

    /// File mode.
    pub st_mode: nix::libc::mode_t,

    /// User ID of owner.
    pub st_uid: nix::libc::uid_t,

    /// Group ID of owner.
    pub st_gid: nix::libc::gid_t,

    /// Padding.
    __pad0: nix::libc::c_int,

    /// Device ID (if special file).
    pub st_rdev: nix::libc::dev_t,

    /// Total size, in bytes (32-bit).
    pub st_size: i32,

    /// Block size for filesystem I/O.
    pub st_blksize: nix::libc::blksize_t,

    /// Number of 512B blocks allocated (32-bit).
    pub st_blocks: i32,

    /// Time of last access (32-bit).
    pub st_atime: i32,

    /// Nanoseconds of last access (32-bit).
    pub st_atime_nsec: i32,

    /// Time of last modification (32-bit).
    pub st_mtime: i32,

    /// Nanoseconds of last modification (32-bit).
    pub st_mtime_nsec: i32,

    /// Time of last status change (32-bit).
    pub st_ctime: i32,

    /// Nanoseconds of last status change (32-bit).
    pub st_ctime_nsec: i32,

    /// Reserved for future use (32-bit).
    __unused: [i32; 3],
}

impl From<nix::libc::stat64> for stat32 {
    #[allow(clippy::cast_possible_truncation)]
    fn from(stat: nix::libc::stat64) -> Self {
        Self {
            st_dev: stat.st_dev,
            st_ino: stat.st_ino as u32,
            st_nlink: stat.st_nlink,
            st_mode: stat.st_mode,
            st_uid: stat.st_uid,
            st_gid: stat.st_gid,
            __pad0: 0,
            st_rdev: stat.st_rdev,
            st_size: stat.st_size as i32,
            st_blksize: stat.st_blksize,
            st_blocks: stat.st_blocks as i32,
            st_atime: stat.st_atime as i32,
            st_atime_nsec: stat.st_atime_nsec as i32,
            st_mtime: stat.st_mtime as i32,
            st_mtime_nsec: stat.st_mtime_nsec as i32,
            st_ctime: stat.st_ctime as i32,
            st_ctime_nsec: stat.st_ctime_nsec as i32,
            __unused: [0; 3],
        }
    }
}

/// This structure represents the Linux 32-bit data structure 'struct iovec'
#[repr(C)]
pub struct iovec32 {
    iov_base: u32,
    iov_len: u32,
}

impl From<iovec32> for nix::libc::iovec {
    fn from(src: iovec32) -> Self {
        nix::libc::iovec {
            iov_base: src.iov_base as *mut _,
            iov_len: src.iov_len as usize,
        }
    }
}

/// This structure represents the Linux 32-bit data structure 'struct mmmsghdr'
#[derive(Copy, Clone)]
#[repr(C)]
pub struct mmsghdr32 {
    pub msg_hdr: msghdr32,
    pub msg_len: u32,
}

/// This structure represents the Linux native data structure 'struct mmsghdr'
#[derive(Copy, Clone)]
#[repr(C)]
pub struct mmsghdr {
    pub msg_hdr: msghdr,
    pub msg_len: nix::libc::c_uint,
}

impl From<mmsghdr32> for mmsghdr {
    fn from(src: mmsghdr32) -> Self {
        mmsghdr {
            msg_hdr: msghdr::from(src.msg_hdr),
            msg_len: src.msg_len,
        }
    }
}

impl From<mmsghdr> for mmsghdr32 {
    fn from(src: mmsghdr) -> Self {
        mmsghdr32 {
            msg_hdr: msghdr32::from(src.msg_hdr),
            msg_len: src.msg_len,
        }
    }
}

/// This structure represents the Linux 32-bit data structure 'struct cmsghdr'
#[repr(C)]
pub struct cmsghdr32 {
    pub cmsg_len: u32,
    pub cmsg_level: i32,
    pub cmsg_type: i32,
}

/// This structure represents the Linux native data structure 'struct cmsghdr'
#[repr(C)]
pub struct cmsghdr {
    pub cmsg_len: nix::libc::size_t,
    pub cmsg_level: nix::libc::c_int,
    pub cmsg_type: nix::libc::c_int,
}

impl From<cmsghdr32> for cmsghdr {
    fn from(src: cmsghdr32) -> Self {
        cmsghdr {
            cmsg_len: src.cmsg_len as nix::libc::size_t,
            cmsg_level: src.cmsg_level,
            cmsg_type: src.cmsg_type,
        }
    }
}

/// This structure represents the Linux 32-bit data structure 'struct msghdr'
#[derive(Copy, Clone)]
#[repr(C)]
pub struct msghdr32 {
    // In 32-bit systems, pointers are 32-bit.
    pub msg_name: u32,       // Use u32 to represent a 32-bit pointer.
    pub msg_namelen: u32,    // socklen_t is typically 32-bit.
    pub msg_iov: u32,        // Use u32 to represent a 32-bit pointer to iovec.
    pub msg_iovlen: u32,     // size_t is 32-bit on 32-bit systems.
    pub msg_control: u32,    // Use u32 to represent a 32-bit pointer.
    pub msg_controllen: u32, // size_t is 32-bit on 32-bit systems.
    pub msg_flags: i32,      // c_int remains the same (32-bit).
}

/// This structure represents the Linux native data structure 'struct msghdr'
#[derive(Copy, Clone)]
#[repr(C)]
pub struct msghdr {
    pub msg_name: *mut nix::libc::c_void,
    pub msg_namelen: nix::libc::socklen_t,
    pub msg_iov: *mut nix::libc::iovec,
    pub msg_iovlen: nix::libc::size_t,
    pub msg_control: *mut nix::libc::c_void,
    pub msg_controllen: nix::libc::size_t,
    pub msg_flags: nix::libc::c_int,
}

impl From<msghdr32> for msghdr {
    fn from(msg: msghdr32) -> Self {
        msghdr {
            msg_name: msg.msg_name as *mut nix::libc::c_void,
            msg_namelen: msg.msg_namelen as nix::libc::socklen_t,
            msg_iov: msg.msg_iov as *mut nix::libc::iovec,
            msg_iovlen: msg.msg_iovlen as nix::libc::size_t,
            msg_control: msg.msg_control as *mut nix::libc::c_void,
            msg_controllen: msg.msg_controllen as nix::libc::size_t,
            msg_flags: msg.msg_flags as nix::libc::c_int,
        }
    }
}

#[allow(clippy::unnecessary_cast)]
#[allow(clippy::cast_possible_truncation)]
impl From<msghdr> for msghdr32 {
    fn from(msg: msghdr) -> Self {
        msghdr32 {
            msg_name: msg.msg_name as u32,
            msg_namelen: msg.msg_namelen as u32,
            msg_iov: msg.msg_iov as u32,
            msg_iovlen: msg.msg_iovlen as u32,
            msg_control: msg.msg_control as u32,
            msg_controllen: msg.msg_controllen as u32,
            msg_flags: msg.msg_flags as i32,
        }
    }
}

/// This structure represents the Linux data structure `struct statx_timestamp`
#[repr(C)]
#[derive(Copy, Clone)]
pub struct FileStatxTimestamp {
    pub tv_sec: i64,
    pub tv_nsec: u32,
    __statx_timestamp_pad1: [i32; 1],
}

/// This structure represents the Linux data structure `struct statx`
#[repr(C)]
pub struct FileStatx {
    pub stx_mask: u32,   // What results were written [uncond]
    stx_blksize: u32,    // Preferred general I/O size [uncond]
    stx_attributes: u64, // Flags conveying information about the file [uncond]

    stx_nlink: u32,    // Number of hard links
    pub stx_uid: u32,  // User ID of owner
    pub stx_gid: u32,  // Group ID of owner
    pub stx_mode: u16, // File mode
    __statx_pad1: [u16; 1],

    pub stx_ino: u64,         // Inode number
    pub stx_size: u64,        // File size
    stx_blocks: u64,          // Number of 512-byte blocks allocated
    stx_attributes_mask: u64, // Mask to show what's supported in stx_attributes

    pub stx_atime: FileStatxTimestamp, // Last access time
    stx_btime: FileStatxTimestamp,     // File creation time
    pub stx_ctime: FileStatxTimestamp, // Last attribute change time
    pub stx_mtime: FileStatxTimestamp, // Last data modification time

    stx_rdev_major: u32, // Device ID of special file [if bdev/cdev]
    stx_rdev_minor: u32,
    pub stx_dev_major: u32, // ID of device containing file [uncond]
    pub stx_dev_minor: u32,

    stx_mnt_id: u64,
    stx_dio_mem_align: u32,    // Memory buffer alignment for direct I/O
    stx_dio_offset_align: u32, // File offset alignment for direct I/O

    __statx_pad2: [u64; 12], // Spare space for future expansion
}

/// This struct represents a directory entry.
#[derive(Clone)]
pub(crate) struct DirEntry {
    /// The `dirent64` structure.
    pub(crate) dirent: Box<[u8]>,

    /// Size of the file name, in bytes.
    pub(crate) namelen: usize,
}

impl std::fmt::Debug for DirEntry {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_tuple("DirEntry").field(&self.name()).finish()
    }
}

impl DirEntry {
    /// Return a `PathBuf` representing the name of the directory entry.
    // SAFETY: We want to allocate the file-list on the heap, hence PathBuf.
    #[allow(clippy::disallowed_types)]
    pub fn to_path_buf(&self) -> PathBuf {
        Path::new(&self.name()).to_path_buf()
    }

    /// Return the name of the directory entry.
    pub fn name(&self) -> OsString {
        // SAFETY: We trust self.dirent points to a valid `dirent64` structure.
        OsStr::from_bytes(unsafe { self.name_bytes() }).to_os_string()
    }

    unsafe fn name_bytes(&self) -> &[u8] {
        let dirent: *const nix::libc::dirent64 = Box::as_ref(&self.dirent).as_ptr().cast();
        let d_name: *const nix::libc::c_char = (*dirent).d_name.as_ptr();
        std::slice::from_raw_parts(d_name.cast(), self.namelen)
    }
}

/// Represents a list of directory entries as returned by `getdents64` system call.
pub(crate) type Directory = NonEmpty<DirEntry>;

/// Retrieve directory entries from an open directory file descriptor.
///
/// # Parameters
///
/// - `fd`: The open directory file descriptor.
///
/// # Returns
///
/// `Some(Directory)` containing the directory entries,
/// `None` when there are no more entries.
///
/// # Safety
///
/// This function calls the `getdents64` system call directly which is an unsafe function.
/// Ensure that `fd` is a valid open directory file descriptor to avoid undefined behavior.
pub(crate) fn getdents(fd: RawFd, count: usize) -> Result<Option<Directory>, Errno> {
    const DIRENT_SIZE: usize = std::mem::size_of::<nix::libc::dirent64>();
    const DIRENT_ALGN: usize = std::mem::align_of::<nix::libc::dirent64>();

    let layout = std::alloc::Layout::from_size_align(
        count.checked_mul(DIRENT_SIZE).ok_or(Errno::EINVAL)?,
        DIRENT_ALGN,
    )
    .or(Err(Errno::EINVAL))?;
    // SAFETY: Allocate buffer with proper alignment and size.
    let buffer = unsafe { std::alloc::alloc(layout) };
    if buffer.is_null() {
        // Yes EINVAL, getdents64 does not return ENOMEM!
        return Err(Errno::EINVAL);
    }

    let size = loop {
        // SAFETY: In libc, we trust.
        match unsafe { getdents64(fd, buffer.cast(), count) } {
            0 => {
                // SAFETY: Deallocate buffer.
                unsafe { std::alloc::dealloc(buffer, layout) };
                return Ok(None);
            }
            error if error < 0 => {
                #[allow(clippy::arithmetic_side_effects)]
                #[allow(clippy::cast_possible_truncation)]
                let error = -error as i32;
                let errrt = match error {
                    nix::libc::EINTR => continue,
                    nix::libc::ENOENT => Ok(None),
                    _ => Err(Errno::from_raw(error)),
                };
                // SAFETY: Deallocate buffer.
                unsafe { std::alloc::dealloc(buffer, layout) };
                return errrt;
            }
            size =>
            {
                #[allow(clippy::cast_possible_truncation)]
                #[allow(clippy::cast_sign_loss)]
                break size as usize
            }
        }
    };

    // Parsing the buffer to extract DirEntry structures.
    let mut entries = Vec::new();
    if entries.try_reserve(count).is_err() {
        // SAFETY: Returning error after advancing the dirfd pointer.
        // Hopefully this is rare enough in practise, and when this
        // OOMs you have bigger things to worry about.
        unsafe { std::alloc::dealloc(buffer, layout) };
        return Err(Errno::EINVAL);
    }

    let mut offset = 0;
    while offset < size {
        // SAFETY: In kernel, we trust.
        unsafe {
            // SAFETY: The `buffer` is explicitly allocated with
            // `Layout::from_size_align` which ensures that the memory
            // alignment matches the alignment requirements of
            // `libc::dirent64`. Therefore, the pointer returned by
            // `alloc` is guaranteed to be suitably aligned for
            // `libc::dirent64` objects. The `offset` used in
            // `buffer.add(offset)` is calculated based on the
            // `d_reclen` field from previously read `libc::dirent64`
            // entries, which aligns with the size of these entries. As
            // each `d_reclen` includes any necessary padding to
            // maintain alignment requirements, incrementing the pointer
            // by this offset maintains proper alignment. This makes the
            // casting operation to `*mut libc::dirent64` safe. We
            // ensure that the `offset` does not exceed the buffer size
            // to prevent out-of-bounds access.
            #[allow(clippy::cast_ptr_alignment)]
            let dirent = buffer.add(offset).cast::<nix::libc::dirent64>();
            let d_reclen = (*dirent).d_reclen as usize;

            // Copy the bytes of the `dirent64` structure from the
            // buffer to a memory owned by `DirEntry`.
            let namelen = nix::libc::strlen((*dirent).d_name.as_ptr());
            let dirent = std::slice::from_raw_parts(dirent.cast(), d_reclen)
                .to_owned()
                .into_boxed_slice();

            entries.push(DirEntry { dirent, namelen });
            offset = offset.saturating_add(d_reclen);
        }
    }

    // SAFETY: Deallocate buffer.
    unsafe { std::alloc::dealloc(buffer, layout) };

    if entries.is_empty() {
        Ok(None)
    } else {
        #[allow(clippy::disallowed_methods)]
        Ok(Some(NonEmpty::from_vec(entries).unwrap()))
    }
}

/// Wrapper for the `getdents64` syscall
#[inline(always)]
unsafe fn getdents64(
    fd: nix::libc::c_int,
    buf: *mut nix::libc::c_void,
    bytes: nix::libc::size_t,
) -> nix::libc::c_long {
    nix::libc::syscall(nix::libc::SYS_getdents64, fd, buf, bytes)
}

/// WaitStatus with support for signals that nix' Signal type don't support.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum WaitStatus {
    Exited(Pid, i32),
    Signaled(Pid, i32, bool),
    Stopped(Pid, i32),
    PtraceEvent(Pid, i32, nix::libc::c_int),
    PtraceSyscall(Pid),
    Continued(Pid),
    StillAlive,
}

impl From<NixWaitStatus> for WaitStatus {
    fn from(status: NixWaitStatus) -> Self {
        match status {
            NixWaitStatus::Exited(pid, code) => WaitStatus::Exited(pid, code),
            NixWaitStatus::Signaled(pid, signal, core_dump) => {
                WaitStatus::Signaled(pid, signal as i32, core_dump)
            }
            NixWaitStatus::Stopped(pid, signal) => WaitStatus::Stopped(pid, signal as i32),
            NixWaitStatus::PtraceEvent(pid, signal, event) => {
                WaitStatus::PtraceEvent(pid, signal as i32, event)
            }
            NixWaitStatus::PtraceSyscall(pid) => WaitStatus::PtraceSyscall(pid),
            NixWaitStatus::Continued(pid) => WaitStatus::Continued(pid),
            NixWaitStatus::StillAlive => WaitStatus::StillAlive,
        }
    }
}

fn exited(status: i32) -> bool {
    nix::libc::WIFEXITED(status)
}

fn exit_status(status: i32) -> i32 {
    nix::libc::WEXITSTATUS(status)
}

fn signaled(status: i32) -> bool {
    nix::libc::WIFSIGNALED(status)
}

fn term_signal(status: i32) -> i32 {
    nix::libc::WTERMSIG(status)
}

fn dumped_core(status: i32) -> bool {
    nix::libc::WCOREDUMP(status)
}

fn stopped(status: i32) -> bool {
    nix::libc::WIFSTOPPED(status)
}

fn stop_signal(status: i32) -> i32 {
    nix::libc::WSTOPSIG(status)
}

fn syscall_stop(status: i32) -> bool {
    // From ptrace(2), setting PTRACE_O_TRACESYSGOOD has the effect
    // of delivering SIGTRAP | 0x80 as the signal number for syscall
    // stops. This allows easily distinguishing syscall stops from
    // genuine SIGTRAP signals.
    nix::libc::WSTOPSIG(status) == libc::SIGTRAP | 0x80
}

fn stop_additional(status: i32) -> nix::libc::c_int {
    (status >> 16) as nix::libc::c_int
}

fn continued(status: i32) -> bool {
    nix::libc::WIFCONTINUED(status)
}

impl WaitStatus {
    pub(crate) fn from_raw(pid: Pid, status: i32) -> WaitStatus {
        if exited(status) {
            WaitStatus::Exited(pid, exit_status(status))
        } else if signaled(status) {
            WaitStatus::Signaled(pid, term_signal(status), dumped_core(status))
        } else if stopped(status) {
            let status_additional = stop_additional(status);
            if syscall_stop(status) {
                WaitStatus::PtraceSyscall(pid)
            } else if status_additional == 0 {
                WaitStatus::Stopped(pid, stop_signal(status))
            } else {
                WaitStatus::PtraceEvent(pid, stop_signal(status), stop_additional(status))
            }
        } else {
            assert!(continued(status));
            WaitStatus::Continued(pid)
        }
    }
}

/// Wrapper for the `waitid` syscall
/// This is identical to nix' waitid except we use our custom WaitStatus.
pub fn waitid(id: Id, flags: WaitPidFlag) -> Result<WaitStatus, Errno> {
    #[allow(clippy::cast_sign_loss)]
    let (idtype, idval) = match id {
        Id::All => (nix::libc::P_ALL, 0),
        Id::Pid(pid) => (nix::libc::P_PID, pid.as_raw() as nix::libc::id_t),
        Id::PGid(pid) => (nix::libc::P_PGID, pid.as_raw() as nix::libc::id_t),
        Id::PIDFd(fd) => (nix::libc::P_PIDFD, fd.as_raw_fd() as nix::libc::id_t),
        _ => unreachable!(),
    };

    // SAFETY: In libc, we trust.
    let siginfo = unsafe {
        // Memory is zeroed rather than uninitialized, as not all platforms
        // initialize the memory in the StillAlive case
        let mut siginfo: libc::siginfo_t = std::mem::zeroed();
        Errno::result(nix::libc::waitid(idtype, idval, &mut siginfo, flags.bits()))?;
        siginfo
    };

    // SAFETY: In libc, we trust.
    let si_pid = unsafe { siginfo.si_pid() };
    if si_pid == 0 {
        return Ok(WaitStatus::StillAlive);
    }

    assert_eq!(siginfo.si_signo, nix::libc::SIGCHLD);

    let pid = Pid::from_raw(si_pid);
    // SAFETY: In libc, we trust.
    let si_status = unsafe { siginfo.si_status() };

    let status = match siginfo.si_code {
        nix::libc::CLD_EXITED => WaitStatus::Exited(pid, si_status),
        nix::libc::CLD_KILLED | nix::libc::CLD_DUMPED => {
            WaitStatus::Signaled(pid, si_status, siginfo.si_code == nix::libc::CLD_DUMPED)
        }
        nix::libc::CLD_STOPPED => WaitStatus::Stopped(pid, si_status),
        nix::libc::CLD_CONTINUED => WaitStatus::Continued(pid),
        nix::libc::CLD_TRAPPED => {
            if si_status == nix::libc::SIGTRAP | 0x80 {
                WaitStatus::PtraceSyscall(pid)
            } else {
                WaitStatus::PtraceEvent(pid, si_status & 0xff, (si_status >> 8) as nix::libc::c_int)
            }
        }
        _ => return Err(Errno::EINVAL),
    };

    Ok(status)
}

pub(crate) fn pipe2_raw(flags: OFlag) -> Result<(RawFd, RawFd), Errno> {
    let mut fds = std::mem::MaybeUninit::<[RawFd; 2]>::uninit();

    // SAFETY: We use this when nix' version which returns an OwnedFd
    // does not work for our purposes e.g. in mini-threads spawned
    // by network syscall handlers.
    let res = unsafe { nix::libc::pipe2(fds.as_mut_ptr().cast(), flags.bits()) };

    Errno::result(res)?;

    // SAFETY: pipe2 returns a valid array of fds.
    let [read, write] = unsafe { fds.assume_init() };
    Ok((read, write))
}

#[allow(clippy::cast_possible_truncation)]
pub(crate) const PF_UNIX: nix::libc::sa_family_t = nix::libc::AF_UNIX as nix::libc::sa_family_t;
#[allow(clippy::cast_possible_truncation)]
pub(crate) const PF_INET: nix::libc::sa_family_t = nix::libc::AF_INET as nix::libc::sa_family_t;
#[allow(clippy::cast_possible_truncation)]
pub(crate) const PF_INET6: nix::libc::sa_family_t = nix::libc::AF_INET6 as nix::libc::sa_family_t;
#[allow(clippy::cast_possible_truncation)]
pub(crate) const PF_ALG: nix::libc::sa_family_t = nix::libc::AF_ALG as nix::libc::sa_family_t;
#[allow(clippy::cast_possible_truncation)]
pub(crate) const PF_NETLINK: nix::libc::sa_family_t =
    nix::libc::AF_NETLINK as nix::libc::sa_family_t;

/// nix' SockAddrLike.family() function does not support AF_ALG.
/// This is a workaround that accesses the underlying sockaddr directly.
pub(crate) fn addr_family<T: SockaddrLike>(addr: &T) -> nix::libc::sa_family_t {
    // SAFETY: This is safe as long as addr.as_ptr() returns a valid pointer to a sockaddr.
    unsafe { (*addr.as_ptr()).sa_family }
}

/// Safe ftruncate64() wrapper.
pub fn ftruncate64(fd: RawFd, len: nix::libc::off64_t) -> Result<(), Errno> {
    // SAFETY: nix does not have a ftruncate64 wrapper.
    Errno::result(unsafe { nix::libc::ftruncate64(fd, len) }).map(drop)
}

/// Want/got stx_mode & S_IFMT.
pub const STATX_TYPE: nix::libc::c_uint = 0x00000001;
/// Want/got stx_mode & ~S_IFMT.
pub const STATX_MODE: nix::libc::c_uint = 0x00000002;
/// Want/got stx_uid.
pub const STATX_UID: nix::libc::c_uint = 0x00000008;
/// Want/got stx_gid.
pub const STATX_GID: nix::libc::c_uint = 0x00000010;
/// Want/got stx_ino.
pub const STATX_INO: nix::libc::c_uint = 0x00000100;
/// Want/got stx_size.
pub const STATX_SIZE: nix::libc::c_uint = 0x00000200;
/// Want all the basic stat information.
pub const STATX_BASIC_STATS: nix::libc::c_uint = 0x000007ff;

/// Safe statx() wrapper.
pub fn statx<P: ?Sized + NixPath>(
    dirfd: Option<RawFd>,
    pathname: &P,
    flags: nix::libc::c_uint,
    mask: nix::libc::c_uint,
) -> Result<FileStatx, Errno> {
    let dirfd = dirfd.unwrap_or(nix::libc::AT_FDCWD);
    let mut dst = std::mem::MaybeUninit::uninit();

    // SAFETY: Neither nix nor libc has a wrapper for statx.
    Errno::result(pathname.with_nix_path(|cstr| unsafe {
        nix::libc::syscall(
            nix::libc::SYS_statx,
            dirfd,
            cstr.as_ptr(),
            flags,
            mask,
            dst.as_mut_ptr(),
        )
    })?)?;

    // SAFETY: statx returned success.
    Ok(unsafe { dst.assume_init() })
}

/// Safe statx() wrapper to use with a FD only.
pub fn fstatx(fd: RawFd, mask: nix::libc::c_uint) -> Result<FileStatx, Errno> {
    let mut dst = std::mem::MaybeUninit::uninit();

    // SAFETY: Neither nix nor libc has a wrapper for statx.
    Errno::result(unsafe {
        nix::libc::syscall(
            nix::libc::SYS_statx,
            fd,
            b"\0".as_ptr(),
            nix::libc::AT_EMPTY_PATH,
            mask,
            dst.as_mut_ptr(),
        )
    })?;

    // SAFETY: statx returned success.
    Ok(unsafe { dst.assume_init() })
}

/// Wrapper for struct stat64.
pub(crate) use nix::libc::stat64 as FileStat64;

pub(crate) fn fstatat64<P: ?Sized + NixPath>(
    dirfd: Option<RawFd>,
    pathname: &P,
    flags: nix::libc::c_int,
) -> Result<FileStat64, Errno> {
    let dirfd = dirfd.unwrap_or(nix::libc::AT_FDCWD);
    let mut dst = std::mem::MaybeUninit::uninit();

    // SAFETY: nix does not has a wrapper for fstatat64.
    Errno::result(pathname.with_nix_path(|cstr| unsafe {
        nix::libc::fstatat64(dirfd, cstr.as_ptr(), dst.as_mut_ptr(), flags)
    })?)?;

    // SAFETY: fstatat64 returned success.
    Ok(unsafe { dst.assume_init() })
}

#[cfg(target_os = "freebsd")]
type fs_type_t = u32;
#[cfg(target_os = "android")]
type fs_type_t = nix::libc::c_ulong;
#[cfg(all(target_os = "linux", target_arch = "s390x", not(target_env = "musl")))]
type fs_type_t = nix::libc::c_uint;
#[cfg(all(target_os = "linux", target_env = "musl"))]
type fs_type_t = nix::libc::c_ulong;
#[cfg(all(target_os = "linux", target_env = "ohos"))]
type fs_type_t = nix::libc::c_ulong;
#[cfg(all(target_os = "linux", target_env = "uclibc"))]
type fs_type_t = nix::libc::c_int;
#[cfg(all(
    target_os = "linux",
    not(any(
        target_arch = "s390x",
        target_env = "musl",
        target_env = "ohos",
        target_env = "uclibc"
    ))
))]
type fs_type_t = nix::libc::__fsword_t;

const BTRFS_SUPER_MAGIC: fs_type_t = nix::libc::BTRFS_SUPER_MAGIC as fs_type_t;
const OVERLAYFS_SUPER_MAGIC: fs_type_t = nix::libc::OVERLAYFS_SUPER_MAGIC as fs_type_t;

/// Wrapper for struct statfs64
pub(crate) struct Statfs64(nix::libc::statfs64);

impl Statfs64 {
    // WORKAROUND:
    // Check if the file resides on a btrfs|overlayfs.
    // Overlayfs does not report device IDs correctly on
    // fstat, which is a known bug:
    // https://github.com/moby/moby/issues/43512
    // Btrfs has the same issue:
    // https://www.reddit.com/r/btrfs/comments/1clgd8u/different_dev_id_reported_by_statx_and/
    // Assume true on errors for safety.
    pub(crate) fn has_broken_device_ids(&self) -> bool {
        matches!(self.0.f_type, OVERLAYFS_SUPER_MAGIC | BTRFS_SUPER_MAGIC)
    }
}

/// Safe wrapper for fstatfs64
pub(crate) fn fstatfs64<F: AsRawFd>(fd: &F) -> Result<Statfs64, Errno> {
    let mut dst = std::mem::MaybeUninit::uninit();

    // SAFETY: nix does not have a wrapper for fstatfs64.
    Errno::result(unsafe { nix::libc::fstatfs64(fd.as_raw_fd(), dst.as_mut_ptr()) })?;

    // SAFETY: fstatfs64 returned success.
    Ok(Statfs64(unsafe { dst.assume_init() }))
}

/// Safe wrapper for epoll_ctl with detailed error handling
pub fn epoll_ctl_safe<F: AsFd>(
    epoll: &Epoll,
    fd: &F,
    event: Option<EpollEvent>,
) -> Result<(), Errno> {
    let (result, ignore_errno) = if let Some(event) = event {
        // Ignore EEXIST for EPOLL_CTL_ADD.
        (epoll.add(fd, event), Errno::EEXIST)
    } else {
        // Ignore ENOENT for EPOLL_CTL_DEL.
        (epoll.delete(fd), Errno::ENOENT)
    };
    match result {
        Ok(()) => Ok(()),
        Err(errno) if errno == ignore_errno => Ok(()),
        Err(e) => Err(e),
    }
}

const EPIOCSPARAMS: IoctlRequest = 0x40088a01;
const EPIOCGPARAMS: IoctlRequest = 0x80088a02u32 as IoctlRequest;

/// Epoll parameters
#[repr(C)]
pub struct EpollParams {
    /// Number of usecs to busy poll
    pub busy_poll_usecs: u32,
    /// Max packets per poll
    pub busy_poll_budget: u16,
    /// Boolean preference
    pub prefer_busy_poll: u16,
    // pad the struct to a multiple of 64bits
    // must be zero.
    pad: u8,
}

impl EpollParams {
    /// Create a new EpollParams structure.
    pub fn new(busy_poll_usecs: u32, busy_poll_budget: u16, prefer_busy_poll: bool) -> Self {
        let prefer_busy_poll = if prefer_busy_poll { 1 } else { 0 };
        Self {
            busy_poll_usecs,
            busy_poll_budget,
            prefer_busy_poll,
            pad: 0,
        }
    }
}

impl Serialize for EpollParams {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let mut map = serializer.serialize_map(Some(3))?; // We expect 3 fields.

        // Serialize busy_poll_usecs field.
        map.serialize_entry("busy_poll_usecs", &self.busy_poll_usecs)?;

        // Serialize busy_poll_budget field.
        map.serialize_entry("busy_poll_budget", &self.busy_poll_budget)?;

        // Serialize prefer_busy_poll.
        map.serialize_entry("prefer_busy_poll", &(self.prefer_busy_poll != 0))?;

        map.end()
    }
}

/// Set epoll parameters for the given epoll file descriptor.
/// Requires Linux>=6.9.
pub fn epoll_set_params<F: AsRawFd>(fd: &F, params: &EpollParams) -> Result<(), Errno> {
    // SAFETY: In libc we trust.
    Errno::result(unsafe { nix::libc::ioctl(fd.as_raw_fd(), EPIOCSPARAMS, params) }).map(drop)
}

/// Get epoll parameters for the given epoll file descriptor.
/// Requires Linux>=6.9.
pub fn epoll_get_params<F: AsRawFd>(fd: &F) -> Result<EpollParams, Errno> {
    let mut params = std::mem::MaybeUninit::uninit();

    // SAFETY: In libc we trust.
    Errno::result(unsafe { nix::libc::ioctl(fd.as_raw_fd(), EPIOCGPARAMS, params.as_mut_ptr()) })?;

    // SAFETY: ioctl returned success.
    Ok(unsafe { params.assume_init() })
}

/// Uses getsockopt SO_DOMAIN to get the domain of the given socket.
pub fn getsockdomain<F: AsRawFd>(fd: &F) -> Result<nix::libc::c_int, Errno> {
    #[allow(clippy::cast_possible_truncation)]
    let mut len = std::mem::size_of::<nix::libc::c_int>() as nix::libc::socklen_t;
    let mut fml: nix::libc::c_int = 0;

    // SAFETY: In libc we trust.
    Errno::result(unsafe {
        nix::libc::getsockopt(
            fd.as_raw_fd(),
            nix::libc::SOL_SOCKET,
            nix::libc::SO_DOMAIN,
            std::ptr::addr_of_mut!(fml) as *mut _,
            &mut len,
        )
    })?;

    Ok(fml)
}
