//
// Syd: rock-solid application kernel
// src/time.rs: Randomized timers
//
// Copyright (c) 2025 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

use std::{
    fs::File,
    io::{Seek, Write},
    os::fd::OwnedFd,
    time::Instant,
};

use nix::errno::Errno;

use crate::{
    config::*,
    fs::{create_memfd, fillrandom, randint, seal_memfd, MFD_ALLOW_SEALING, MFD_NOEXEC_SEAL},
};

/// Randomized sysinfo(2)
pub struct SysInfo(libc::sysinfo);

impl SysInfo {
    /// Initialize a new randomized sysinfo(2) structure.
    // TODO: Use checked arithmetic!
    #[allow(clippy::arithmetic_side_effects)]
    pub fn new() -> Result<Self, Errno> {
        // SAFETY: `libc::sysinfo` is a POD (plain old data) type, so
        // zeroing it out does not violate any invariants. We do this
        // to ensure it's initialized to a known state before
        // randomizing.
        let mut info: libc::sysinfo = unsafe { std::mem::zeroed() };

        // Randomize the struct memory in 256-byte batches to avoid EINVAL from fillrandom.
        let info_siz = std::mem::size_of::<libc::sysinfo>();
        let info_ptr = std::ptr::addr_of_mut!(info) as *mut u8;
        let mut offset = 0_usize;
        while offset < info_siz {
            let end = (offset + 256).min(info_siz);
            // SAFETY: We create a slice covering a valid portion
            // (offset..end) of `info`'s memory. This is safe because
            // offset and end are kept within the size of `info`.
            let slice =
                unsafe { std::slice::from_raw_parts_mut(info_ptr.add(offset), end - offset) };
            fillrandom(slice)?;
            offset = end;
        }

        // SAFETY: Use realistic values to avoid easy detection.
        info.mem_unit = 1;

        info.totalhigh = 0;
        info.freehigh = 0;

        info.totalswap = 0;
        info.freeswap = 0;

        // Define realistic memory size limits.
        const MIN_RAM: libc::c_ulong = 0x8000000; // 128 MB
        const MAX_RAM: libc::c_ulong = 0xffffffff; // 4 GB

        // Adjust `totalram` to be a power of two within realistic limits.
        let mut totalram = info.totalram % (MAX_RAM - MIN_RAM + 1) + MIN_RAM;
        totalram = if totalram.is_power_of_two() {
            totalram
        } else {
            totalram.checked_next_power_of_two().unwrap_or(MAX_RAM) >> 1
        };
        totalram = totalram.clamp(MIN_RAM, MAX_RAM);
        info.totalram = totalram;

        // Adjust `freeram` to be a power of two ≤ `totalram`.
        let mut freeram = info.freeram % (info.totalram + 1);
        freeram = if freeram.is_power_of_two() {
            freeram
        } else {
            freeram.checked_next_power_of_two().unwrap_or(info.totalram) >> 1
        };
        freeram = freeram.min(info.totalram);
        info.freeram = freeram;

        // Adjust `sharedram` to be a power of two ≤ `totalram`.
        let mut sharedram = info.sharedram % (info.totalram + 1);
        sharedram = if sharedram.is_power_of_two() {
            sharedram
        } else {
            sharedram
                .checked_next_power_of_two()
                .unwrap_or(info.totalram)
                >> 1
        };
        sharedram = sharedram.min(info.totalram);
        info.sharedram = sharedram;

        // Adjust `bufferram` to be a power of two ≤ `totalram`.
        let mut bufferram = info.bufferram % (info.totalram + 1);
        bufferram = if bufferram.is_power_of_two() {
            bufferram
        } else {
            bufferram
                .checked_next_power_of_two()
                .unwrap_or(info.totalram)
                >> 1
        };
        bufferram = bufferram.min(info.totalram);
        info.bufferram = bufferram;

        // SAFETY: Use the global RAND_TIMER for consistent timing.
        // Note, uptime is `long` in glibc and `unsigned long` in musl.
        #[cfg(target_env = "musl")]
        {
            info.uptime = RAND_TIMER().uptime() as libc::c_ulong;
        }
        #[cfg(not(target_env = "musl"))]
        {
            info.uptime = RAND_TIMER().uptime() as libc::c_long;
        }

        // Load averages are fixed-point numbers with scaling factor 65536.
        const LOAD_SCALE: libc::c_ulong = 0x10000;
        const MAX_LOAD: libc::c_ulong = LOAD_SCALE * 16; // Max load average of 16.0
        for load in &mut info.loads {
            *load %= MAX_LOAD + 1;
        }

        // Adjust `procs` to be within a realistic range.
        const MIN_PROCS: libc::c_ushort = 2;
        const MAX_PROCS: libc::c_ushort = 0x8000;
        info.procs = (info.procs % (MAX_PROCS - MIN_PROCS + 1)) + MIN_PROCS;

        // Zero out the padding for security.
        info.pad = 0;

        Ok(Self(info))
    }

    /// Return the inner sysinfo structure.
    pub fn as_raw(self) -> libc::sysinfo {
        self.0
    }

    /// Produces a randomized string mimicking proc_loadavg(5), e.g:
    ///
    /// `"<load1> <load5> <load15> <running>/<procs> <last_pid>"`
    ///
    /// Loads come from `self.0.loads`.  The `running` count is random
    /// up to `self.0.procs`.  The `last_pid` is also random, in a
    /// plausible range (e.g. 256..65535).
    ///
    /// Example output:
    /// 2.64 2.28 2.26 3/541 1232852
    #[allow(clippy::cast_precision_loss)]
    pub fn proc_loadavg(&self) -> Result<String, Errno> {
        // Linux kernel scaling factor for load averages: 65536
        let load_scale = 65536.0;

        // Convert each load from fixed-point to float
        let load1 = self.0.loads[0] as f64 / load_scale;
        let load5 = self.0.loads[1] as f64 / load_scale;
        let load15 = self.0.loads[2] as f64 / load_scale;

        // `procs` is the total number of processes.
        // We'll pick a random "running" count from 1..=procs (if procs > 0).
        // If procs is 0 (extreme corner case), we'll treat running as 0.
        let procs = self.0.procs;

        #[allow(clippy::cast_possible_truncation)]
        let running = if procs == 0 {
            0
        } else {
            // Cap the random value at procs, ensuring running <= procs
            let r = randint(1..=u64::from(procs))?;
            r.min(u64::from(procs)) as u16
        };

        // Choose a plausible last_pid in the range [256..63535].
        let last_pid = randint(256..=0xFFFF)?;

        // Format like proc_loadavg(5).
        Ok(format!(
            "{load1:.2} {load5:.2} {load15:.2} {running}/{procs} {last_pid}\n"
        ))
    }

    /// Returns a /proc/uptime compatible memory file descriptor.
    ///
    /// The memory file descriptor is write-sealed.
    /// The memory file descriptor is exec-sealed if restrict_memfd is true.
    pub fn proc_loadavg_fd(&self, restrict_memfd: bool) -> Result<OwnedFd, Errno> {
        let repr = self.proc_loadavg()?;
        let data = repr.as_bytes();

        // Note, MFD_ALLOW_SEALING is implied for MFD_NOEXEC_SEAL.
        let flags = if restrict_memfd {
            MFD_NOEXEC_SEAL
        } else {
            MFD_ALLOW_SEALING
        };
        let fd = create_memfd(b"syd-loadavg\0", flags)?;
        let mut file = File::from(fd);

        file.write_all(data).or(Err(Errno::EIO))?;
        file.rewind().or(Err(Errno::EIO))?;

        // SAFETY: Deny further writes to the file descriptor.
        seal_memfd(&file)?;

        Ok(file.into())
    }
}

/// A cryptographically wrapped monotonic timer that masks both uptime
/// and idle time with independent realistic offsets.
pub struct RandTimer {
    /// Monotonic reference point for both fields.
    pub start: Instant,
    /// 64-bit random offset for reported uptime.
    pub uptime_offset: u64,
    /// 64-bit random offset for reported idle time.
    pub idle_offset: u64,
}

impl RandTimer {
    /// Generates a new timer with random offsets for uptime and idle.
    pub fn new() -> Result<Self, Errno> {
        // Fill both buffers with cryptographically secure bytes.
        // Use a plausible max offset ~194 days (0xFF_FFFF ≈ 16.7 million seconds).
        // This yields realistic yet unpredictable uptime/idle metrics.
        Ok(Self {
            start: Instant::now(),
            uptime_offset: randint(1..=0xFF_FFFF)?,
            idle_offset: randint(1..=0xFF_FFFF)?,
        })
    }

    /// Returns a masked uptime in seconds, wrapping on overflow.
    pub fn uptime(&self) -> u64 {
        let elapsed = self.start.elapsed().as_secs();
        elapsed.wrapping_add(self.uptime_offset)
    }

    /// Returns a masked idle time in seconds, wrapping on overflow.
    pub fn idle(&self) -> u64 {
        let elapsed = self.start.elapsed().as_secs();
        elapsed.wrapping_add(self.idle_offset)
    }

    /// Returns /proc/uptime compatible String representation.
    pub fn proc(&self) -> String {
        let elapsed = self.start.elapsed().as_secs();
        format!(
            "{}.{:02} {}.{:02}\n",
            elapsed.wrapping_add(self.uptime_offset),
            self.uptime_offset % 100,
            elapsed.wrapping_add(self.idle_offset),
            self.idle_offset % 100
        )
    }

    /// Returns a /proc/uptime compatible memory file descriptor.
    ///
    /// The memory file descriptor is write-sealed.
    /// The memory file descriptor is exec-sealed if restrict_memfd is true.
    pub fn proc_fd(&self, restrict_memfd: bool) -> Result<OwnedFd, Errno> {
        let repr = self.proc();
        let data = repr.as_bytes();

        // Note, MFD_ALLOW_SEALING is implied for MFD_NOEXEC_SEAL.
        let flags = if restrict_memfd {
            MFD_NOEXEC_SEAL
        } else {
            MFD_ALLOW_SEALING
        };
        let fd = create_memfd(b"syd-uptime\0", flags)?;
        let mut file = File::from(fd);

        file.write_all(data).or(Err(Errno::EIO))?;
        file.rewind().or(Err(Errno::EIO))?;

        // SAFETY: Deny further writes to the file descriptor.
        seal_memfd(&file)?;

        Ok(file.into())
    }
}

#[cfg(test)]
mod tests {
    use std::{thread, time::Duration};

    use super::RandTimer;

    /// Basic creation test: ensures RandTimer can be constructed and used.
    #[test]
    fn test_basic_creation() {
        let rt = RandTimer::new().expect("RandTimer creation failed");
        let _ = rt.uptime();
        let _ = rt.idle();
    }

    /// Verifies monotonic behavior over a short sleep for both uptime and idle.
    #[test]
    fn test_monotonic_increase() {
        let rt = RandTimer::new().expect("RandTimer creation failed");
        let before_uptime = rt.uptime();
        let before_idle = rt.idle();
        thread::sleep(Duration::from_millis(10));
        let after_uptime = rt.uptime();
        let after_idle = rt.idle();
        assert!(
            after_uptime >= before_uptime,
            "Uptime decreased from {} to {}",
            before_uptime,
            after_uptime
        );
        assert!(
            after_idle >= before_idle,
            "Idle time decreased from {} to {}",
            before_idle,
            after_idle
        );
    }

    /// Fires many calls to `uptime` and `idle` in quick succession.
    #[test]
    fn test_rapid_fire() {
        let rt = RandTimer::new().expect("RandTimer creation failed");
        for _ in 0..10_000 {
            let _ = rt.uptime();
            let _ = rt.idle();
        }
    }

    /// Stress-test repeated creation of RandTimer objects.
    #[test]
    fn test_repeated_creation() {
        for _ in 0..1000 {
            let rt = RandTimer::new().expect("RandTimer creation failed");
            assert_ne!(
                rt.uptime(),
                0,
                "Uptime offset might be zero too often, suspicious RNG."
            );
            assert_ne!(
                rt.idle(),
                0,
                "Idle offset might be zero too often, suspicious RNG."
            );
        }
    }

    /// Concurrency test: multiple threads each create + use RandTimer heavily.
    #[test]
    fn test_concurrency() {
        let threads = 8;
        let iterations = 2000;
        let mut handles = Vec::new();
        for _ in 0..threads {
            handles.push(thread::spawn(move || {
                for _ in 0..iterations {
                    let rt = RandTimer::new().unwrap();
                    let _ = rt.uptime();
                    let _ = rt.idle();
                }
            }));
        }
        for handle in handles {
            handle.join().expect("Thread panic in concurrency test");
        }
    }

    /// Ensures uptime offsets vary across multiple RandTimers. Checks for suspicious uniform offsets.
    #[test]
    fn test_uptime_offset_variability() {
        let iterations = 30;
        let mut offsets = Vec::new();
        for _ in 0..iterations {
            let rt = RandTimer::new().expect("RandTimer creation failed");
            // "Peek" uptime offset by subtracting the measured elapsed from `uptime`.
            let elapsed = rt.start.elapsed().as_secs();
            let offset_guess = rt.uptime().wrapping_sub(elapsed);
            offsets.push(offset_guess);
        }
        let all_same = offsets.windows(2).all(|w| w[0] == w[1]);
        assert!(
            !all_same,
            "All uptime offsets identical over {} RandTimer creations, suspicious RNG!",
            iterations
        );
    }

    /// Ensures idle offsets vary across multiple RandTimers.
    #[test]
    fn test_idle_offset_variability() {
        let iterations = 30;
        let mut offsets = Vec::new();
        for _ in 0..iterations {
            let rt = RandTimer::new().expect("RandTimer creation failed");
            let elapsed = rt.start.elapsed().as_secs();
            let offset_guess = rt.idle().wrapping_sub(elapsed);
            offsets.push(offset_guess);
        }
        let all_same = offsets.windows(2).all(|w| w[0] == w[1]);
        assert!(!all_same, "All idle offsets identical, suspicious RNG!");
    }

    /// Test artificially forcing uptime offset near u64::MAX to see if wrapping works.
    #[test]
    fn test_uptime_wrapping() {
        let mut rt = RandTimer::new().expect("RandTimer creation failed");
        rt.uptime_offset = u64::MAX - 1;
        let before = rt.uptime();
        thread::sleep(Duration::from_secs(1));
        let after = rt.uptime();
        assert!(
            after != before,
            "No change in uptime after forcing near-max offset + 1s sleep!"
        );
    }

    /// Test artificially forcing idle offset near u64::MAX to see if wrapping works.
    #[test]
    fn test_idle_wrapping() {
        let mut rt = RandTimer::new().expect("RandTimer creation failed");
        rt.idle_offset = u64::MAX - 1;
        let before = rt.idle();
        thread::sleep(Duration::from_secs(1));
        let after = rt.idle();
        assert!(
            after != before,
            "No change in idle time after forcing near-max offset + 1s sleep!"
        );
    }

    /// Check forcibly set offsets to zero for near "raw monotonic" behavior.
    #[test]
    fn test_force_offsets_zero() {
        let mut rt = RandTimer::new().expect("RandTimer creation failed");
        rt.uptime_offset = 0;
        rt.idle_offset = 0;
        let t1_up = rt.uptime();
        let t1_idle = rt.idle();
        thread::sleep(Duration::from_millis(5));
        let t2_up = rt.uptime();
        let t2_idle = rt.idle();
        assert!(
            t2_up >= t1_up,
            "Uptime decreased with zero offset: {} to {}",
            t1_up,
            t2_up
        );
        assert!(
            t2_idle >= t1_idle,
            "Idle decreased with zero offset: {} to {}",
            t1_idle,
            t2_idle
        );
    }

    /// Force a very large idle offset and a small sleep. Ensures no panic or freeze.
    #[test]
    fn test_large_idle_offset_small_sleep() {
        let mut rt = RandTimer::new().expect("RandTimer creation failed");
        rt.idle_offset = u64::MAX / 2;
        let before = rt.idle();
        thread::sleep(Duration::from_secs(1));
        let after = rt.idle();
        assert_ne!(
            before, after,
            "Idle unchanged after short sleep with large offset!"
        );
    }

    /// Big loop creation test for both offsets.
    #[test]
    fn test_big_loop_creation() {
        for i in 0..10_000 {
            let rt = RandTimer::new().expect("RandTimer creation failed");
            if i % 1000 == 0 {
                let _ = rt.uptime();
                let _ = rt.idle();
            }
        }
    }

    /// Force multiple odd offsets for both uptime and idle, ensure each acts consistently.
    #[test]
    fn test_various_forced_offsets() {
        let test_offsets = [
            (1, 1),
            (42, 999_999_999),
            (0x0000FFFF_FFFFFFFF, 0xFFFFAAAA_FFFFFFFF),
            (0xFFFFFFFF_FFFFFFFF, 0x55555555_55555555),
        ];
        for &(u_off, i_off) in &test_offsets {
            let mut rt = RandTimer::new().expect("RandTimer creation failed");
            rt.uptime_offset = u_off;
            rt.idle_offset = i_off;
            let up1 = rt.uptime();
            let idle1 = rt.idle();
            thread::sleep(Duration::from_millis(2));
            let up2 = rt.uptime();
            let idle2 = rt.idle();
            assert!(
                up2 >= up1,
                "Uptime offset {} yields invalid progression: {} -> {}",
                u_off,
                up1,
                up2
            );
            assert!(
                idle2 >= idle1,
                "Idle offset {} yields invalid progression: {} -> {}",
                i_off,
                idle1,
                idle2
            );
        }
    }
}
