//! `/proc` access: finding the client, locating CardsDLL, and positioned reads
//! and writes against `/proc/<pid>/mem`.
//!
//! Positioned I/O (`pread`/`pwrite`) is used rather than seek+read: a 64-bit
//! virtual address is passed straight through as the file offset, so nothing
//! depends on a shared file cursor.

use std::fs::{self, File, OpenOptions};
use std::io;
use std::os::unix::fs::FileExt;

use crate::patch::Memory;
use crate::{is_client_comm, parse_cardsdll_base, pid_from_proc_entry};

/// Pids whose `comm` is exactly `FIFA17.exe`.
///
/// Sorted ascending so that, when more than one client is somehow running, the
/// per-pid log lines come out in a stable order (the Python inherits readdir
/// order, which is arbitrary).
pub fn find_pids() -> Vec<u32> {
    let mut out = Vec::new();
    let Ok(entries) = fs::read_dir("/proc") else {
        return out;
    };
    for entry in entries.flatten() {
        let name = entry.file_name();
        let Some(pid) = name.to_str().and_then(pid_from_proc_entry) else {
            continue;
        };
        // A pid can exit between readdir and this read; that is not an error.
        if let Ok(comm) = fs::read_to_string(format!("/proc/{pid}/comm")) {
            if is_client_comm(&comm) {
                out.push(pid);
            }
        }
    }
    out.sort_unstable();
    out
}

/// Base address of the mapped CardsDLL, or `None` while it is not mapped.
pub fn cardsdll_base(pid: u32) -> Option<u64> {
    let maps = fs::read_to_string(format!("/proc/{pid}/maps")).ok()?;
    parse_cardsdll_base(&maps)
}

/// [`Memory`] over one live client process.
pub struct ProcMem {
    pid: u32,
}

impl ProcMem {
    pub fn new(pid: u32) -> Self {
        Self { pid }
    }
}

impl Memory for ProcMem {
    fn pid(&self) -> u32 {
        self.pid
    }

    /// A failure here normally means the address is not mapped yet — the packer
    /// has not unpacked that code — which the watch loop treats as "come back
    /// next tick", not as a failure worth reporting. Read-only handle.
    fn read(&self, va: u64, buf: &mut [u8]) -> io::Result<()> {
        File::open(format!("/proc/{}/mem", self.pid))?.read_exact_at(buf, va)
    }

    /// Opened read+write like the Python's `r+b`; write-only is not universally
    /// accepted for `/proc/<pid>/mem` across kernels.
    fn write(&self, va: u64, data: &[u8]) -> io::Result<()> {
        OpenOptions::new()
            .read(true)
            .write(true)
            .open(format!("/proc/{}/mem", self.pid))?
            .write_all_at(data, va)
    }
}

/// Whether `/proc/<pid>` still exists — the launcher-liveness check.
pub fn pid_alive(pid: i64) -> bool {
    // Formatted exactly like the Python so a negative or zero pid behaves the
    // same way (the path simply does not exist).
    fs::metadata(format!("/proc/{pid}")).is_ok()
}

/// Real uid of this process, from the ownership of `/proc/self`.
///
/// std exposes no `getuid`, and this crate takes no dependencies; `/proc` is
/// mandatory for the patcher anyway, so reading it back is not a new assumption.
pub fn current_uid() -> io::Result<u32> {
    use std::os::unix::fs::MetadataExt;
    Ok(fs::metadata("/proc/self")?.uid())
}

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

    #[test]
    fn our_own_pid_is_alive_and_pid_zero_is_not() {
        let me: i64 = fs::read_to_string("/proc/self/stat")
            .unwrap()
            .split(' ')
            .next()
            .unwrap()
            .parse()
            .unwrap();
        assert!(pid_alive(me));
        // /proc/0 and /proc/-1 never exist, matching the Python's path check.
        assert!(!pid_alive(0));
        assert!(!pid_alive(-1));
    }

    #[test]
    fn uid_is_readable() {
        // Only that it resolves; the value is environment-dependent.
        assert!(current_uid().is_ok());
    }

    #[test]
    fn find_pids_scan_is_safe_without_a_client() {
        // Deterministic without a client: the scan must not panic and must only
        // ever return numeric pids.
        for pid in find_pids() {
            assert!(pid > 0);
        }
    }

    #[test]
    fn positioned_io_round_trips_against_our_own_address_space() {
        // Patching FIFA is not testable here, but the /proc/<pid>/mem mechanism
        // is: read and write this process's own heap through the same code path.
        let me: u32 = fs::read_to_string("/proc/self/stat")
            .unwrap()
            .split(' ')
            .next()
            .unwrap()
            .parse()
            .unwrap();
        let mem = ProcMem::new(me);
        // black_box throughout: this buffer is mutated by the kernel on our
        // behalf, never by Rust code, so the compiler must not assume it is
        // unchanged across the write.
        let target = std::hint::black_box(vec![0x75u8, 0x0f, 0x11, 0x22]);
        let va = target.as_ptr() as u64;

        let mut seen = [0u8; 4];
        mem.read(va, &mut seen).unwrap();
        assert_eq!(seen, *target);

        mem.write(va, &[0x7f, 0x0f]).unwrap();
        assert_eq!(*std::hint::black_box(&target), [0x7f, 0x0f, 0x11, 0x22]);

        // An address that is certainly not mapped reads as an error, which the
        // watch loop treats as "not unpacked yet".
        assert!(mem.read(0x1000, &mut seen).is_err());
    }
}
