use std::ffi::{CStr, CString};
use std::io::{Error, Result};

/// Get password file entry and return user name.
///
/// This function wraps the standard C library function getpwuid().
/// The getpwuid() function returns a pointer to a structure containing the
/// broken-out fields of the record in the password database (e.g., the local
/// password file /etc/passwd, NIS, and LDAP) that matches the user ID uid.
pub(crate) fn getpwuid_name(uid: u32) -> Result<Option<String>> {
    let mut bufsize: usize = 1024;
    let mut pwd = std::mem::MaybeUninit::<libc::passwd>::uninit();
    let mut result = std::ptr::null_mut::<libc::passwd>();
    loop {
        let mut buf = vec![0u8; bufsize];
        let rc = unsafe {
            libc::getpwuid_r(
                uid,
                pwd.as_mut_ptr(),
                buf.as_mut_ptr() as *mut libc::c_char,
                buf.len(),
                &mut result,
            )
        };
        if rc == libc::ERANGE {
            // Buffer too small
            bufsize *= 2;
            continue;
        }
        if rc == libc::ENOENT {
            return Ok(None);
        }
        if rc != 0 {
            return Err(Error::last_os_error());
        }
        if result.is_null() {
            return Ok(None);
        }
        let name = unsafe { core::ffi::CStr::from_ptr((*result).pw_name) };
        return Ok(Some(name.to_string_lossy().to_string()));
    }
}

/// Get group file entry and return group name.
///
/// This function wraps the standard C library function getgrgid().
/// The getgrgid() function returns a pointer to a structure containing the
/// broken-out fields of the record in the group database (e.g., the local
/// group file /etc/group, NIS, and LDAP) that matches the group ID gid.
pub(crate) fn getgrgid_name(gid: u32) -> Result<Option<String>> {
    let mut bufsize: usize = 1024;
    let mut group = std::mem::MaybeUninit::<libc::group>::uninit();
    let mut result = std::ptr::null_mut::<libc::group>();
    loop {
        let mut buf = vec![0u8; bufsize];
        let rc = unsafe {
            libc::getgrgid_r(
                gid,
                group.as_mut_ptr(),
                buf.as_mut_ptr() as *mut libc::c_char,
                buf.len(),
                &mut result,
            )
        };
        if rc == libc::ERANGE {
            // Buffer too small
            bufsize *= 2;
            continue;
        }
        if rc == libc::ENOENT {
            return Ok(None);
        }
        if rc != 0 {
            return Err(Error::last_os_error());
        }
        if result.is_null() {
            return Ok(None);
        }
        let name = unsafe { core::ffi::CStr::from_ptr((*result).gr_name) };
        return Ok(Some(name.to_string_lossy().to_string()));
    }
}

pub(crate) fn major(dev: u64) -> u32 {
    libc::major(dev)
}

pub(crate) fn minor(dev: u64) -> u32 {
    libc::minor(dev)
}

pub(crate) fn mknod(pathname: &str, mode: libc::mode_t, major: u32, minor: u32) -> Result<()> {
    let p = CString::new(pathname)?;
    let rc = unsafe { libc::mknod(p.as_ptr(), mode, libc::makedev(major, minor)) };
    if rc != 0 {
        return Err(Error::last_os_error());
    };
    Ok(())
}

pub(crate) fn set_modified(path: &str, mtime: i64) -> Result<()> {
    let p = CString::new(path)?;
    let mut modified: libc::timespec = unsafe { std::mem::zeroed() };
    modified.tv_sec = mtime;
    // times contains the access time followed by modfied time
    let times = [modified, modified];
    let rc = unsafe {
        libc::utimensat(
            libc::AT_FDCWD,
            p.as_ptr(),
            times.as_ptr(),
            libc::AT_SYMLINK_NOFOLLOW,
        )
    };
    if rc != 0 {
        return Err(Error::last_os_error());
    };
    Ok(())
}

// TODO: Use c"…" string literal for `format` once stable
fn strftime(format: &[u8], tm: *mut libc::tm) -> Result<String> {
    let mut s = [0u8; 19];
    let length = unsafe {
        libc::strftime(
            s.as_mut_ptr() as *mut libc::c_char,
            s.len(),
            CStr::from_bytes_with_nul_unchecked(format).as_ptr(),
            tm,
        )
    };
    if length == 0 {
        return Err(Error::other("strftime returned 0"));
    }
    Ok(String::from_utf8_lossy(&s[..length]).to_string())
}

pub(crate) fn strftime_local(format: &[u8], timestamp: u32) -> Result<String> {
    let mut tm = std::mem::MaybeUninit::<libc::tm>::uninit();
    let result = unsafe { libc::localtime_r(&timestamp.into(), tm.as_mut_ptr()) };
    if result.is_null() {
        return Err(Error::last_os_error());
    };
    strftime(format, result)
}

#[cfg(test)]
pub(crate) mod tests {
    use super::*;
    use crate::temp_dir::TempDir;
    use crate::tests::set_timezone_to_utc;
    use std::time::{Duration, SystemTime};

    #[test]
    fn test_getpwuid_name_root() {
        let got = getpwuid_name(0).unwrap();
        assert_eq!(got, Some("root".to_string()));
    }

    #[test]
    fn test_getpwuid_name_non_existing() {
        // Assume that this UID is not in /etc/passwd (nobody is 65534)
        let got = getpwuid_name(65520).unwrap();
        assert_eq!(got, None);
    }

    #[test]
    fn test_getgrgid_name_root() {
        let got = getgrgid_name(0).unwrap();
        assert_eq!(got, Some("root".to_string()));
    }

    #[test]
    fn test_getgrgid_name_non_existing() {
        // Assume that this GID is not in /etc/passwd (nogroup is 65534)
        let got = getgrgid_name(65520).unwrap();
        assert_eq!(got, None);
    }

    #[test]
    // Create a temporary directory and set the mtime 10 seconds earlier
    // than the current mtime of the directory.
    fn test_set_modified() {
        let dir = TempDir::new().unwrap();
        let modified = dir.path.metadata().unwrap().modified().unwrap();
        let duration = modified.duration_since(SystemTime::UNIX_EPOCH).unwrap();
        let new_modified = SystemTime::UNIX_EPOCH
            .checked_add(Duration::new(duration.as_secs() - 10, 0))
            .unwrap();

        let mtime = new_modified.duration_since(SystemTime::UNIX_EPOCH).unwrap();
        let p = dir.path.clone().into_os_string().into_string().unwrap();
        set_modified(&p, mtime.as_secs().try_into().unwrap()).unwrap();

        assert_eq!(
            dir.path.metadata().unwrap().modified().unwrap(),
            new_modified
        );
    }

    #[test]
    fn test_strftime_local_year() {
        let time = strftime_local(b"%b %e  %Y\0", 2278410030).unwrap();
        assert_eq!(time, "Mar 14  2042");
    }

    #[test]
    fn test_strftime_local_hour() {
        set_timezone_to_utc();
        let time = strftime_local(b"%b %e %H:%M\0", 1720735264).unwrap();
        assert_eq!(time, "Jul 11 22:01");
    }
}
