// Host-side filesystem backend for the FUSE server.
//
// The `FsBackend` trait is the boundary between FUSE protocol decoding
// (which lives in `super::server`) and actual filesystem operations
// (which differ per impl):
//
//   `MemoryFs`  — in-memory tree, used by tests; deterministic,
//                 no host fs dependency.
//   `PosixFs`   — real host filesystem (lands in a subsequent slice
//                 once the FUSE handlers exist + are tested).
//   `DaemonFs`  — placeholder for when we extract the FUSE server
//                 into a shared per-host daemon for cross-VM page
//                 sharing. The trait stays the same; only the
//                 transport changes.
//
// The trait is `&self` for queries and uses interior mutability where
// state changes (handle table, ref counts). This is what lets us share
// a single `Arc<dyn FsBackend>` across multiple worker threads later
// without recursive locking.

use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;
use std::sync::Mutex;

use super::protocol::{Attr, SetattrIn, S_IFDIR, S_IFREG};

/// FUSE error space — negated errno. The server wraps `Result<_, Errno>`
/// into OutHeader.error.
pub type Errno = i32;

pub const ENOENT: Errno = -2;
pub const EIO: Errno = -5;
pub const EBADF: Errno = -9;
pub const EACCES: Errno = -13;
pub const EEXIST: Errno = -17;
pub const ENOTDIR: Errno = -20;
pub const EISDIR: Errno = -21;
pub const EINVAL: Errno = -22;
pub const ENOSPC: Errno = -28;
pub const EROFS: Errno = -30;
pub const ENAMETOOLONG: Errno = -36;
pub const ENOSYS: Errno = -38;
pub const ENOTSUP: Errno = -45;
pub const EPERM: Errno = -1;

/// Result of a LOOKUP: nodeid + attr.
#[derive(Clone, Debug)]
pub struct Entry {
    pub nodeid: u64,
    pub generation: u64,
    pub attr: Attr,
    /// How long the kernel may cache this entry (seconds).
    pub entry_valid: u64,
    pub attr_valid: u64,
}

/// One readdir entry emitted by the backend. The server packs these
/// into the wire format (dirent header + name + 8-byte align).
#[derive(Clone, Debug)]
pub struct DirEntry {
    pub ino: u64,
    pub name: Vec<u8>,
    /// dt_* type code (see protocol::DT_REG, DT_DIR, …).
    pub typ: u32,
}

/// Filesystem statistics returned by FUSE_STATFS.
#[derive(Clone, Copy, Debug)]
pub struct StatFs {
    pub blocks: u64,
    pub bfree: u64,
    pub bavail: u64,
    pub files: u64,
    pub ffree: u64,
    pub bsize: u32,
    pub namelen: u32,
    pub frsize: u32,
}

/// Host-side filesystem behind a FUSE mount.
///
/// All methods return `Errno` (negated errno) on failure. Implementors
/// must:
///   - Be `Send + Sync` so a shared daemon can drive multiple VMs.
///   - Tolerate concurrent calls — use interior mutability when needed.
///   - Maintain stable nodeids across the lifetime of the mount
///     (snapshot/restore re-establishes the mount, so nodeids only
///     have to be stable WITHIN one mount session).
pub trait FsBackend: Send + Sync {
    /// Resolve a name under `parent`. Returns the child's Entry on hit,
    /// `ENOENT` on miss. Implementations are expected to bump a
    /// reference count so that subsequent FUSE_FORGET releases it.
    fn lookup(&self, parent: u64, name: &OsStr) -> Result<Entry, Errno>;

    /// Drop `nlookup` references to `nodeid`. May free the inode if the
    /// count hits zero. Never returns an error in FUSE (the protocol
    /// doesn't carry a reply for FORGET); we use `()` instead of Errno.
    fn forget(&self, nodeid: u64, nlookup: u64);

    /// Get attributes of `nodeid`. `fh` is set when called from a
    /// file-descriptor context (the guest may pass FUSE_GETATTR_FH).
    fn getattr(&self, nodeid: u64, fh: Option<u64>) -> Result<Attr, Errno>;

    /// Open a regular file. Returns a backend-specific file handle
    /// that the server stores in the handle table.
    fn open(&self, nodeid: u64, flags: u32) -> Result<u64, Errno>;

    /// Read up to `size` bytes at `offset` from the file behind `fh`.
    /// Returns the actual bytes (may be shorter at EOF).
    fn read(&self, nodeid: u64, fh: u64, offset: u64, size: u32) -> Result<Vec<u8>, Errno>;

    /// Write `data` at `offset` into the file behind `fh`.
    /// Returns the number of bytes written. Default impl returns
    /// `EROFS` so read-only backends don't have to override.
    fn write(
        &self,
        _nodeid: u64,
        _fh: u64,
        _offset: u64,
        _data: &[u8],
    ) -> Result<u32, Errno> {
        Err(EROFS)
    }

    /// Flush any buffered writes for `fh` to durable storage.
    /// Called on FUSE_FSYNC; default is no-op for backends that don't
    /// buffer (POSIX pread/pwrite goes through the kernel directly).
    fn fsync(&self, _nodeid: u64, _fh: u64, _datasync: bool) -> Result<(), Errno> {
        Ok(())
    }

    /// Release a previously-opened file handle.
    fn release(&self, nodeid: u64, fh: u64) -> Result<(), Errno>;

    /// Open a directory. Returns a handle; readdir's `offset` is
    /// per-handle.
    fn opendir(&self, nodeid: u64, flags: u32) -> Result<u64, Errno>;

    /// List directory entries starting at `offset`. The backend
    /// returns a logical position in each entry; the kernel passes
    /// the last one back as `offset` on the next call. Implementations
    /// may return fewer entries than would fit in `size` bytes.
    fn readdir(
        &self,
        nodeid: u64,
        fh: u64,
        offset: u64,
        size: u32,
    ) -> Result<Vec<DirEntry>, Errno>;

    /// Release a previously-opened directory handle.
    fn releasedir(&self, nodeid: u64, fh: u64) -> Result<(), Errno>;

    /// Filesystem-wide statistics. Used by `df` and similar.
    fn statfs(&self, nodeid: u64) -> Result<StatFs, Errno>;

    /// FUSE_CREATE: atomic lookup-or-create. Creates `parent/name` as
    /// a regular file with `mode` perms, opens it with `flags`,
    /// returns (Entry, fh). Default returns EROFS so read-only
    /// backends are inert.
    fn create(
        &self,
        _parent: u64,
        _name: &OsStr,
        _mode: u32,
        _flags: u32,
    ) -> Result<(Entry, u64), Errno> {
        Err(EROFS)
    }

    /// FUSE_MKDIR: create directory.
    fn mkdir(
        &self,
        _parent: u64,
        _name: &OsStr,
        _mode: u32,
    ) -> Result<Entry, Errno> {
        Err(EROFS)
    }

    /// FUSE_UNLINK: remove a name (non-dir).
    fn unlink(&self, _parent: u64, _name: &OsStr) -> Result<(), Errno> {
        Err(EROFS)
    }

    /// FUSE_RMDIR: remove an empty directory.
    fn rmdir(&self, _parent: u64, _name: &OsStr) -> Result<(), Errno> {
        Err(EROFS)
    }

    /// FUSE_SYMLINK: create a symbolic link at `parent/name` pointing to
    /// `target`. The target is stored verbatim (POSIX symlink(2)) — the
    /// host does NOT resolve or validate it. Returns the new symlink's
    /// Entry. Default returns EROFS so read-only backends are inert.
    fn symlink(
        &self,
        _parent: u64,
        _name: &OsStr,
        _target: &OsStr,
    ) -> Result<Entry, Errno> {
        Err(EROFS)
    }

    /// FUSE_READLINK: return the target bytes of a symbolic link.
    /// Default returns EINVAL (matches readlink(2) on a non-symlink).
    fn readlink(&self, _nodeid: u64) -> Result<Vec<u8>, Errno> {
        Err(EINVAL)
    }

    /// FUSE_LINK: create a hard link at `new_parent/new_name` referring
    /// to the same inode as `nodeid`. Hard links are inside-mount only —
    /// the host's link(2) returns EXDEV across filesystems and our
    /// mount is one FS, so they cannot escape. Default returns EROFS.
    fn link(
        &self,
        _nodeid: u64,
        _new_parent: u64,
        _new_name: &OsStr,
    ) -> Result<Entry, Errno> {
        Err(EROFS)
    }

    /// FUSE_SETATTR: apply selected attribute changes (mode, owner,
    /// size, atime/mtime) to `nodeid`. The `valid` bitmask on
    /// [`SetattrIn`] indicates which fields the kernel wants applied;
    /// everything else MUST be ignored. The reply is the post-change
    /// attribute set.
    ///
    /// `fh` is `Some` when the kernel issued the setattr against an
    /// open file (e.g. ftruncate after write), so backends can use the
    /// fd to avoid an extra path resolution.
    ///
    /// Default returns `EROFS` so read-only backends are inert.
    fn setattr(
        &self,
        _nodeid: u64,
        _fh: Option<u64>,
        _attr: SetattrIn,
    ) -> Result<Attr, Errno> {
        Err(EROFS)
    }

    /// FUSE_RENAME / FUSE_RENAME2: rename `old_parent/old_name` to
    /// `new_parent/new_name`. `flags` carries `RENAME_NOREPLACE` /
    /// `RENAME_EXCHANGE` for the Rename2 path; 0 means vanilla rename.
    /// Default returns `EROFS`.
    fn rename(
        &self,
        _old_parent: u64,
        _old_name: &OsStr,
        _new_parent: u64,
        _new_name: &OsStr,
        _flags: u32,
    ) -> Result<(), Errno> {
        Err(EROFS)
    }

    /// FUSE_FLUSH: sent on every `close(2)` of a file. The semantics
    /// in the FUSE spec are "best-effort sync"; backends that don't
    /// buffer can no-op. Critically NOT a release — `fh` remains valid
    /// until the matching FUSE_RELEASE arrives. Default = Ok.
    fn flush(&self, _nodeid: u64, _fh: u64) -> Result<(), Errno> {
        Ok(())
    }

    /// FUSE_FSYNCDIR: sync a directory's contents to durable storage.
    /// Default is a no-op since most host filesystems sync directory
    /// entries on every dentry mutation already.
    fn fsyncdir(&self, _nodeid: u64, _fh: u64, _datasync: bool) -> Result<(), Errno> {
        Ok(())
    }

    /// DAX critical path: produce a host VA covering
    /// `[foffset, foffset+len)` of the file behind `(nodeid, fh)`.
    /// The returned pointer must remain valid until the matching
    /// `dax_unmap` call. Backends that can't honor DAX (e.g. the
    /// in-memory test fs) return `ENOTSUP`.
    ///
    /// Implementation contract:
    /// - `len` is a multiple of the host page granule (16 KiB on
    ///   Apple Silicon); FUSE_INIT advertises this via map_alignment.
    /// - `prot` is `DAX_PROT_*` bits from `dax::DAX_PROT_*`.
    /// - The pointer must be addressable AND mapped MAP_SHARED if
    ///   the file is real, so writes from the guest propagate back
    ///   through the host's page cache. (Validated by spike 22.)
    fn dax_map(
        &self,
        _nodeid: u64,
        _fh: u64,
        _foffset: u64,
        _len: u64,
        _prot: u32,
    ) -> Result<*mut u8, Errno> {
        Err(ENOTSUP)
    }

    /// Release a host VA previously returned by `dax_map`. Best-effort
    /// per backend (POSIX would munmap; in-memory backends may no-op).
    fn dax_unmap(&self, _nodeid: u64, _host_va: *mut u8, _len: u64) -> Result<(), Errno> {
        Err(ENOTSUP)
    }
}

// ====================================================================
// MemoryFs — in-memory tree used for tests.
// ====================================================================

/// Internal node kinds.
#[derive(Clone, Debug)]
enum Node {
    Dir(Vec<(Vec<u8>, u64)>),
    File(Vec<u8>),
}

struct State {
    /// nodeid → (Node, Attr, lookup_count).
    inodes: std::collections::BTreeMap<u64, (Node, Attr, u64)>,
    /// fh → (nodeid, is_dir).
    handles: std::collections::BTreeMap<u64, (u64, bool)>,
    next_nodeid: u64,
    next_fh: u64,
}

/// In-memory filesystem suitable for unit tests. Initialized with a
/// root directory; tests can populate via [`MemoryFs::add_file`] and
/// [`MemoryFs::add_dir`].
pub struct MemoryFs {
    st: Mutex<State>,
}

impl Default for MemoryFs {
    fn default() -> Self {
        Self::new()
    }
}

impl MemoryFs {
    pub fn new() -> Self {
        let mut inodes = std::collections::BTreeMap::new();
        let mut root_attr = Attr::default();
        root_attr.ino = super::protocol::FUSE_ROOT_ID;
        root_attr.mode = S_IFDIR | 0o755;
        root_attr.nlink = 2;
        root_attr.size = 0;
        root_attr.blksize = 4096;
        inodes.insert(
            super::protocol::FUSE_ROOT_ID,
            (Node::Dir(Vec::new()), root_attr, 0),
        );
        Self {
            st: Mutex::new(State {
                inodes,
                handles: std::collections::BTreeMap::new(),
                next_nodeid: super::protocol::FUSE_ROOT_ID + 1,
                next_fh: 1,
            }),
        }
    }

    /// Create a file under `parent` named `name` with `contents`.
    /// Returns the new nodeid.
    pub fn add_file(&self, parent: u64, name: &[u8], contents: Vec<u8>) -> u64 {
        let mut st = self.st.lock().unwrap();
        let nodeid = st.next_nodeid;
        st.next_nodeid += 1;
        let mut attr = Attr::default();
        attr.ino = nodeid;
        attr.mode = S_IFREG | 0o644;
        attr.nlink = 1;
        attr.size = contents.len() as u64;
        attr.blocks = contents.len() as u64 / 512 + 1;
        attr.blksize = 4096;
        st.inodes.insert(nodeid, (Node::File(contents), attr, 0));
        // Link into parent.
        if let Some((Node::Dir(entries), _, _)) = st.inodes.get_mut(&parent) {
            entries.push((name.to_vec(), nodeid));
        } else {
            panic!("parent {parent} is not a directory");
        }
        nodeid
    }

    /// Create a directory under `parent`. Returns the new nodeid.
    pub fn add_dir(&self, parent: u64, name: &[u8]) -> u64 {
        let mut st = self.st.lock().unwrap();
        let nodeid = st.next_nodeid;
        st.next_nodeid += 1;
        let mut attr = Attr::default();
        attr.ino = nodeid;
        attr.mode = S_IFDIR | 0o755;
        attr.nlink = 2;
        attr.blksize = 4096;
        st.inodes.insert(nodeid, (Node::Dir(Vec::new()), attr, 0));
        if let Some((Node::Dir(entries), _, _)) = st.inodes.get_mut(&parent) {
            entries.push((name.to_vec(), nodeid));
        } else {
            panic!("parent {parent} is not a directory");
        }
        nodeid
    }
}

impl FsBackend for MemoryFs {
    fn lookup(&self, parent: u64, name: &OsStr) -> Result<Entry, Errno> {
        let mut st = self.st.lock().unwrap();
        let entries = match st.inodes.get(&parent) {
            Some((Node::Dir(es), _, _)) => es.clone(),
            Some(_) => return Err(ENOTDIR),
            None => return Err(ENOENT),
        };
        let name_bytes = name.as_bytes();
        let target = entries
            .iter()
            .find(|(n, _)| n == name_bytes)
            .map(|(_, id)| *id)
            .ok_or(ENOENT)?;
        let (_, attr, lookup_count) = st.inodes.get_mut(&target).ok_or(ENOENT)?;
        *lookup_count += 1;
        Ok(Entry {
            nodeid: target,
            generation: 0,
            attr: *attr,
            entry_valid: 1,
            attr_valid: 1,
        })
    }

    fn forget(&self, nodeid: u64, nlookup: u64) {
        let mut st = self.st.lock().unwrap();
        if let Some((_, _, count)) = st.inodes.get_mut(&nodeid) {
            *count = count.saturating_sub(nlookup);
        }
        // Don't actually delete entries — tests want them stable. Real
        // implementations would free here when count hits 0 AND there
        // are no open handles.
    }

    fn getattr(&self, nodeid: u64, _fh: Option<u64>) -> Result<Attr, Errno> {
        let st = self.st.lock().unwrap();
        st.inodes.get(&nodeid).map(|(_, a, _)| *a).ok_or(ENOENT)
    }

    fn open(&self, nodeid: u64, _flags: u32) -> Result<u64, Errno> {
        let mut st = self.st.lock().unwrap();
        match st.inodes.get(&nodeid) {
            Some((Node::File(_), _, _)) => {}
            Some(_) => return Err(EISDIR),
            None => return Err(ENOENT),
        }
        let fh = st.next_fh;
        st.next_fh += 1;
        st.handles.insert(fh, (nodeid, false));
        Ok(fh)
    }

    fn read(&self, nodeid: u64, fh: u64, offset: u64, size: u32) -> Result<Vec<u8>, Errno> {
        let st = self.st.lock().unwrap();
        let (h_node, is_dir) = *st.handles.get(&fh).ok_or(EBADF)?;
        if is_dir || h_node != nodeid {
            return Err(EBADF);
        }
        let data = match st.inodes.get(&nodeid) {
            Some((Node::File(d), _, _)) => d.clone(),
            Some(_) => return Err(EISDIR),
            None => return Err(ENOENT),
        };
        let off = offset as usize;
        if off >= data.len() {
            return Ok(Vec::new());
        }
        let end = (off + size as usize).min(data.len());
        Ok(data[off..end].to_vec())
    }

    fn release(&self, nodeid: u64, fh: u64) -> Result<(), Errno> {
        let mut st = self.st.lock().unwrap();
        let (h_node, is_dir) = st.handles.remove(&fh).ok_or(EBADF)?;
        if is_dir || h_node != nodeid {
            // Spec is forgiving here; we re-insert for sanity.
            st.handles.insert(fh, (h_node, is_dir));
            return Err(EBADF);
        }
        Ok(())
    }

    fn write(&self, nodeid: u64, fh: u64, offset: u64, data: &[u8]) -> Result<u32, Errno> {
        let mut st = self.st.lock().unwrap();
        let (h_node, is_dir) = *st.handles.get(&fh).ok_or(EBADF)?;
        if is_dir || h_node != nodeid {
            return Err(EBADF);
        }
        let bytes = match st.inodes.get_mut(&nodeid) {
            Some((Node::File(d), attr, _)) => {
                let off = offset as usize;
                let end = off + data.len();
                if d.len() < end {
                    d.resize(end, 0);
                }
                d[off..end].copy_from_slice(data);
                attr.size = d.len() as u64;
                data.len() as u32
            }
            Some(_) => return Err(EISDIR),
            None => return Err(ENOENT),
        };
        Ok(bytes)
    }

    fn opendir(&self, nodeid: u64, _flags: u32) -> Result<u64, Errno> {
        let mut st = self.st.lock().unwrap();
        match st.inodes.get(&nodeid) {
            Some((Node::Dir(_), _, _)) => {}
            Some(_) => return Err(ENOTDIR),
            None => return Err(ENOENT),
        }
        let fh = st.next_fh;
        st.next_fh += 1;
        st.handles.insert(fh, (nodeid, true));
        Ok(fh)
    }

    fn readdir(
        &self,
        nodeid: u64,
        fh: u64,
        offset: u64,
        _size: u32,
    ) -> Result<Vec<DirEntry>, Errno> {
        let st = self.st.lock().unwrap();
        let (h_node, is_dir) = *st.handles.get(&fh).ok_or(EBADF)?;
        if !is_dir || h_node != nodeid {
            return Err(EBADF);
        }
        let entries = match st.inodes.get(&nodeid) {
            Some((Node::Dir(es), _, _)) => es.clone(),
            _ => return Err(ENOTDIR),
        };
        let mut out = Vec::new();
        for (i, (name, child_id)) in entries.iter().enumerate().skip(offset as usize) {
            let typ = match st.inodes.get(child_id) {
                Some((Node::Dir(_), _, _)) => super::protocol::DT_DIR,
                Some((Node::File(_), _, _)) => super::protocol::DT_REG,
                None => super::protocol::DT_UNKNOWN,
            };
            out.push(DirEntry {
                ino: *child_id,
                name: name.clone(),
                typ,
            });
            // Each readdir caller can pass back the last off+1 to
            // continue; that's what `i+1` represents below in DirentHeader.off.
            let _ = i;
        }
        Ok(out)
    }

    fn releasedir(&self, nodeid: u64, fh: u64) -> Result<(), Errno> {
        let mut st = self.st.lock().unwrap();
        let (h_node, is_dir) = st.handles.remove(&fh).ok_or(EBADF)?;
        if !is_dir || h_node != nodeid {
            st.handles.insert(fh, (h_node, is_dir));
            return Err(EBADF);
        }
        Ok(())
    }

    fn statfs(&self, _nodeid: u64) -> Result<StatFs, Errno> {
        Ok(StatFs {
            blocks: 1 << 20,
            bfree: 1 << 19,
            bavail: 1 << 19,
            files: 1 << 16,
            ffree: 1 << 15,
            bsize: 4096,
            namelen: 255,
            frsize: 4096,
        })
    }
}

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

    #[test]
    fn memfs_lookup_and_read_round_trip() {
        let fs = MemoryFs::new();
        let f = fs.add_file(super::super::protocol::FUSE_ROOT_ID, b"hello.txt", b"hi!".to_vec());
        let e = fs.lookup(super::super::protocol::FUSE_ROOT_ID, OsStr::new("hello.txt")).unwrap();
        assert_eq!(e.nodeid, f);
        assert_eq!(e.attr.size, 3);
        let fh = fs.open(f, 0).unwrap();
        let data = fs.read(f, fh, 0, 1024).unwrap();
        assert_eq!(&data, b"hi!");
        fs.release(f, fh).unwrap();
    }

    #[test]
    fn memfs_readdir_lists_children_with_types() {
        let fs = MemoryFs::new();
        fs.add_file(super::super::protocol::FUSE_ROOT_ID, b"a.txt", b"a".to_vec());
        fs.add_dir(super::super::protocol::FUSE_ROOT_ID, b"sub");
        let dh = fs.opendir(super::super::protocol::FUSE_ROOT_ID, 0).unwrap();
        let entries = fs.readdir(super::super::protocol::FUSE_ROOT_ID, dh, 0, 4096).unwrap();
        assert_eq!(entries.len(), 2);
        let by_name: std::collections::HashMap<&[u8], u32> =
            entries.iter().map(|e| (e.name.as_slice(), e.typ)).collect();
        assert_eq!(by_name[&b"a.txt"[..]], super::super::protocol::DT_REG);
        assert_eq!(by_name[&b"sub"[..]], super::super::protocol::DT_DIR);
        fs.releasedir(super::super::protocol::FUSE_ROOT_ID, dh).unwrap();
    }

    #[test]
    fn memfs_read_offset_and_eof() {
        let fs = MemoryFs::new();
        let f = fs.add_file(super::super::protocol::FUSE_ROOT_ID, b"x", (0u8..100).collect());
        let fh = fs.open(f, 0).unwrap();
        let chunk = fs.read(f, fh, 50, 10).unwrap();
        assert_eq!(chunk, (50u8..60).collect::<Vec<_>>());
        let eof = fs.read(f, fh, 200, 10).unwrap();
        assert!(eof.is_empty());
    }

    #[test]
    fn memfs_open_directory_errors() {
        let fs = MemoryFs::new();
        let d = fs.add_dir(super::super::protocol::FUSE_ROOT_ID, b"sub");
        let err = fs.open(d, 0).unwrap_err();
        assert_eq!(err, EISDIR);
    }

    #[test]
    fn memfs_lookup_miss_returns_enoent() {
        let fs = MemoryFs::new();
        let err = fs.lookup(super::super::protocol::FUSE_ROOT_ID, OsStr::new("nope")).unwrap_err();
        assert_eq!(err, ENOENT);
    }
}
