//
// Syd: rock-solid application kernel
// src/filemap.rs: File descriptor map for path canonicalization
//
// Copyright (c) 2026 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

use std::{
    cell::RefCell,
    mem::MaybeUninit,
    os::fd::{AsRawFd, IntoRawFd},
    slice::from_raw_parts,
};

use libc::c_uint;
use memchr::arch::all::{is_equal, memchr::One};
use nix::errno::Errno;
use rax::RaxMap;

use crate::{
    fd::{closeall, ROOT_FD, ROOT_F_MODE, ROOT_MNT_ID},
    lookup::{FileMapEntry, FileType, MaybeFd},
    path::XPath,
    XPathBuf,
};

// Inner map type.
type InnerMap = RaxMap<XPathBuf, FileMapEntry>;

/// A map that associates paths with file descriptors and metadata.
pub struct FileMap(InnerMap);

impl FileMap {
    /// Find a descendant of the given path, returning its descriptor and relative depth.
    pub(crate) fn find_descendant(&mut self, path: &XPath) -> Option<(MaybeFd, usize)> {
        let mut iter = MaybeUninit::<rax::RaxIterator<XPathBuf, FileMapEntry>>::uninit();

        // SAFETY: self.0.rax is a valid radix tree pointer.
        unsafe { rax::raxStart(&raw mut iter as *mut rax::raxIterator, self.0.rax) };

        // SAFETY: Iterator is initialized with raxStart before use.
        let mut iter = unsafe { iter.assume_init() };

        // Fix self-referential pointers after move.
        iter.fixup();

        let path = path.as_bytes();
        if !iter.seek_bytes(rax::GREATER_EQUAL, path) {
            return None;
        }

        let mut result = None;
        while !iter.eof() {
            // SAFETY: iter.key and key_len are guaranteed by rax to
            // point to a valid byte sequence representing the current
            // key.
            let cpath = unsafe { from_raw_parts(iter.key, iter.key_len) };

            if !(cpath.len() >= path.len() && is_equal(path, &cpath[..path.len()])) {
                break; // prefix not found.
            }

            let left = &cpath[path.len()..];
            if left.is_empty() || left[0] == b'/' || path.last() == Some(&b'/') {
                if let Some(entry) = iter.value() {
                    let depth = One::new(b'/').count(left);
                    result = Some((MaybeFd::RawFd(entry.fd.as_raw_fd()), depth));
                    break;
                }
            }

            iter.next();
        }
        result
    }

    /// Attempt to insert entry or fail with `Errno::ENOMEM`.
    pub(crate) fn try_insert(
        &mut self,
        path: XPathBuf,
        entry: FileMapEntry,
    ) -> Result<Option<FileMapEntry>, Errno> {
        Ok(self
            .0
            .insert(path, Box::new(entry))
            .or(Err(Errno::ENOMEM))?
            .map(|b| *b))
    }

    /// Remove entry from map.
    pub(crate) fn remove(&mut self, path: &XPath) -> Option<FileMapEntry> {
        if path.is_root() {
            self.map_get(path).cloned()
        } else {
            self.map_remove(path)
        }
    }

    /// Query entry and get a reference.
    pub(crate) fn get(&self, path: &XPath) -> Option<&FileMapEntry> {
        self.map_get(path)
    }

    /// Query entry and get a mutable reference.
    pub(crate) fn get_mut(&mut self, path: &XPath) -> Option<&mut FileMapEntry> {
        self.map_get_mut(path)
    }

    fn map_get(&self, path: &XPath) -> Option<&FileMapEntry> {
        let bytes = path.as_bytes();
        // SAFETY: self.0.rax is a valid pointer to a radix tree.  bytes
        // is a valid byte slice, and we pass its pointer and length
        // correctly to raxFind.
        unsafe {
            let value = rax::raxFind(self.0.rax, bytes.as_ptr(), bytes.len());
            if value.is_null() || std::ptr::eq(value, rax::raxNotFound) {
                None
            } else {
                Some(&*(value as *mut libc::c_void as *const FileMapEntry))
            }
        }
    }

    fn map_get_mut(&mut self, path: &XPath) -> Option<&mut FileMapEntry> {
        let bytes = path.as_bytes();
        // SAFETY: self.0.rax is a valid pointer to a radix tree.  bytes
        // is a valid byte slice, and we pass its pointer and length
        // correctly to raxFind.
        unsafe {
            let value = rax::raxFind(self.0.rax, bytes.as_ptr(), bytes.len());
            if value.is_null() || std::ptr::eq(value, rax::raxNotFound) {
                None
            } else {
                Some(&mut *(value as *mut libc::c_void as *mut FileMapEntry))
            }
        }
    }

    fn map_remove(&mut self, path: &XPath) -> Option<FileMapEntry> {
        self.0.remove(XPathBuf::from(path.as_bytes())).1.map(|b| *b)
    }

    /// Attempt to create a new FileMap.
    pub(crate) fn try_new() -> Result<Self, Errno> {
        let mut map = Self(RaxMap::try_new().or(Err(Errno::ENOMEM))?);

        let entry = FileMapEntry::new(
            ROOT_FD().into(),
            Some(FileType::Dir),
            Some(ROOT_F_MODE()),
            Some(ROOT_MNT_ID()),
            None,
        );
        map.try_insert(XPathBuf::from("/"), entry)?;

        Ok(map)
    }

    /// Set default allocator for `FileMap` to hardened-malloc.
    ///
    /// # Safety
    ///
    /// This function is not thread-safe.
    pub fn set_alloc_hardened() {
        #[cfg(all(
            not(coverage),
            not(feature = "prof"),
            not(target_os = "android"),
            not(target_arch = "riscv64"),
            target_page_size_4k,
            target_pointer_width = "64"
        ))]
        {
            extern "C" fn hmalloc(size: usize) -> *mut u8 {
                // SAFETY: Delegate to hardened_malloc.
                unsafe { hardened_malloc::malloc(size).cast() }
            }
            extern "C" fn hrealloc(ptr: *mut libc::c_void, size: usize) -> *mut u8 {
                // SAFETY: Delegate to hardened_malloc.
                unsafe { hardened_malloc::realloc(ptr, size).cast() }
            }
            extern "C" fn hfree(ptr: *mut libc::c_void) {
                // SAFETY: Delegate to hardened_malloc.
                unsafe { hardened_malloc::free(ptr) }
            }
            // SAFETY: This is called once during init, before any threading happens.
            unsafe { rax::set_allocator(hmalloc, hrealloc, hfree) };
        }
    }
}

impl Drop for FileMap {
    fn drop(&mut self) {
        // Collect SafeOwnedFd entries to close.
        #[expect(clippy::cast_possible_truncation)]
        let closefds = RefCell::new(Vec::with_capacity(self.0.len() as usize));

        self.0.iter(|_, iter| {
            if iter.seek_min() {
                loop {
                    if let Some(entry) = iter.value() {
                        let entry = entry as *const _ as *mut FileMapEntry;
                        // SAFETY: entry is procured from a valid
                        // reference to entry. We uniquely mutate it
                        // right before dropping the entire FileMap
                        // tree.
                        unsafe {
                            if let MaybeFd::Owned(fd) = std::mem::take(&mut (*entry).fd) {
                                #[expect(clippy::cast_sign_loss)]
                                closefds.borrow_mut().push(fd.into_raw_fd() as c_uint);
                            }
                        }
                    }
                    if !iter.forward() {
                        break;
                    }
                }
            }
        });

        let mut closefds = closefds.into_inner();
        if closefds.is_empty() {
            return;
        }

        // Close all owned file descriptors at once.
        // Nothing to be done on close_range(2) errors.
        closefds.sort_unstable();
        let _ = closeall(&closefds);
    }
}

#[cfg(test)]
mod tests {
    use std::{os::fd::AsRawFd, sync::Once};

    use super::*;
    use crate::{
        fd::{open_static_files, ROOT_FD},
        lookup::{FileMapEntry, FileType, MaybeFd},
        path::{XPath, XPathBuf},
    };

    fn setup() {
        static INIT: Once = Once::new();
        INIT.call_once(|| {
            open_static_files().unwrap();
        });
    }

    // Helper: build a `FileMapEntry` carrying a raw fd.
    fn entry_raw(fd: i32) -> FileMapEntry {
        FileMapEntry::new(
            MaybeFd::RawFd(fd),
            Some(FileType::Reg),
            Some(0o644),
            Some(1),
            None,
        )
    }

    // Helper: build a directory `FileMapEntry` carrying a raw fd.
    fn entry_dir(fd: i32) -> FileMapEntry {
        FileMapEntry::new(
            MaybeFd::RawFd(fd),
            Some(FileType::Dir),
            Some(0o755),
            Some(1),
            None,
        )
    }

    #[test]
    fn test_filemap_1() {
        setup();
        let map = FileMap::try_new().unwrap();

        // Root entry "/" must exist.
        let root = map.get(XPath::from_bytes(b"/"));
        assert!(root.is_some());

        let entry = root.unwrap();
        assert_eq!(entry.fd.as_raw_fd(), ROOT_FD());
    }

    #[test]
    fn test_filemap_2() {
        setup();
        let map = FileMap::try_new().unwrap();
        let entry = map.get(XPath::from_bytes(b"/")).unwrap();
        assert_eq!(entry.fd.as_raw_fd(), ROOT_FD());
    }

    #[test]
    fn test_filemap_3() {
        setup();
        let mut map = FileMap::try_new().unwrap();

        let path = XPathBuf::from("/home");
        let old = map.try_insert(path, entry_dir(42)).unwrap();
        assert!(old.is_none());

        let got = map.get(XPath::from_bytes(b"/home"));
        assert!(got.is_some());
        assert_eq!(got.unwrap().fd.as_raw_fd(), 42);
    }

    #[test]
    fn test_filemap_4() {
        setup();
        let mut map = FileMap::try_new().unwrap();

        let path = XPathBuf::from("/etc");
        map.try_insert(path.clone(), entry_dir(10)).unwrap();

        let old = map.try_insert(path, entry_dir(20)).unwrap();
        assert!(old.is_some());
        assert_eq!(old.unwrap().fd.as_raw_fd(), 10);

        let got = map.get(XPath::from_bytes(b"/etc"));
        assert_eq!(got.unwrap().fd.as_raw_fd(), 20);
    }

    #[test]
    fn test_filemap_5() {
        setup();
        let mut map = FileMap::try_new().unwrap();

        let paths = ["/a", "/b", "/c", "/a/b", "/a/b/c"];
        for (i, p) in paths.iter().enumerate() {
            let fd = (100 + i) as i32;
            map.try_insert(XPathBuf::from(*p), entry_raw(fd)).unwrap();
        }

        for (i, p) in paths.iter().enumerate() {
            let fd = (100 + i) as i32;
            let e = map.get(XPath::from_bytes(p.as_bytes()));
            assert!(e.is_some(), "entry for {p} must exist");
            assert_eq!(e.unwrap().fd.as_raw_fd(), fd);
        }
    }

    #[test]
    fn test_filemap_6() {
        setup();
        let map = FileMap::try_new().unwrap();

        assert!(map.get(XPath::from_bytes(b"/no/such/path")).is_none());
    }

    #[test]
    fn test_filemap_7() {
        setup();
        let mut map = FileMap::try_new().unwrap();

        map.try_insert(XPathBuf::from("/mutable"), entry_raw(50))
            .unwrap();

        {
            let e = map.get_mut(XPath::from_bytes(b"/mutable")).unwrap();
            e.fd = MaybeFd::RawFd(99);
        }

        let e = map.get(XPath::from_bytes(b"/mutable")).unwrap();
        assert_eq!(e.fd.as_raw_fd(), 99);
    }

    #[test]
    fn test_filemap_8() {
        setup();
        let mut map = FileMap::try_new().unwrap();

        assert!(map.get_mut(XPath::from_bytes(b"/ghost")).is_none());
    }

    #[test]
    fn test_filemap_9() {
        setup();
        let mut map = FileMap::try_new().unwrap();

        map.try_insert(XPathBuf::from("/tmp"), entry_dir(30))
            .unwrap();
        assert!(map.get(XPath::from_bytes(b"/tmp")).is_some());

        let removed = map.remove(XPath::from_bytes(b"/tmp"));
        assert!(removed.is_some());
        assert_eq!(removed.unwrap().fd.as_raw_fd(), 30);

        assert!(map.get(XPath::from_bytes(b"/tmp")).is_none());
    }

    #[test]
    fn test_filemap_10() {
        setup();
        let mut map = FileMap::try_new().unwrap();

        let removed = map.remove(XPath::from_bytes(b"/nonexistent"));
        assert!(removed.is_none());
    }

    #[test]
    fn test_filemap_11() {
        setup();
        let mut map = FileMap::try_new().unwrap();

        let removed = map.remove(XPath::from_bytes(b"/"));
        assert!(removed.is_some());
        assert!(map.get(XPath::from_bytes(b"/")).is_some());
    }

    #[test]
    fn test_filemap_12() {
        setup();
        let mut map = FileMap::try_new().unwrap();

        map.try_insert(XPathBuf::from("/usr"), entry_dir(60))
            .unwrap();

        let result = map.find_descendant(XPath::from_bytes(b"/usr"));
        assert!(result.is_some());
        let (fd, depth) = result.unwrap();
        assert_eq!(fd.as_raw_fd(), 60);
        assert_eq!(depth, 0);
    }

    #[test]
    fn test_filemap_13() {
        setup();
        let mut map = FileMap::try_new().unwrap();

        map.try_insert(XPathBuf::from("/usr/bin"), entry_dir(70))
            .unwrap();

        let result = map.find_descendant(XPath::from_bytes(b"/usr"));
        assert!(result.is_some());
        let (fd, depth) = result.unwrap();
        assert_eq!(fd.as_raw_fd(), 70);
        assert_eq!(depth, 1);
    }

    #[test]
    fn test_filemap_14() {
        setup();
        let mut map = FileMap::try_new().unwrap();

        map.try_insert(XPathBuf::from("/a/b/c/d/e"), entry_raw(80))
            .unwrap();

        let result = map.find_descendant(XPath::from_bytes(b"/a"));
        assert!(result.is_some());
        let (fd, depth) = result.unwrap();
        assert_eq!(fd.as_raw_fd(), 80);
        assert_eq!(depth, 4, "/a/b/c/d/e is 4 levels below /a");
    }

    #[test]
    fn test_filemap_15() {
        setup();
        let mut map = FileMap::try_new().unwrap();

        map.try_insert(XPathBuf::from("/opt/foo"), entry_raw(90))
            .unwrap();

        let result = map.find_descendant(XPath::from_bytes(b"/var"));
        assert!(result.is_none());
    }

    #[test]
    fn test_filemap_16() {
        setup();
        let mut map = FileMap::try_new().unwrap();

        map.try_insert(XPathBuf::from("/usr"), entry_dir(60))
            .unwrap();
        map.try_insert(XPathBuf::from("/usrlocal"), entry_raw(61))
            .unwrap();

        let result = map.find_descendant(XPath::from_bytes(b"/usr"));
        assert!(result.is_some());
        let (fd, depth) = result.unwrap();
        assert_eq!(fd.as_raw_fd(), 60);
        assert_eq!(depth, 0);
    }

    #[test]
    fn test_filemap_17() {
        setup();
        let mut map = FileMap::try_new().unwrap();

        map.try_insert(XPathBuf::from("/lib/a"), entry_raw(100))
            .unwrap();
        map.try_insert(XPathBuf::from("/lib/b"), entry_raw(101))
            .unwrap();

        let result = map.find_descendant(XPath::from_bytes(b"/lib"));
        assert!(result.is_some());
        let (fd, depth) = result.unwrap();
        assert_eq!(fd.as_raw_fd(), 100);
        assert_eq!(depth, 1);
    }

    #[test]
    fn test_filemap_18() {
        setup();
        let mut map = FileMap::try_new().unwrap();

        let result = map.find_descendant(XPath::from_bytes(b"/"));
        assert!(result.is_some());
        let (fd, depth) = result.unwrap();
        assert_eq!(fd.as_raw_fd(), ROOT_FD());
        assert_eq!(depth, 0);
    }

    #[test]
    fn test_filemap_19() {
        setup();
        let mut map = FileMap::try_new().unwrap();

        map.try_insert(XPathBuf::from("/srv"), entry_dir(110))
            .unwrap();

        let result = map.find_descendant(XPath::from_bytes(b"/"));
        assert!(result.is_some());
        let (fd, _depth) = result.unwrap();
        assert_eq!(fd.as_raw_fd(), ROOT_FD());
    }

    #[test]
    fn test_filemap_20() {
        setup();
        let mut map = FileMap::try_new().unwrap();

        for i in 0..u16::MAX {
            let path = format!("/large/{i}");
            let fd = (1000i32 + i as i32) as i32;
            map.try_insert(XPathBuf::from(path), entry_raw(fd)).unwrap();
        }

        for i in 0..u16::MAX {
            let path = format!("/large/{i}");
            let fd = (1000i32 + i as i32) as i32;
            let e = map.get(XPath::from_bytes(path.as_bytes()));
            assert!(e.is_some(), "entry {path} must exist");
            assert_eq!(e.unwrap().fd.as_raw_fd(), fd);
        }

        let result = map.find_descendant(XPath::from_bytes(b"/large"));
        assert!(result.is_some());
    }

    #[test]
    fn test_filemap_21() {
        setup();
        let mut map = FileMap::try_new().unwrap();

        let path = XPathBuf::from("/cycle");
        map.try_insert(path.clone(), entry_raw(200)).unwrap();
        assert_eq!(
            map.get(XPath::from_bytes(b"/cycle"))
                .unwrap()
                .fd
                .as_raw_fd(),
            200
        );

        let removed = map.remove(XPath::from_bytes(b"/cycle"));
        assert!(removed.is_some());
        assert!(map.get(XPath::from_bytes(b"/cycle")).is_none());

        map.try_insert(path, entry_raw(300)).unwrap();
        assert_eq!(
            map.get(XPath::from_bytes(b"/cycle"))
                .unwrap()
                .fd
                .as_raw_fd(),
            300
        );
    }

    #[test]
    fn test_filemap_22() {
        setup();
        let mut map = FileMap::try_new().unwrap();

        let deep = "/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p";
        map.try_insert(XPathBuf::from(deep), entry_raw(400))
            .unwrap();

        let e = map.get(XPath::from_bytes(deep.as_bytes()));
        assert!(e.is_some());
        assert_eq!(e.unwrap().fd.as_raw_fd(), 400);

        let result = map.find_descendant(XPath::from_bytes(b"/a"));
        assert!(result.is_some());
        let (_fd, depth) = result.unwrap();
        assert_eq!(depth, 15);
    }

    #[test]
    fn test_filemap_23() {
        setup();
        let mut map = FileMap::try_new().unwrap();

        map.try_insert(XPathBuf::from("/usr/lib/lib.so.6"), entry_raw(500))
            .unwrap();

        let e = map.get(XPath::from_bytes(b"/usr/lib/lib.so.6"));
        assert!(e.is_some());
        assert_eq!(e.unwrap().fd.as_raw_fd(), 500);
    }

    #[test]
    fn test_filemap_24() {
        setup();
        let mut map = FileMap::try_new().unwrap();

        for i in 0..10 {
            let fd = (9000 + i) as i32;
            let path = format!("/drop_test/{i}");
            map.try_insert(XPathBuf::from(path), entry_raw(fd)).unwrap();
        }
    }

    #[test]
    fn test_filemap_25() {
        setup();
        let mut map = FileMap::try_new().unwrap();

        map.try_insert(XPathBuf::from("/data/file"), entry_raw(600))
            .unwrap();

        let result = map.find_descendant(XPath::from_bytes(b"/data/"));
        assert!(result.is_some());
        let (fd, depth) = result.unwrap();
        assert_eq!(fd.as_raw_fd(), 600);
        assert_eq!(depth, 0);
    }

    #[test]
    fn test_filemap_26() {
        setup();
        let mut map = FileMap::try_new().unwrap();

        map.try_insert(XPathBuf::from("/foobar"), entry_raw(700))
            .unwrap();
        let result = map.find_descendant(XPath::from_bytes(b"/foo"));
        assert!(result.is_none());
    }

    #[test]
    fn test_filemap_27() {
        setup();
        let mut map = FileMap::try_new().unwrap();

        map.try_insert(XPathBuf::from("/foo/bar"), entry_raw(710))
            .unwrap();
        map.try_insert(XPathBuf::from("/foobar"), entry_raw(720))
            .unwrap();

        let result = map.find_descendant(XPath::from_bytes(b"/foo"));
        assert!(result.is_some());
        let (fd, depth) = result.unwrap();
        assert_eq!(fd.as_raw_fd(), 710);
        assert_eq!(depth, 1);
    }
}
