// This file is part of the uutils coreutils package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.

// spell-checker:ignore gettime BOOTTIME clockid boottime nusers loadavg getloadavg timeval

//! Provides functions to get system uptime, number of users and load average.

// The code was originally written in uu_uptime
// (https://github.com/uutils/coreutils/blob/main/src/uu/uptime/src/uptime.rs)
// but was eventually moved here.
// See https://github.com/uutils/coreutils/pull/7289 for discussion.

use crate::error::{UError, UResult};
use crate::translate;
use jiff::Timestamp;
use jiff::tz::TimeZone;
use libc::time_t;
use thiserror::Error;

#[derive(Debug, Error)]
pub enum UptimeError {
    #[error("{}", translate!("uptime-lib-error-system-uptime"))]
    SystemUptime,
    #[error("{}", translate!("uptime-lib-error-system-loadavg"))]
    SystemLoadavg,
    #[error("{}", translate!("uptime-lib-error-windows-loadavg"))]
    WindowsLoadavg,
    #[error("{}", translate!("uptime-lib-error-boot-time"))]
    BootTime,
}

impl UError for UptimeError {
    fn code(&self) -> i32 {
        1
    }
}

/// Returns the formatted time string, e.g. "12:34:56"
pub fn get_formatted_time() -> String {
    Timestamp::now()
        .to_zoned(TimeZone::system())
        .strftime("%H:%M:%S")
        .to_string()
}

/// Safely get macOS boot time using sysctl command
///
/// This function uses the sysctl command-line tool to retrieve the kernel
/// boot time on macOS, avoiding any unsafe code. It parses the output
/// of the sysctl command to extract the boot time.
///
/// # Returns
///
/// Returns Some(time_t) if successful, None if the call fails.
#[cfg(target_os = "macos")]
fn get_macos_boot_time_sysctl() -> Option<time_t> {
    use std::process::Command;

    // Execute sysctl command to get boot time
    let output = Command::new("sysctl")
        .arg("-n")
        .arg("kern.boottime")
        .output();

    if let Ok(output) = output {
        if output.status.success() {
            // Parse output format: { sec = 1729338352, usec = 0 } Wed Oct 19 08:25:52 2025
            // We need to extract the seconds value from the structured output
            let stdout = String::from_utf8_lossy(&output.stdout);

            // Extract the seconds from the output
            // Look for "sec = " pattern
            if let Some(sec_start) = stdout.find("sec = ") {
                let sec_part = &stdout[sec_start + 6..];
                if let Some(sec_end) = sec_part.find(',') {
                    let sec_str = &sec_part[..sec_end];
                    if let Ok(boot_time) = sec_str.trim().parse::<i64>() {
                        return Some(boot_time as time_t);
                    }
                }
            }
        }
    }

    None
}

/// Get the system uptime
///
/// # Arguments
///
/// boot_time: Option<time_t> - Manually specify the boot time, or None to try to get it from the system.
///
/// # Returns
///
/// Returns a UResult with the uptime in seconds if successful, otherwise an UptimeError.
#[cfg(target_os = "openbsd")]
pub fn get_uptime(_boot_time: Option<time_t>) -> UResult<i64> {
    use libc::CLOCK_BOOTTIME;
    use libc::clock_gettime;

    use libc::c_int;
    use libc::timespec;

    let mut tp: timespec = timespec {
        tv_sec: 0,
        tv_nsec: 0,
    };

    // OpenBSD prototype: clock_gettime(clk_id: ::clockid_t, tp: *mut ::timespec) -> ::c_int;
    let ret: c_int = unsafe { clock_gettime(CLOCK_BOOTTIME, &raw mut tp) };

    if ret == 0 {
        #[cfg(target_pointer_width = "64")]
        let uptime: i64 = tp.tv_sec;
        #[cfg(not(target_pointer_width = "64"))]
        let uptime: i64 = tp.tv_sec.into();

        Ok(uptime)
    } else {
        Err(UptimeError::SystemUptime)?
    }
}

/// Get the system uptime
///
/// # Arguments
///
/// boot_time: Option<time_t> - Manually specify the boot time, or None to try to get it from the system.
///
/// # Returns
///
/// Returns a UResult with the uptime in seconds if successful, otherwise an UptimeError.
#[cfg(unix)]
#[cfg(not(target_os = "openbsd"))]
pub fn get_uptime(boot_time: Option<time_t>) -> UResult<i64> {
    use crate::utmpx::BOOT_TIME;
    use crate::utmpx::Utmpx;
    use std::fs::File;
    use std::io::Read;

    let mut proc_uptime_s = String::new();

    let proc_uptime = File::open("/proc/uptime")
        .ok()
        .and_then(|mut f| f.read_to_string(&mut proc_uptime_s).ok())
        .and_then(|_| proc_uptime_s.split_whitespace().next())
        .and_then(|s| s.split('.').next().unwrap_or("0").parse::<i64>().ok());

    if let Some(uptime) = proc_uptime {
        return Ok(uptime);
    }

    // Try provided boot_time or derive from utmpx
    let derived_boot_time = boot_time.or_else(|| {
        Utmpx::iter_all_records()
            .filter(|r| r.record_type() == BOOT_TIME)
            .map(|r| r.login_time().unix_timestamp())
            .find(|&ts| ts > 0)
            .map(|ts| ts as time_t)
    });

    // macOS-specific fallback: use sysctl kern.boottime when utmpx did not provide BOOT_TIME
    //
    // On macOS, the utmpx BOOT_TIME record can be unreliable or absent, causing intermittent
    // test failures (see issue #3621: https://github.com/uutils/coreutils/issues/3621).
    // The sysctl(CTL_KERN, KERN_BOOTTIME) approach is the canonical way to retrieve boot time
    // on macOS and is always available, making uptime more reliable on this platform.
    //
    // This fallback only runs if utmpx failed to provide a boot time.
    #[cfg(target_os = "macos")]
    let derived_boot_time = {
        let mut t = derived_boot_time;
        if t.is_none() {
            // Use a safe wrapper function to get boot time via sysctl
            if let Some(boot_time) = get_macos_boot_time_sysctl() {
                t = Some(boot_time);
            }
        }
        t
    };

    if let Some(t) = derived_boot_time {
        let now = Timestamp::now().as_second();
        #[cfg(target_pointer_width = "64")]
        let boottime: i64 = t;
        #[cfg(not(target_pointer_width = "64"))]
        let boottime: i64 = t.into();
        if now < boottime {
            Err(UptimeError::BootTime)?;
        }
        return Ok(now - boottime);
    }

    Err(UptimeError::SystemUptime)?
}

/// The format used to display a FormattedUptime.
pub enum OutputFormat {
    /// Typical `uptime` output (e.g. 2 days, 3:04).
    HumanReadable,

    /// Pretty printed output (e.g. 2 days, 3 hours, 04 minutes).
    PrettyPrint,
}

struct FormattedUptime {
    days: i64,
    hours: i64,
    mins: i64,
}

impl FormattedUptime {
    fn new(seconds: i64) -> Self {
        let days = seconds / 86400;
        let hours = (seconds - (days * 86400)) / 3600;
        let mins = (seconds - (days * 86400) - (hours * 3600)) / 60;

        Self { days, hours, mins }
    }

    fn get_human_readable_uptime(&self) -> String {
        translate!(
        "uptime-format",
        "days" => self.days,
        "time" => format!("{:02}:{:02}", self.hours, self.mins))
    }

    fn get_pretty_print_uptime(&self) -> String {
        let mut parts = Vec::new();
        if self.days > 0 {
            parts.push(translate!("uptime-format-pretty-day", "day" => self.days));
        }
        if self.hours > 0 {
            parts.push(translate!("uptime-format-pretty-hour", "hour" => self.hours));
        }
        if self.mins > 0 || parts.is_empty() {
            parts.push(translate!("uptime-format-pretty-min", "min" => self.mins));
        }
        parts.join(", ")
    }
}

/// Get the system uptime
///
/// # Arguments
///
/// boot_time will be ignored, pass None.
///
/// # Returns
///
/// Returns a UResult with the uptime in seconds if successful, otherwise an UptimeError.
#[cfg(windows)]
#[allow(clippy::unnecessary_wraps, reason = "needed on some platforms")]
pub fn get_uptime(_boot_time: Option<time_t>) -> UResult<i64> {
    use windows_sys::Win32::System::SystemInformation::GetTickCount;
    // SAFETY: always return u32
    let uptime = unsafe { GetTickCount() };
    Ok(uptime as i64 / 1000)
}

/// Get the system uptime in a human-readable format
///
/// # Arguments
///
/// boot_time: Option<time_t> - Manually specify the boot time, or None to try to get it from the system.
/// output_format: OutputFormat - Selects the format of the output string.
///
/// # Returns
///
/// Returns a UResult with the uptime in a human-readable format(e.g. "1 day, 3:45") if successful, otherwise an UptimeError.
#[inline]
pub fn get_formatted_uptime(
    boot_time: Option<time_t>,
    output_format: OutputFormat,
) -> UResult<String> {
    let uptime = get_uptime(boot_time)?;

    if uptime < 0 {
        Err(UptimeError::SystemUptime)?;
    }

    let formatted_uptime = FormattedUptime::new(uptime);

    match output_format {
        OutputFormat::HumanReadable => Ok(formatted_uptime.get_human_readable_uptime()),
        OutputFormat::PrettyPrint => Ok(formatted_uptime.get_pretty_print_uptime()),
    }
}

/// Get the number of users currently logged in
///
/// # Returns
///
/// Returns the number of users currently logged in if successful, otherwise 0.
#[cfg(unix)]
#[cfg(not(target_os = "openbsd"))]
// see: https://gitlab.com/procps-ng/procps/-/blob/4740a0efa79cade867cfc7b32955fe0f75bf5173/library/uptime.c#L63-L115
pub fn get_nusers() -> usize {
    use crate::utmpx::USER_PROCESS;
    use crate::utmpx::Utmpx;

    let mut num_user = 0;
    Utmpx::iter_all_records().for_each(|ut| {
        if ut.record_type() == USER_PROCESS {
            num_user += 1;
        }
    });
    num_user
}

/// Get the number of users currently logged in
///
/// # Returns
///
/// Returns the number of users currently logged in if successful, otherwise 0
#[cfg(target_os = "openbsd")]
pub fn get_nusers(file: &str) -> usize {
    use utmp_classic::{UtmpEntry, parse_from_path};

    let Ok(entries) = parse_from_path(file) else {
        return 0;
    };

    if entries.is_empty() {
        return 0;
    }

    // Count entries that have a non-empty user field
    entries
        .iter()
        .filter_map(|entry| match entry {
            UtmpEntry::UTMP { user, .. } if !user.is_empty() => Some(()),
            _ => None,
        })
        .count()
}

/// Get the number of users currently logged in
///
/// # Returns
///
/// Returns the number of users currently logged in if successful, otherwise 0
#[cfg(target_os = "windows")]
pub fn get_nusers() -> usize {
    use std::ptr;
    use windows_sys::Win32::System::RemoteDesktop::{
        WTS_CURRENT_SERVER_HANDLE, WTSEnumerateSessionsW, WTSFreeMemory,
        WTSQuerySessionInformationW,
    };

    let mut num_user = 0;

    // SAFETY: WTS_CURRENT_SERVER_HANDLE is a valid handle
    unsafe {
        let mut session_info_ptr = ptr::null_mut();
        let mut session_count = 0;

        let result = WTSEnumerateSessionsW(
            WTS_CURRENT_SERVER_HANDLE,
            0,
            1,
            &raw mut session_info_ptr,
            &raw mut session_count,
        );
        if result == 0 {
            return 0;
        }

        let sessions = std::slice::from_raw_parts(session_info_ptr, session_count as usize);

        for session in sessions {
            let mut buffer: *mut u16 = ptr::null_mut();
            let mut bytes_returned = 0;

            let result = WTSQuerySessionInformationW(
                WTS_CURRENT_SERVER_HANDLE,
                session.SessionId,
                5,
                &raw mut buffer,
                &raw mut bytes_returned,
            );
            if result == 0 || buffer.is_null() {
                continue;
            }

            let cstr = std::ffi::CStr::from_ptr(buffer.cast());
            if !cstr.is_empty() {
                num_user += 1;
            }

            WTSFreeMemory(buffer.cast());
        }

        WTSFreeMemory(session_info_ptr.cast());
    }

    num_user
}

/// Format the number of users to a human-readable string
///
/// # Returns
///
/// e.g. "0 users", "1 user", "2 users"
#[inline]
pub fn format_nusers(n: usize) -> String {
    translate!(
        "uptime-user-count",
        "count" => n
    )
}

/// Get the number of users currently logged in, in a human-readable format
///
/// # Returns
///
/// e.g. "0 user", "1 user", "2 users"
#[inline]
pub fn get_formatted_nusers() -> String {
    #[cfg(not(target_os = "openbsd"))]
    return format_nusers(get_nusers());

    #[cfg(target_os = "openbsd")]
    format_nusers(get_nusers("/var/run/utmp"))
}

/// Get the system load average
///
/// # Returns
///
/// Returns a UResult with the load average if successful, otherwise an UptimeError.
/// The load average is a tuple of three floating point numbers representing the 1-minute, 5-minute, and 15-minute load averages.
#[cfg(unix)]
pub fn get_loadavg() -> UResult<(f64, f64, f64)> {
    use crate::libc::c_double;
    use libc::getloadavg;

    let mut avg: [c_double; 3] = [0.0; 3];
    // SAFETY: checked whether it returns -1
    let loads: i32 = unsafe { getloadavg(avg.as_mut_ptr(), 3) };

    if loads == -1 {
        Err(UptimeError::SystemLoadavg)?
    } else {
        Ok((avg[0], avg[1], avg[2]))
    }
}

/// Get the system load average
/// Windows does not have an equivalent to the load average on Unix-like systems.
///
/// # Returns
///
/// Returns a UResult with an UptimeError.
#[cfg(windows)]
pub fn get_loadavg() -> UResult<(f64, f64, f64)> {
    Err(UptimeError::WindowsLoadavg)?
}

/// Get the system load average in a human-readable format
///
/// # Returns
///
/// Returns a UResult with the load average in a human-readable format if successful, otherwise an UptimeError.
/// e.g. "load average: 0.00, 0.00, 0.00"
#[inline]
pub fn get_formatted_loadavg() -> UResult<String> {
    let loadavg = get_loadavg()?;
    let mut args = fluent::FluentArgs::new();
    args.set("avg1", format!("{:.2}", loadavg.0));
    args.set("avg5", format!("{:.2}", loadavg.1));
    args.set("avg15", format!("{:.2}", loadavg.2));
    Ok(crate::locale::get_message_with_args(
        "uptime-lib-format-loadavg",
        args,
    ))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::locale;

    #[test]
    fn test_format_nusers() {
        unsafe {
            std::env::set_var("LANG", "en_US.UTF-8");
        }
        let _ = locale::setup_localization("uptime");
        assert_eq!("0 users", format_nusers(0));
        assert_eq!("1 user", format_nusers(1));
        assert_eq!("2 users", format_nusers(2));
    }

    /// Test that sysctl kern.boottime is accessible on macOS and returns valid boot time.
    /// This ensures the fallback mechanism added for issue #3621 works correctly.
    #[test]
    #[cfg(target_os = "macos")]
    fn test_macos_sysctl_boottime_available() {
        // Test the safe wrapper function
        let boot_time = get_macos_boot_time_sysctl();

        // Verify the safe wrapper succeeded
        assert!(
            boot_time.is_some(),
            "get_macos_boot_time_sysctl should succeed on macOS"
        );

        let boot_time = boot_time.unwrap();

        // Verify boot time is valid (positive, reasonable value)
        assert!(boot_time > 0, "Boot time should be positive");

        // Boot time should be after 2000-01-01 (946684800 seconds since epoch)
        assert!(
            boot_time > 946_684_800,
            "Boot time should be after year 2000"
        );

        // Boot time should be before current time
        let boot_time = Timestamp::from_second(boot_time).unwrap();
        let now = Timestamp::now();
        assert!(boot_time < now, "Boot time should be before current time");
    }

    /// Test that get_uptime always succeeds on macOS due to sysctl fallback.
    /// This addresses the intermittent failures reported in issue #3621.
    #[test]
    #[cfg(target_os = "macos")]
    fn test_get_uptime_always_succeeds_on_macos() {
        // Call get_uptime without providing boot_time, forcing the system
        // to use utmpx or fall back to sysctl
        let result = get_uptime(None);

        assert!(
            result.is_ok(),
            "get_uptime should always succeed on macOS with sysctl fallback"
        );

        let uptime = result.unwrap();
        assert!(uptime > 0, "Uptime should be positive");

        // Reasonable upper bound: system hasn't been up for more than 365 days
        // (This is just a sanity check)
        assert!(
            uptime < 365 * 86400,
            "Uptime seems unreasonably high: {uptime} seconds"
        );
    }

    /// Test get_uptime consistency by calling it multiple times.
    /// Verifies the sysctl fallback produces stable results.
    #[test]
    #[cfg(target_os = "macos")]
    fn test_get_uptime_macos_consistency() {
        let uptime1 = get_uptime(None).expect("First call should succeed");
        let uptime2 = get_uptime(None).expect("Second call should succeed");

        // Uptimes should be very close (within 1 second)
        let diff = (uptime1 - uptime2).abs();
        assert!(
            diff <= 1,
            "Consecutive uptime calls should be consistent, got {uptime1} and {uptime2}"
        );
    }
}
