//! Read-only window into a *running* CS2 server's memory — the runtime oracle that verifies the
//! offline derivations against ground truth. No injection, no debugger: just `/proc/<pid>/mem` (needs
//! ptrace access — same-user with `yama/ptrace_scope=0`, or `CAP_SYS_PTRACE`).
//!
//! Offline we resolve `.rela.dyn` by hand to recover as-loaded pointer values; the running process is
//! the authority on what those values actually are. So reading the same structures live and comparing
//! confirms both our relocation logic and the struct layout — and, because runtime-populated fields
//! (e.g. `m_pSchemaBinding`) are non-null live but zero on disk, proves we are reading live state.

use anyhow::{Context, Result};
use std::collections::HashMap;
use std::fs::File;
use std::os::unix::fs::FileExt;

pub struct LiveProcess {
    mem: File,
    bases: HashMap<String, u64>, // library filename -> load base (lowest mapping address)
    paths: HashMap<String, String>, // library filename -> the FULL path the process actually mapped
    writable: Vec<(u64, u64)>,   // rw anonymous regions (heap etc.) — where live objects live
    executable: Vec<(u64, u64)>, // r-x regions — where valid code/vtable-slot targets must land
}

impl LiveProcess {
    pub fn attach(pid: u32) -> Result<Self> {
        let maps = std::fs::read_to_string(format!("/proc/{pid}/maps"))
            .with_context(|| format!("read /proc/{pid}/maps (is pid {pid} running?)"))?;
        let mut bases: HashMap<String, u64> = HashMap::new();
        let mut paths: HashMap<String, String> = HashMap::new();
        let mut writable: Vec<(u64, u64)> = Vec::new();
        let mut executable: Vec<(u64, u64)> = Vec::new();
        for line in maps.lines() {
            // format: START-END perms offset dev inode  path
            let (range, rest) = match line.split_once(' ') {
                Some(x) => x,
                None => continue,
            };
            let perms = rest.split(' ').next().unwrap_or("");
            let path = line.rsplit_once(char::is_whitespace).map_or("", |(_, p)| p);
            let Some((start, end)) = range.split_once('-').and_then(|(a, b)| {
                Some((
                    u64::from_str_radix(a, 16).ok()?,
                    u64::from_str_radix(b, 16).ok()?,
                ))
            }) else {
                continue;
            };
            if path.ends_with(".so") && path.starts_with('/') {
                let fname = path.rsplit('/').next().unwrap_or(path).to_string();
                paths
                    .entry(fname.clone())
                    .or_insert_with(|| path.to_string());
                bases
                    .entry(fname)
                    .and_modify(|b| *b = (*b).min(start))
                    .or_insert(start);
            }
            // writable anonymous memory = the heap where runtime objects (entities) are allocated
            if perms.starts_with("rw") && (path.is_empty() || path == "[heap]") {
                writable.push((start, end));
            }
            if perms.starts_with('r') && perms.contains('x') {
                executable.push((start, end));
            }
        }
        executable.sort_unstable();
        let mem = File::open(format!("/proc/{pid}/mem")).with_context(|| {
            format!(
                "open /proc/{pid}/mem — needs ptrace access (yama ptrace_scope=0 or run as root)"
            )
        })?;
        Ok(Self {
            mem,
            bases,
            paths,
            writable,
            executable,
        })
    }

    /// Is `addr` inside an executable mapping? A valid function pointer / vtable slot target must be.
    pub fn is_exec(&self, addr: u64) -> bool {
        self.executable
            .binary_search_by(|&(s, e)| {
                if addr < s {
                    std::cmp::Ordering::Greater
                } else if addr >= e {
                    std::cmp::Ordering::Less
                } else {
                    std::cmp::Ordering::Equal
                }
            })
            .is_ok()
    }

    /// Best-effort read of `n` bytes at runtime `addr` (short/empty on an unmapped page).
    pub fn read_bytes(&self, addr: u64, n: usize) -> Vec<u8> {
        let mut buf = vec![0u8; n];
        let got = self.mem.read_at(&mut buf, addr).unwrap_or(0);
        buf.truncate(got);
        buf
    }

    /// Scan the writable/heap regions for object instances whose vtable pointer is `vtable` — i.e.
    /// live instances of the class that owns that vtable. Returns the object base addresses (an
    /// object's first qword is its vtable pointer). Stops at `max` hits.
    pub fn find_instances(&self, vtable: u64, max: usize) -> Vec<u64> {
        let mut hits = Vec::new();
        let mut buf = vec![0u8; 1 << 20]; // 1 MiB window
        let needle = vtable.to_le_bytes();
        'outer: for &(start, end) in &self.writable {
            let mut addr = start;
            while addr < end {
                let n = ((end - addr) as usize).min(buf.len());
                // `buf` is reused across windows, so scanning past a SHORT read matches stale bytes from the
                // previous window and reports addresses that hold nothing of the sort. Bind both the scan and
                // the advance to what was actually read.
                let got = self.mem.read_at(&mut buf[..n], addr).unwrap_or(0);
                if got < 8 {
                    addr += n as u64;
                    continue;
                }
                // objects are pointer-aligned, so only 8-aligned positions can be a vtable slot
                let mut i = 0;
                while i + 8 <= got {
                    if buf[i..i + 8] == needle {
                        hits.push(addr + i as u64);
                        if hits.len() >= max {
                            break 'outer;
                        }
                    }
                    i += 8;
                }
                // advance by the 8-aligned prefix consumed: a persistently short-reading region still makes
                // progress (never re-reads the same bytes), and the skipped tail is retried on the next pass.
                addr += (got & !7) as u64;
            }
        }
        hits
    }

    /// Load base (slide) of a library — its lowest mapping address. Since CS2 `.so` files link at
    /// vaddr 0, the runtime address of a file vaddr `v` is simply `base + v`.
    pub fn base(&self, lib: &str) -> Option<u64> {
        self.bases.get(lib).copied()
    }

    /// The full path the process actually mapped for `lib` (a basename). The authority on WHICH file of a
    /// given name is loaded when several exist on disk — a game tree can hold the engine's own
    /// `libserver.so` and a loader shim of the same name several directories away.
    pub fn mapped_path(&self, lib: &str) -> Option<&str> {
        self.paths.get(lib).map(String::as_str)
    }

    fn read(&self, addr: u64, buf: &mut [u8]) -> Result<()> {
        self.mem
            .read_exact_at(buf, addr)
            .with_context(|| format!("read {} bytes at {addr:#x}", buf.len()))
    }

    pub fn read_u64(&self, addr: u64) -> Result<u64> {
        let mut b = [0u8; 8];
        self.read(addr, &mut b)?;
        Ok(u64::from_le_bytes(b))
    }

    pub fn read_i32(&self, addr: u64) -> Result<i32> {
        let mut b = [0u8; 4];
        self.read(addr, &mut b)?;
        Ok(i32::from_le_bytes(b))
    }

    pub fn read_u16(&self, addr: u64) -> Result<u16> {
        let mut b = [0u8; 2];
        self.read(addr, &mut b)?;
        Ok(u16::from_le_bytes(b))
    }

    /// NUL-terminated string at runtime `addr` (bounded). Reads may land near an unmapped page, so a
    /// short read is fine — we take whatever came back up to the terminator.
    pub fn read_cstr(&self, addr: u64) -> Result<String> {
        let mut buf = [0u8; 256];
        let n = self.mem.read_at(&mut buf, addr).unwrap_or(0);
        let end = buf[..n].iter().position(|&c| c == 0).unwrap_or(n);
        Ok(String::from_utf8_lossy(&buf[..end]).into_owned())
    }
}

/// Result of a remote call: the return value (RAX) and whether the function returned cleanly to our
/// trap (vs faulting internally on a bad argument).
pub struct CallResult {
    pub rax: u64,
    pub clean_return: bool,
}

/// Call the function at runtime address `func` inside process `pid` with `args` (SysV: up to 6 in
/// registers), via ptrace. Attaches, saves the main thread's registers, sets up a call frame whose
/// return address is 0 (so the function traps on return, where we read RAX), runs it, then restores
/// the thread exactly — the SIGSEGV from the return trap is suppressed. Needs ptrace permission
/// (owned child, or same-user with ptrace_scope=0). UNSAFE: only call leaf-ish functions with valid args.
pub fn call_remote(pid: i32, func: u64, args: &[u64]) -> Result<CallResult> {
    use anyhow::bail;
    let dbg = std::env::var("SOURCE2ROSETTA_DBG").is_ok();
    unsafe {
        if libc::ptrace(libc::PTRACE_ATTACH, pid, 0usize, 0usize) < 0 {
            bail!(
                "PTRACE_ATTACH {pid} failed (errno {}) — need ptrace permission",
                errno()
            );
        }
        let mut status = 0i32;
        if libc::waitpid(pid, &mut status, 0) < 0 {
            libc::ptrace(libc::PTRACE_DETACH, pid, 0usize, 0usize);
            bail!("waitpid(attach) failed");
        }
        if dbg {
            eprintln!(
                "[call] attached; stop status {status:#x} (stopped={})",
                libc::WIFSTOPPED(status)
            );
        }
        let mut saved: libc::user_regs_struct = std::mem::zeroed();
        if libc::ptrace(libc::PTRACE_GETREGS, pid, 0usize, &mut saved as *mut _) < 0 {
            libc::ptrace(libc::PTRACE_DETACH, pid, 0usize, 0usize);
            bail!("PTRACE_GETREGS failed");
        }
        let restore = |saved: &libc::user_regs_struct| {
            libc::ptrace(libc::PTRACE_SETREGS, pid, 0usize, saved as *const _);
            libc::ptrace(libc::PTRACE_DETACH, pid, 0usize, 0usize);
        };

        let mut regs = saved;
        // If we attached mid-syscall, orig_rax holds the syscall number and the kernel would run its
        // syscall-restart logic on our injected rip. Setting it to -1 says "no syscall in progress".
        regs.orig_rax = u64::MAX;
        let slots = [
            &mut regs.rdi as *mut u64,
            &mut regs.rsi,
            &mut regs.rdx,
            &mut regs.rcx,
            &mut regs.r8,
            &mut regs.r9,
        ];
        for (i, &a) in args.iter().take(6).enumerate() {
            *slots[i] = a;
        }
        // Scratch stack BELOW the 128-byte redzone so we never corrupt the interrupted frame; write a
        // return address of 0 and keep SysV's `rsp % 16 == 8` at function entry.
        let mut sp = (saved.rsp - 512) & !0xfu64;
        sp -= 8;
        if libc::ptrace(libc::PTRACE_POKEDATA, pid, sp as usize, 0usize) < 0 {
            restore(&saved);
            bail!(
                "POKEDATA(return addr) at {sp:#x} failed (errno {})",
                errno()
            );
        }
        let wrote = libc::ptrace(libc::PTRACE_PEEKDATA, pid, sp as usize, 0usize);
        regs.rsp = sp;
        regs.rip = func;
        if libc::ptrace(libc::PTRACE_SETREGS, pid, 0usize, &regs as *const _) < 0 {
            restore(&saved);
            bail!("PTRACE_SETREGS failed");
        }
        if dbg {
            eprintln!(
                "[call] rip={func:#x} rsp={sp:#x} rdi={:#x} retaddr-slot={wrote:#x} (want 0)",
                regs.rdi
            );
        }

        // Run, absorbing any spurious signals, until the function returns into our null trap.
        loop {
            libc::ptrace(libc::PTRACE_CONT, pid, 0usize, 0usize);
            if libc::waitpid(pid, &mut status, 0) < 0 || !libc::WIFSTOPPED(status) {
                restore(&saved);
                bail!("target vanished mid-call (status {status:#x})");
            }
            let sig = libc::WSTOPSIG(status);
            let mut cur: libc::user_regs_struct = std::mem::zeroed();
            libc::ptrace(libc::PTRACE_GETREGS, pid, 0usize, &mut cur as *mut _);
            if dbg {
                eprintln!(
                    "[call] stop sig={sig} rip={:#x} rax={:#x}",
                    cur.rip, cur.rax
                );
            }
            if cur.rip == 0 {
                let r = CallResult {
                    rax: cur.rax,
                    clean_return: true,
                };
                restore(&saved);
                return Ok(r);
            }
            if sig == libc::SIGSEGV || sig == libc::SIGILL || sig == libc::SIGBUS {
                let r = CallResult {
                    rax: cur.rax,
                    clean_return: false,
                };
                restore(&saved);
                return Ok(r);
            }
            // any other signal (SIGSTOP/timer/…): swallow it and keep running the call
        }
    }
}

fn errno() -> i32 {
    unsafe { *libc::__errno_location() }
}
