// Per-process writable-file enumeration, native on each OS.
//
// On Linux the original implementation in `proc_::read_writing_files`
// walks `/proc/<pid>/fdinfo` + `/proc/<pid>/fd`.  This module
// dispatches that on Linux and provides equivalent native
// implementations for macOS (libproc / proc_pidinfo) and Windows
// (NtQuerySystemInformation + DuplicateHandle + GetFinalPathNameByHandle).
//
// All implementations:
//   - return an empty `Vec` on any access denial / unknown PID
//     (never panic, never bubble errors)
//   - cap the result at `limit` entries so a process with thousands
//     of open files doesn't dominate one collector tick
//   - exclude pipes / sockets / device nodes / anonymous memory
//     mappings — only real on-disk files

use std::path::PathBuf;

/// Cross-platform entry point.  Returns `Vec<PathBuf>` of files the
/// target PID has open with write access (O_WRONLY / O_RDWR on POSIX,
/// FILE_GENERIC_WRITE on Windows).
pub fn read(pid: u32, limit: usize) -> Vec<PathBuf> {
    impl_::read(pid, limit)
}

// ── Linux ──────────────────────────────────────────────────────────────────
#[cfg(target_os = "linux")]
mod impl_ {
    use super::*;
    pub fn read(pid: u32, limit: usize) -> Vec<PathBuf> {
        crate::proc_::read_writing_files(pid, limit)
    }
}

// ── macOS ──────────────────────────────────────────────────────────────────
#[cfg(target_os = "macos")]
mod impl_ {
    use super::*;
    use std::ffi::c_void;
    use std::os::raw::{c_char, c_int, c_uint};

    // Apple's libproc.h flavor constants (stable since 10.5).
    const PROC_PIDLISTFDS:           c_int = 1;
    const PROC_PIDFDVNODEPATHINFO:   c_int = 2;
    const PROX_FDTYPE_VNODE:         u32   = 1;

    // libSystem.dylib symbols.  Available on every macOS without an
    // explicit `link` directive — the dynamic linker resolves them.
    extern "C" {
        fn proc_pidinfo(
            pid: c_int, flavor: c_int, arg: u64,
            buffer: *mut c_void, buffersize: c_int,
        ) -> c_int;
        fn proc_pidfdinfo(
            pid: c_int, fd: c_int, flavor: c_int,
            buffer: *mut c_void, buffersize: c_int,
        ) -> c_int;
    }

    #[repr(C)]
    #[derive(Default, Clone, Copy)]
    struct ProcFdInfo {
        proc_fd: c_int,
        proc_fdtype: c_uint,
    }

    /// Subset of `proc_fileinfo` from sys/proc_info.h.  We only read
    /// `fi_openflags`; the surrounding fields are present so the
    /// struct size matches what proc_pidfdinfo writes.
    #[repr(C)]
    #[derive(Default, Clone, Copy)]
    struct ProcFileInfo {
        fi_openflags: u32,
        fi_status:    u32,
        fi_offset:    i64,
        fi_type:      i32,
        fi_guardflags: u32,
    }

    #[repr(C)]
    #[derive(Clone, Copy)]
    struct VnodeInfo {
        // 152-byte stat block; we don't read any of it.
        _opaque: [u8; 152],
    }
    impl Default for VnodeInfo {
        fn default() -> Self { Self { _opaque: [0u8; 152] } }
    }

    #[repr(C)]
    #[derive(Clone, Copy)]
    struct VnodeInfoPath {
        vip_vi:   VnodeInfo,
        vip_path: [c_char; 1024],   // MAXPATHLEN, NUL-terminated
    }
    impl Default for VnodeInfoPath {
        fn default() -> Self { Self { vip_vi: VnodeInfo::default(), vip_path: [0; 1024] } }
    }

    #[repr(C)]
    #[derive(Default, Clone, Copy)]
    struct VnodeFdInfoWithPath {
        pfi:  ProcFileInfo,
        pvip: VnodeInfoPath,
    }

    pub fn read(pid: u32, limit: usize) -> Vec<PathBuf> {
        let pid = pid as c_int;

        // Step 1: probe how big the FD list is.
        let probe = unsafe {
            proc_pidinfo(pid, PROC_PIDLISTFDS, 0, std::ptr::null_mut(), 0)
        };
        if probe <= 0 { return Vec::new(); }
        // Cap at 4096 entries to avoid a runaway alloc on a process
        // with millions of fds.
        let needed = (probe as usize).min(4096 * std::mem::size_of::<ProcFdInfo>());
        let entry_count = needed / std::mem::size_of::<ProcFdInfo>();
        let mut buf: Vec<ProcFdInfo> = vec![ProcFdInfo::default(); entry_count];

        // Step 2: fetch the actual FD list.
        let written = unsafe {
            proc_pidinfo(
                pid, PROC_PIDLISTFDS, 0,
                buf.as_mut_ptr() as *mut c_void,
                (buf.len() * std::mem::size_of::<ProcFdInfo>()) as c_int,
            )
        };
        if written <= 0 { return Vec::new(); }
        let got = (written as usize) / std::mem::size_of::<ProcFdInfo>();

        let mut out: Vec<PathBuf> = Vec::with_capacity(limit.min(got));
        for fd in buf.iter().take(got) {
            if out.len() >= limit { break; }
            if fd.proc_fdtype != PROX_FDTYPE_VNODE { continue; }

            // Step 3: per-FD path + flags.
            let mut info = VnodeFdInfoWithPath::default();
            let n = unsafe {
                proc_pidfdinfo(
                    pid, fd.proc_fd, PROC_PIDFDVNODEPATHINFO,
                    &mut info as *mut _ as *mut c_void,
                    std::mem::size_of::<VnodeFdInfoWithPath>() as c_int,
                )
            };
            if n <= 0 { continue; }
            // O_WRONLY = 1, O_RDWR = 2 (POSIX).
            let flags = info.pfi.fi_openflags as i32;
            if flags & (libc::O_WRONLY | libc::O_RDWR) == 0 { continue; }

            // vip_path is NUL-terminated c_char (i8 on Apple).
            let path = &info.pvip.vip_path;
            let nul = path.iter().position(|&b| b == 0).unwrap_or(path.len());
            let bytes: Vec<u8> = path[..nul].iter().map(|&c| c as u8).collect();
            let s = match std::str::from_utf8(&bytes) {
                Ok(s) => s,
                Err(_) => continue,
            };
            if s.is_empty() || s.starts_with("/dev/") { continue; }
            out.push(PathBuf::from(s));
        }
        out
    }
}

// ── Windows ────────────────────────────────────────────────────────────────
#[cfg(windows)]
mod impl_ {
    use super::*;
    use std::ffi::OsString;
    use std::os::windows::ffi::OsStringExt;

    use windows_sys::Win32::Foundation::{
        CloseHandle, DuplicateHandle, DUPLICATE_SAME_ACCESS, HANDLE, INVALID_HANDLE_VALUE,
        STATUS_INFO_LENGTH_MISMATCH, NTSTATUS,
    };
    use windows_sys::Win32::Storage::FileSystem::{
        GetFinalPathNameByHandleW, FILE_GENERIC_WRITE, FILE_NAME_NORMALIZED,
    };
    use windows_sys::Win32::System::Threading::{
        GetCurrentProcess, OpenProcess, PROCESS_DUP_HANDLE,
    };
    use windows_sys::Wdk::System::SystemInformation::{
        NtQuerySystemInformation,
    };

    // Manual SystemExtendedHandleInformation = 0x40 (windows-sys
    // doesn't expose the constant for the extended variant).
    const SYSTEM_EXTENDED_HANDLE_INFORMATION: i32 = 0x40;
    // Object type index for File on Windows is variable across Windows
    // versions; rather than enumerate the type table we filter by
    // GrantedAccess including FILE_GENERIC_WRITE bits.

    #[repr(C)]
    struct SystemHandleInformationEx {
        number_of_handles: usize,
        reserved: usize,
        handles: [SystemHandleTableEntryInfoEx; 1],
    }

    #[repr(C)]
    #[derive(Clone, Copy)]
    struct SystemHandleTableEntryInfoEx {
        object: *mut std::ffi::c_void,
        unique_process_id: usize,
        handle_value: usize,
        granted_access: u32,
        creator_back_trace_index: u16,
        object_type_index: u16,
        handle_attributes: u32,
        reserved: u32,
    }

    pub fn read(pid: u32, limit: usize) -> Vec<PathBuf> {
        let target_pid = pid as usize;
        // Step 1: query the global handle table.  Windows refuses if
        // we don't pre-size the buffer, so loop until the call fits.
        let mut buf: Vec<u8> = vec![0u8; 256 * 1024];
        let table: *const SystemHandleInformationEx = loop {
            let mut needed: u32 = 0;
            let status: NTSTATUS = unsafe {
                NtQuerySystemInformation(
                    SYSTEM_EXTENDED_HANDLE_INFORMATION,
                    buf.as_mut_ptr() as *mut _,
                    buf.len() as u32,
                    &mut needed,
                )
            };
            if status == 0 { break buf.as_ptr() as *const _; }
            if status == STATUS_INFO_LENGTH_MISMATCH {
                // Grow generously — the handle table on a busy box can
                // be many MiB.  Cap at 64 MiB so a runaway query can't
                // OOM agtop.
                let grow = ((needed as usize).max(buf.len() * 2)).min(64 * 1024 * 1024);
                if grow == buf.len() { return Vec::new(); }
                buf.resize(grow, 0);
                continue;
            }
            return Vec::new();
        };

        // Step 2: enumerate, filtering to (a) handles owned by our PID
        // and (b) handles with FILE_GENERIC_WRITE in their granted-access
        // mask.  Open a single dup-source handle on the target process.
        let proc_handle: HANDLE = unsafe {
            OpenProcess(PROCESS_DUP_HANDLE, 0, pid)
        };
        if proc_handle.is_null() || proc_handle == INVALID_HANDLE_VALUE {
            return Vec::new();
        }

        let header = unsafe { &*table };
        let count = header.number_of_handles;
        let entries: *const SystemHandleTableEntryInfoEx = unsafe {
            (table as *const u8)
                .add(std::mem::size_of::<usize>() * 2) // skip number_of_handles + reserved
                as *const SystemHandleTableEntryInfoEx
        };

        let mut out: Vec<PathBuf> = Vec::with_capacity(limit);
        let me = unsafe { GetCurrentProcess() };

        for i in 0..count {
            if out.len() >= limit { break; }
            let entry = unsafe { *entries.add(i) };
            if entry.unique_process_id != target_pid { continue; }
            // FILE_GENERIC_WRITE = STANDARD_RIGHTS_WRITE | FILE_WRITE_DATA |
            // FILE_WRITE_ATTRIBUTES | FILE_WRITE_EA | SYNCHRONIZE = 0x120116.
            // Match any handle whose grant overlaps the write-data bit.
            if entry.granted_access & FILE_GENERIC_WRITE == 0 { continue; }

            // Step 3: duplicate into our process so we can resolve a path.
            let mut dup: HANDLE = std::ptr::null_mut();
            let ok = unsafe {
                DuplicateHandle(
                    proc_handle,
                    entry.handle_value as HANDLE,
                    me,
                    &mut dup,
                    0,
                    0,
                    DUPLICATE_SAME_ACCESS,
                )
            };
            if ok == 0 || dup.is_null() { continue; }

            // Step 4: ask Windows for the final path.
            let mut wbuf = [0u16; 32_768];
            let n = unsafe {
                GetFinalPathNameByHandleW(dup, wbuf.as_mut_ptr(), wbuf.len() as u32, FILE_NAME_NORMALIZED)
            };
            unsafe { CloseHandle(dup); }
            if n == 0 || (n as usize) > wbuf.len() { continue; }
            let s = OsString::from_wide(&wbuf[..n as usize]);
            let s = s.to_string_lossy();
            // Strip the `\\?\` long-path prefix Windows returns.
            let trimmed = s.strip_prefix(r"\\?\").unwrap_or(&s);
            // Skip device paths.
            if trimmed.starts_with(r"\Device\") { continue; }
            out.push(PathBuf::from(trimmed));
        }

        unsafe { CloseHandle(proc_handle); }
        out
    }
}

// ── FreeBSD / DragonFly: libprocstat ───────────────────────────────────────
//
// FreeBSD ships libprocstat (since 9.x) which gives us proper per-fd
// path resolution — the same data `fstat -p <pid>` reports.  DragonFly
// has a compatible procstat fork.
//
// OpenBSD / NetBSD don't track per-fd paths in the kernel (their
// kvm_getfiles returns inode + dev only, no path), so FD enumeration
// there can return numbers but not paths — almost useless for agtop's
// "writing files" surface.  Falls through to the empty-Vec stub.
#[cfg(any(target_os = "freebsd", target_os = "dragonfly"))]
mod impl_ {
    use super::*;
    use std::ffi::{c_char, c_int, c_uint, c_void, CStr};

    // sys/user.h — type tags for libprocstat's filestat.fs_type.
    const PS_FST_TYPE_VNODE:  c_int = 1;
    // sys/user.h — bits in filestat.fs_flags.
    const PS_FST_FFLAG_WRITE: c_int = 0x0002;
    // sys/sysctl.h — what selector for procstat_getprocs.
    const KERN_PROC_PID:      c_int = 1;

    /// Mirror of `struct filestat` from <libprocstat.h>.  Layout has
    /// been stable since FreeBSD 9.0 — fs_path's offset specifically
    /// is part of the public ABI surface.  We only read scalar fields
    /// and the path pointer; we never construct or modify one.
    #[repr(C)]
    struct FileStat {
        fs_type:       c_int,
        fs_flags:      c_int,
        fs_fflags:     c_int,
        fs_uflags:     c_int,
        fs_fd:         c_int,
        fs_ref_count:  c_int,
        fs_offset:     i64,
        fs_typedep:    *mut c_void,
        fs_path:       *mut c_char,
        // STAILQ_ENTRY(filestat) — next pointer at the tail.
        next_stqe_next: *mut FileStat,
    }

    /// Mirror of the STAILQ_HEAD that procstat_getfiles returns.
    #[repr(C)]
    struct FileStatList {
        stqh_first: *mut FileStat,
        stqh_last:  *mut *mut FileStat,
    }

    // libprocstat symbols.  kinfo_proc is opaque to us — we never
    // dereference it, just thread the pointer through the API.
    #[link(name = "procstat")]
    extern "C" {
        fn procstat_open_sysctl() -> *mut c_void;
        fn procstat_close(ps: *mut c_void);
        fn procstat_getprocs(
            ps: *mut c_void, what: c_int, arg: c_int, count: *mut c_uint,
        ) -> *mut c_void;       // returns kinfo_proc *
        fn procstat_freeprocs(ps: *mut c_void, p: *mut c_void);
        fn procstat_getfiles(
            ps: *mut c_void, p: *mut c_void, mmapped: c_int,
        ) -> *mut FileStatList;
        fn procstat_freefiles(ps: *mut c_void, head: *mut FileStatList);
    }

    pub fn read(pid: u32, limit: usize) -> Vec<PathBuf> {
        unsafe {
            let ps = procstat_open_sysctl();
            if ps.is_null() { return Vec::new(); }

            let mut count: c_uint = 0;
            let kproc = procstat_getprocs(ps, KERN_PROC_PID, pid as c_int, &mut count);
            if kproc.is_null() || count == 0 {
                procstat_close(ps);
                return Vec::new();
            }

            let head = procstat_getfiles(ps, kproc, 0);
            if head.is_null() {
                procstat_freeprocs(ps, kproc);
                procstat_close(ps);
                return Vec::new();
            }

            let mut out: Vec<PathBuf> = Vec::with_capacity(limit);
            let mut node = (*head).stqh_first;
            while !node.is_null() {
                if out.len() >= limit { break; }
                let f = &*node;
                // Vnode entries are the only ones that carry a real
                // filesystem path; pipes / sockets / kqueues have null
                // fs_path even when fs_flags includes WRITE.
                let writable = (f.fs_flags & PS_FST_FFLAG_WRITE) != 0;
                let is_vnode = f.fs_type == PS_FST_TYPE_VNODE;
                if writable && is_vnode && !f.fs_path.is_null() {
                    let cstr = CStr::from_ptr(f.fs_path);
                    if let Ok(s) = cstr.to_str() {
                        if !s.is_empty() && !s.starts_with("/dev/") {
                            out.push(PathBuf::from(s));
                        }
                    }
                }
                node = f.next_stqe_next;
            }

            procstat_freefiles(ps, head);
            procstat_freeprocs(ps, kproc);
            procstat_close(ps);
            out
        }
    }
}

// ── OpenBSD / NetBSD: stub ─────────────────────────────────────────────────
//
// kvm_getfiles returns inode + dev but no path; reconstructing paths
// would require walking every mounted filesystem and reverse-mapping
// inodes, which is both expensive and unreliable.  Out of scope.
#[cfg(any(target_os = "openbsd", target_os = "netbsd"))]
mod impl_ {
    use super::*;
    pub fn read(_pid: u32, _limit: usize) -> Vec<PathBuf> { Vec::new() }
}

// ── Anything else (illumos, Solaris, Haiku, ...) ───────────────────────────
#[cfg(not(any(
    target_os = "linux", target_os = "macos", windows,
    target_os = "freebsd", target_os = "dragonfly",
    target_os = "openbsd", target_os = "netbsd",
)))]
mod impl_ {
    use super::*;
    pub fn read(_pid: u32, _limit: usize) -> Vec<PathBuf> { Vec::new() }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Write;
    use std::fs::OpenOptions;

    /// Cross-platform end-to-end test.  Opens a tempfile for writing
    /// and asserts our writable-FD enumerator finds it in the current
    /// process's open-file set.  Runs identically on Linux, macOS,
    /// and Windows; on FreeBSD it skips with a notice (no impl).
    #[test]
    #[cfg(any(target_os = "linux", target_os = "macos", windows))]
    fn enumerates_self_open_writable_file() {
        let tmp = tempfile::NamedTempFile::new().unwrap();
        let path = tmp.path().to_path_buf();
        // Re-open with explicit write so the OS records us as writer.
        let mut f = OpenOptions::new().write(true).open(&path).unwrap();
        writeln!(f, "agtop-test").unwrap();
        f.sync_all().unwrap();
        // Keep `f` alive for the duration of the read.
        let pid = std::process::id();
        let files = read(pid, 4096);
        // Drop after enumeration so the assertion message can reference path.
        let canon_target = std::fs::canonicalize(&path).unwrap_or(path.clone());
        // Match either the raw or canonicalised path; macOS often
        // returns the resolved /private/var/folders/... path while
        // Linux / Windows return what was opened.
        let found = files.iter().any(|p| {
            p == &path || p == &canon_target
                // Substring fallback for cases like Windows long-path
                // prefixes / macOS /private/var symlink resolution.
                || p.to_string_lossy().contains(path.file_name().unwrap().to_str().unwrap())
        });
        drop(f);
        drop(tmp);
        assert!(found,
            "writing_files::read({}) did not include the test tempfile.\n\
             Opened: {:?}\n\
             Returned ({} entries):\n{}",
            pid, path, files.len(),
            files.iter().take(20).map(|p| format!("  {}", p.display())).collect::<Vec<_>>().join("\n"));
    }
}
