//! A read-only filesystem exposing revision resolvers and Git trees.

use super::inode::{InodeRecord, InodeTable};
use super::resolve::{
    is_hex_revision_spec, is_valid_tree_entry_name, join_path_components, reference_candidates,
    reference_revision_name, revision_candidates, split_path_bytes, RefNamespace,
};
use crate::cli::SubmodulesMode;
use crate::config::MountConfig;
use crate::git::{
    CommitInfo, GitObjectId, GitObjectKind, GitReference, GixRepository, HistoryEntry, LfsState,
    ObjectStore, RevResolver, StoreError, TreeEntry, TreeInfo,
};
use anyhow::Result;
use fuser::{
    fuse_forget_one, FileAttr, FileType, Filesystem, KernelConfig, ReplyAttr, ReplyBmap,
    ReplyCreate, ReplyData, ReplyDirectory, ReplyEmpty, ReplyEntry, ReplyIoctl, ReplyLock,
    ReplyLseek, ReplyOpen, ReplyStatfs, ReplyWrite, ReplyXattr, Request,
};
use libc::{
    EACCES, EINVAL, EIO, EISDIR, ENAMETOOLONG, ENODATA, ENOENT, ENOSYS, ENOTDIR, ERANGE, EROFS,
    O_ACCMODE, O_APPEND, O_RDWR, O_TRUNC, O_WRONLY, W_OK,
};
use std::collections::BTreeSet;
use std::convert::TryFrom;
use std::ffi::{OsStr, OsString};
use std::io::{Read, Seek, SeekFrom};
use std::os::unix::ffi::{OsStrExt, OsStringExt};
use std::path::Path;
use std::sync::{Arc, Mutex};
use std::time::{Duration, SystemTime, UNIX_EPOCH};

const BLOCK_SIZE: u32 = 4096;
const DIRECTORY_SIZE: u64 = 4096;
const LONG_TTL: Duration = Duration::from_secs(24 * 60 * 60);
const SHORT_TTL: Duration = Duration::from_secs(1);
const XATTR_OID: &[u8] = b"user.timefs.oid";
const XATTR_MODE: &[u8] = b"user.timefs.mode";
const XATTR_TYPE: &[u8] = b"user.timefs.type";
const XATTR_COMMIT: &[u8] = b"user.timefs.commit";
const XATTR_LFS: &[u8] = b"user.timefs.lfs";

const ROOT_ENTRY_NAMES: [&[u8]; 5] = [b"now", b"at", b"commits", b"refs", b"history"];
const REFS_ENTRY_NAMES: [&[u8]; 3] = [b"heads", b"tags", b"remotes"];

#[derive(Debug)]
pub(crate) struct HeadFilesystem {
    store: Arc<GixRepository>,
    submodules: SubmodulesMode,
    uid: u32,
    gid: u32,
    mount_time: SystemTime,
    inodes: InodeTable<NodeKey, Node>,
    logged_backend_errors: Mutex<BTreeSet<String>>,
}

#[derive(Clone, Debug, Eq, Hash, PartialEq)]
enum NodeKey {
    Root,
    Path(Vec<Vec<u8>>),
}

#[derive(Clone, Debug)]
struct Node {
    kind: NodeKind,
}

#[derive(Clone, Debug)]
enum NodeKind {
    Root,
    AtResolver,
    AtPrefix,
    CommitsResolver,
    HistoryResolver,
    HistoryPrefix,
    HistoryDirectory {
        versions: Arc<[HistoryVersion]>,
    },
    RefsRoot,
    RefIndex {
        namespace: RefNamespace,
        components: Vec<Vec<u8>>,
    },
    Directory {
        source: Arc<GixRepository>,
        tree: TreeInfo,
        context: RevisionContext,
    },
    File {
        source: Arc<GixRepository>,
        oid: GitObjectId,
        mode: u32,
        context: RevisionContext,
    },
    Symlink {
        source: Arc<GixRepository>,
        oid: GitObjectId,
        context: RevisionContext,
    },
    GitlinkDirectory {
        oid: GitObjectId,
        context: RevisionContext,
    },
    GitlinkFile {
        oid: GitObjectId,
        context: RevisionContext,
    },
}

#[derive(Clone, Debug)]
struct RevisionContext {
    commit: CommitInfo,
    ttl: TtlClass,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum TtlClass {
    Long,
    Short,
}

enum RevisionLookup {
    Found(ResolvedRevision),
    Missing(String),
}

enum AtPathResolution {
    Revision {
        consumed: usize,
        resolved: ResolvedRevision,
    },
    PrefixDirectory,
}

struct ResolvedRevision {
    source: Arc<GixRepository>,
    tree: TreeInfo,
    context: RevisionContext,
}

struct RecursedSubmodule {
    source: Arc<GixRepository>,
    tree: TreeInfo,
    context: RevisionContext,
}

#[derive(Clone, Debug)]
struct HistoryVersion {
    name: Vec<u8>,
    entry: HistoryEntry,
}

struct DirEntrySpec {
    ino: u64,
    offset: i64,
    kind: FileType,
    name: OsString,
}

impl HeadFilesystem {
    pub(crate) fn new(store: GixRepository, config: &MountConfig) -> Result<Self> {
        let uid = config.uid.unwrap_or_else(current_uid);
        let gid = config.gid.unwrap_or_else(current_gid);
        let root = Node {
            kind: NodeKind::Root,
        };

        Ok(Self {
            store: Arc::new(store),
            submodules: config.submodules,
            uid,
            gid,
            mount_time: SystemTime::now(),
            inodes: InodeTable::new(NodeKey::Root, root),
            logged_backend_errors: Mutex::new(BTreeSet::new()),
        })
    }

    fn lookup_record(
        &mut self,
        parent: u64,
        name: &OsStr,
    ) -> Result<InodeRecord<NodeKey, Node>, i32> {
        let parent_record = self.inodes.get(parent).cloned().ok_or(ENOENT)?;
        let name_bytes = name.as_bytes();
        if name_bytes.len() > 255 {
            return Err(ENAMETOOLONG);
        }

        if name_bytes == b"." {
            return self
                .inodes
                .remember_lookup(parent_record.key.clone(), || Ok(parent_record.node.clone()));
        }

        if name_bytes == b".." {
            let parent_key = parent_key(&parent_record.key);
            let resolved = self.resolve_node(&parent_key);
            return self.inodes.remember_lookup(parent_key, || resolved);
        }

        if !is_directory_like(&parent_record.node.kind) {
            return Err(ENOTDIR);
        }

        let child_key = child_key(&parent_record.key, name_bytes);
        let resolved = self.resolve_node(&child_key);
        self.inodes.remember_lookup(child_key, || resolved)
    }

    fn resolve_node(&self, key: &NodeKey) -> Result<Node, i32> {
        match key {
            NodeKey::Root => Ok(Node {
                kind: NodeKind::Root,
            }),
            NodeKey::Path(components) => self.resolve_path(components),
        }
    }

    fn resolve_path(&self, components: &[Vec<u8>]) -> Result<Node, i32> {
        let Some(first) = components.first() else {
            return Err(ENOENT);
        };

        match first.as_slice() {
            b"now" => self.resolve_now_path(&components[1..]),
            b"at" => self.resolve_at_path(&components[1..]),
            b"commits" => self.resolve_commits_path(&components[1..]),
            b"refs" => self.resolve_refs_path(&components[1..]),
            b"history" => self.resolve_history_path(&components[1..]),
            _ => Err(ENOENT),
        }
    }

    fn resolve_now_path(&self, tail: &[Vec<u8>]) -> Result<Node, i32> {
        let resolved = match self.try_resolve_revision(b"HEAD", TtlClass::Short)? {
            RevisionLookup::Found(resolved) => resolved,
            RevisionLookup::Missing(reason) => {
                tracing::info!(revision = "HEAD", cause = %reason, "failed to resolve revision");
                return Err(ENOENT);
            }
        };

        self.resolve_revision_tree_path(resolved, tail)
    }

    fn resolve_at_path(&self, tail: &[Vec<u8>]) -> Result<Node, i32> {
        if tail.is_empty() {
            return Ok(Node {
                kind: NodeKind::AtResolver,
            });
        }

        match self.find_at_path_resolution(tail)? {
            AtPathResolution::Revision { consumed, resolved } => {
                self.resolve_revision_tree_path(resolved, &tail[consumed..])
            }
            AtPathResolution::PrefixDirectory => Ok(Node {
                kind: NodeKind::AtPrefix,
            }),
        }
    }

    fn find_at_path_resolution(&self, tail: &[Vec<u8>]) -> Result<AtPathResolution, i32> {
        let mut full_reason = None;

        for candidate in revision_candidates(tail) {
            match self
                .try_resolve_revision(&candidate.spec, ttl_for_revision_spec(&candidate.spec))?
            {
                RevisionLookup::Found(resolved) => {
                    return Ok(AtPathResolution::Revision {
                        consumed: candidate.consumed,
                        resolved,
                    });
                }
                RevisionLookup::Missing(reason) => {
                    if candidate.consumed == tail.len() {
                        full_reason = Some(reason);
                    }
                }
            }
        }

        if self.has_at_revision_prefix(tail)? {
            return Ok(AtPathResolution::PrefixDirectory);
        }

        let spec = join_path_components(tail);
        tracing::info!(
            revision = %display_bytes(&spec),
            cause = %full_reason.unwrap_or_else(|| String::from("revision not found")),
            "failed to resolve revision"
        );
        Err(ENOENT)
    }

    fn resolve_commits_path(&self, tail: &[Vec<u8>]) -> Result<Node, i32> {
        if tail.is_empty() {
            return Ok(Node {
                kind: NodeKind::CommitsResolver,
            });
        }

        let spec = tail[0].as_slice();
        let resolved = match self.try_resolve_commit_prefix(spec)? {
            RevisionLookup::Found(resolved) => resolved,
            RevisionLookup::Missing(reason) => {
                tracing::info!(
                    revision = %display_bytes(spec),
                    cause = %reason,
                    "failed to resolve revision"
                );
                return Err(ENOENT);
            }
        };

        self.resolve_revision_tree_path(resolved, &tail[1..])
    }

    fn resolve_refs_path(&self, tail: &[Vec<u8>]) -> Result<Node, i32> {
        if tail.is_empty() {
            return Ok(Node {
                kind: NodeKind::RefsRoot,
            });
        }

        let Some(namespace) = RefNamespace::from_component(tail[0].as_slice()) else {
            return Err(ENOENT);
        };

        self.resolve_ref_namespace_path(namespace, &tail[1..])
    }

    fn resolve_history_path(&self, tail: &[Vec<u8>]) -> Result<Node, i32> {
        if tail.is_empty() {
            return Ok(Node {
                kind: NodeKind::HistoryResolver,
            });
        }

        for candidate in revision_candidates(tail) {
            let entries = self
                .store
                .history_entries(&candidate.spec)
                .map_err(|error| self.store_error_errno(error))?;
            if entries.is_empty() {
                continue;
            }

            let versions = history_versions(&entries);
            if candidate.consumed == tail.len() {
                return Ok(Node {
                    kind: NodeKind::HistoryDirectory { versions },
                });
            }

            if candidate.consumed.saturating_add(1) != tail.len() {
                return Err(ENOTDIR);
            }

            let Some(name) = tail.get(candidate.consumed) else {
                return Err(ENOENT);
            };
            let Some(version) = versions.iter().find(|version| version.name == *name) else {
                return Err(ENOENT);
            };
            return self.resolve_history_version(version);
        }

        let prefix = join_path_components(tail);
        let has_prefix = self
            .store
            .history_prefix_exists(&prefix)
            .map_err(|error| self.store_error_errno(error))?;
        if has_prefix {
            return Ok(Node {
                kind: NodeKind::HistoryPrefix,
            });
        }

        Err(ENOENT)
    }

    fn resolve_ref_namespace_path(
        &self,
        namespace: RefNamespace,
        tail: &[Vec<u8>],
    ) -> Result<Node, i32> {
        if tail.is_empty() {
            return Ok(Node {
                kind: NodeKind::RefIndex {
                    namespace,
                    components: Vec::new(),
                },
            });
        }

        for candidate in reference_candidates(namespace, tail) {
            if !self.has_exact_reference(&candidate.spec)? {
                continue;
            }

            let resolved = self.resolve_existing_reference(&candidate.spec)?;
            return self.resolve_revision_tree_path(resolved, &tail[candidate.consumed..]);
        }

        if self.has_reference_prefix(namespace, tail)? {
            return Ok(Node {
                kind: NodeKind::RefIndex {
                    namespace,
                    components: tail.to_vec(),
                },
            });
        }

        Err(ENOENT)
    }

    fn resolve_history_version(&self, version: &HistoryVersion) -> Result<Node, i32> {
        let tree = self
            .store
            .root_tree(&version.entry.commit)
            .map_err(|error| self.store_error_errno(error))?;
        let context = RevisionContext {
            commit: version.entry.commit.clone(),
            ttl: TtlClass::Long,
        };
        let path = split_path_bytes(&version.entry.path);
        if path.iter().any(Vec::is_empty) {
            return Err(EIO);
        }
        self.resolve_tree_path(Arc::clone(&self.store), &context, &tree, &[], &path)
    }

    fn resolve_existing_reference(&self, full_name: &[u8]) -> Result<ResolvedRevision, i32> {
        match self.try_resolve_revision(full_name, TtlClass::Short)? {
            RevisionLookup::Found(resolved) => Ok(resolved),
            RevisionLookup::Missing(reason) => {
                tracing::warn!(
                    reference = %display_bytes(full_name),
                    cause = %reason,
                    "failed to resolve existing reference"
                );
                Err(EIO)
            }
        }
    }

    fn resolve_revision_tree_path(
        &self,
        resolved: ResolvedRevision,
        tail: &[Vec<u8>],
    ) -> Result<Node, i32> {
        if tail.is_empty() {
            return Ok(Node {
                kind: NodeKind::Directory {
                    source: Arc::clone(&resolved.source),
                    tree: resolved.tree,
                    context: resolved.context,
                },
            });
        }

        self.resolve_tree_path(
            Arc::clone(&resolved.source),
            &resolved.context,
            &resolved.tree,
            &[],
            tail,
        )
    }

    fn resolve_tree_path(
        &self,
        source: Arc<GixRepository>,
        context: &RevisionContext,
        root_tree: &TreeInfo,
        relative_path: &[Vec<u8>],
        components: &[Vec<u8>],
    ) -> Result<Node, i32> {
        let mut current_source = source;
        let mut current_tree = root_tree.clone();
        let mut current_context = context.clone();
        let mut current_relative = relative_path.to_vec();

        for (index, component) in components.iter().enumerate() {
            let last = index + 1 == components.len();
            let entry = self
                .find_entry(&current_source, &current_tree, component)?
                .ok_or(ENOENT)?;
            let mut entry_relative = current_relative.clone();
            entry_relative.push(entry.name.clone());

            match entry.kind {
                GitObjectKind::Tree => {
                    if last {
                        return Ok(node_from_entry(
                            Arc::clone(&current_source),
                            &entry,
                            current_context.clone(),
                        ));
                    }
                    current_tree = TreeInfo {
                        oid: entry.oid.clone(),
                    };
                    current_relative = entry_relative;
                }
                GitObjectKind::Blob => {
                    if last {
                        return Ok(node_from_entry(
                            Arc::clone(&current_source),
                            &entry,
                            current_context.clone(),
                        ));
                    }
                    return Err(ENOTDIR);
                }
                GitObjectKind::Commit => {
                    if last {
                        if let Some(recursed) = self.try_open_submodule(
                            &current_source,
                            &entry_relative,
                            &entry.oid,
                            &current_context,
                        )? {
                            return Ok(Node {
                                kind: NodeKind::Directory {
                                    source: recursed.source,
                                    tree: recursed.tree,
                                    context: recursed.context,
                                },
                            });
                        }

                        return Ok(Node {
                            kind: NodeKind::GitlinkDirectory {
                                oid: entry.oid.clone(),
                                context: current_context.clone(),
                            },
                        });
                    }

                    let Some(recursed) = self.try_open_submodule(
                        &current_source,
                        &entry_relative,
                        &entry.oid,
                        &current_context,
                    )?
                    else {
                        if components[index + 1].as_slice() == b".gitlink" {
                            if index + 2 != components.len() {
                                return Err(ENOTDIR);
                            }
                            return Ok(Node {
                                kind: NodeKind::GitlinkFile {
                                    oid: entry.oid.clone(),
                                    context: current_context,
                                },
                            });
                        }
                        return Err(ENOENT);
                    };

                    current_source = recursed.source;
                    current_tree = recursed.tree;
                    current_context = recursed.context;
                    current_relative.clear();
                }
            }
        }

        Err(ENOENT)
    }

    fn try_resolve_revision(&self, spec: &[u8], ttl: TtlClass) -> Result<RevisionLookup, i32> {
        let commit = match self.store.resolve_revision(spec) {
            Ok(commit) => commit,
            Err(error) => {
                return classify_revision_error(self, spec, error);
            }
        };
        let tree = self
            .store
            .root_tree(&commit)
            .map_err(|error| self.store_error_errno(error))?;

        Ok(RevisionLookup::Found(ResolvedRevision {
            source: Arc::clone(&self.store),
            tree,
            context: RevisionContext { commit, ttl },
        }))
    }

    fn try_resolve_commit_prefix(&self, spec: &[u8]) -> Result<RevisionLookup, i32> {
        let commit = match self.store.resolve_commit_prefix(spec) {
            Ok(commit) => commit,
            Err(error) => {
                return classify_revision_error(self, spec, error);
            }
        };
        let tree = self
            .store
            .root_tree(&commit)
            .map_err(|error| self.store_error_errno(error))?;

        Ok(RevisionLookup::Found(ResolvedRevision {
            source: Arc::clone(&self.store),
            tree,
            context: RevisionContext {
                commit,
                ttl: TtlClass::Long,
            },
        }))
    }

    fn find_entry(
        &self,
        source: &Arc<GixRepository>,
        tree: &TreeInfo,
        name: &[u8],
    ) -> Result<Option<TreeEntry>, i32> {
        let entries = self.tree_entries(source, tree)?;
        Ok(entries.into_iter().find(|entry| entry.name == name))
    }

    fn tree_entries(
        &self,
        source: &Arc<GixRepository>,
        tree: &TreeInfo,
    ) -> Result<Vec<TreeEntry>, i32> {
        let entries = source
            .list_tree(tree)
            .map_err(|error| self.store_error_errno(error))?;
        for entry in &entries {
            validate_tree_entry_name(&entry.name)?;
        }
        Ok(entries)
    }

    fn try_open_submodule(
        &self,
        source: &Arc<GixRepository>,
        relative_path: &[Vec<u8>],
        commit: &GitObjectId,
        parent_context: &RevisionContext,
    ) -> Result<Option<RecursedSubmodule>, i32> {
        if self.submodules != SubmodulesMode::Recurse {
            return Ok(None);
        }

        let path = join_path_components(relative_path);
        let opened = source
            .open_submodule(&path, commit)
            .map_err(|error| self.store_error_errno(error))?;
        Ok(opened.map(|opened| RecursedSubmodule {
            source: Arc::new(opened.store),
            tree: opened.tree,
            context: RevisionContext {
                commit: opened.commit,
                ttl: parent_context.ttl,
            },
        }))
    }

    fn listed_references(&self) -> Result<Vec<GitReference>, i32> {
        self.store
            .list_references()
            .map_err(|error| self.store_error_errno(error))
    }

    fn has_exact_reference(&self, full_name: &[u8]) -> Result<bool, i32> {
        Ok(self
            .listed_references()?
            .into_iter()
            .any(|reference| reference.full_name == full_name))
    }

    fn has_reference_prefix(
        &self,
        namespace: RefNamespace,
        components: &[Vec<u8>],
    ) -> Result<bool, i32> {
        let relative = join_path_components(components);
        let prefix = namespace.prefix_with_relative(&relative);
        Ok(self
            .listed_references()?
            .into_iter()
            .any(|reference| reference.full_name.starts_with(&prefix)))
    }

    fn next_reference_components(
        &self,
        namespace: RefNamespace,
        components: &[Vec<u8>],
    ) -> Result<Vec<Vec<u8>>, i32> {
        let relative = join_path_components(components);
        let full_prefix = namespace.full_name(&relative);
        let next_prefix = if relative.is_empty() {
            namespace.prefix().to_vec()
        } else {
            let mut prefix = full_prefix;
            prefix.push(b'/');
            prefix
        };

        let mut names = BTreeSet::new();
        for reference in self.listed_references()? {
            let Some(remainder) = reference.full_name.strip_prefix(next_prefix.as_slice()) else {
                continue;
            };
            if remainder.is_empty() {
                continue;
            }
            let name = remainder
                .split(|byte| *byte == b'/')
                .next()
                .map_or_else(Vec::new, |component| component.to_vec());
            if !name.is_empty() {
                names.insert(name);
            }
        }

        Ok(names.into_iter().collect())
    }

    fn has_at_revision_prefix(&self, components: &[Vec<u8>]) -> Result<bool, i32> {
        let prefix = join_path_components(components);
        let mut slash_prefix = prefix.clone();
        slash_prefix.push(b'/');

        Ok(self.listed_references()?.into_iter().any(|reference| {
            reference_revision_name(&reference).is_some_and(|name| name.starts_with(&slash_prefix))
        }))
    }

    fn file_attr(&self, record: &InodeRecord<NodeKey, Node>) -> Result<FileAttr, i32> {
        let size = self.node_size(&record.node)?;

        Ok(FileAttr {
            ino: record.ino,
            size,
            blocks: size_to_blocks(size),
            atime: self.node_timestamp(&record.node),
            mtime: self.node_timestamp(&record.node),
            ctime: self.node_timestamp(&record.node),
            crtime: self.node_timestamp(&record.node),
            kind: node_file_type(&record.node),
            perm: node_permissions(&record.node),
            nlink: 1,
            uid: self.uid,
            gid: self.gid,
            rdev: 0,
            blksize: BLOCK_SIZE,
            flags: 0,
        })
    }

    fn node_timestamp(&self, node: &Node) -> SystemTime {
        match &node.kind {
            NodeKind::Root
            | NodeKind::AtResolver
            | NodeKind::AtPrefix
            | NodeKind::CommitsResolver
            | NodeKind::HistoryResolver
            | NodeKind::HistoryPrefix
            | NodeKind::HistoryDirectory { .. }
            | NodeKind::RefsRoot
            | NodeKind::RefIndex { .. } => self.mount_time,
            NodeKind::Directory { context, .. }
            | NodeKind::File { context, .. }
            | NodeKind::Symlink { context, .. }
            | NodeKind::GitlinkDirectory { context, .. }
            | NodeKind::GitlinkFile { context, .. } => {
                commit_time_to_system_time(context.commit.committer_time_seconds)
            }
        }
    }

    fn node_ttl(&self, node: &Node) -> Duration {
        match &node.kind {
            NodeKind::Root
            | NodeKind::AtResolver
            | NodeKind::AtPrefix
            | NodeKind::CommitsResolver
            | NodeKind::HistoryResolver
            | NodeKind::HistoryPrefix
            | NodeKind::HistoryDirectory { .. }
            | NodeKind::RefsRoot
            | NodeKind::RefIndex { .. } => SHORT_TTL,
            NodeKind::Directory { context, .. }
            | NodeKind::File { context, .. }
            | NodeKind::Symlink { context, .. }
            | NodeKind::GitlinkDirectory { context, .. }
            | NodeKind::GitlinkFile { context, .. } => match context.ttl {
                TtlClass::Long => LONG_TTL,
                TtlClass::Short => SHORT_TTL,
            },
        }
    }

    fn node_size(&self, node: &Node) -> Result<u64, i32> {
        match &node.kind {
            NodeKind::Root
            | NodeKind::AtResolver
            | NodeKind::AtPrefix
            | NodeKind::CommitsResolver
            | NodeKind::HistoryResolver
            | NodeKind::HistoryPrefix
            | NodeKind::HistoryDirectory { .. }
            | NodeKind::RefsRoot
            | NodeKind::RefIndex { .. }
            | NodeKind::Directory { .. }
            | NodeKind::GitlinkDirectory { .. } => Ok(DIRECTORY_SIZE),
            NodeKind::File { source, oid, .. } => self.blob_size(source, oid),
            NodeKind::Symlink { source, oid, .. } => self
                .read_symlink_target(source, oid)
                .map(|target| u64::try_from(target.len()).unwrap_or(u64::MAX)),
            NodeKind::GitlinkFile { oid, .. } => Ok(u64::try_from(oid.as_hex().len())
                .unwrap_or(0)
                .saturating_add(1)),
        }
    }

    fn blob_size(&self, source: &Arc<GixRepository>, oid: &GitObjectId) -> Result<u64, i32> {
        source
            .blob_len(oid)
            .map_err(|error| self.store_error_errno(error))
    }

    fn read_bytes(&self, node: &Node, offset: i64, size: u32) -> Result<Vec<u8>, i32> {
        let start = u64::try_from(offset).map_err(|_| EINVAL)?;
        let limit = usize::try_from(size).map_err(|_| EIO)?;

        match &node.kind {
            NodeKind::Root
            | NodeKind::AtResolver
            | NodeKind::AtPrefix
            | NodeKind::CommitsResolver
            | NodeKind::HistoryResolver
            | NodeKind::HistoryPrefix
            | NodeKind::HistoryDirectory { .. }
            | NodeKind::RefsRoot
            | NodeKind::RefIndex { .. }
            | NodeKind::Directory { .. }
            | NodeKind::GitlinkDirectory { .. } => Err(EISDIR),
            NodeKind::Symlink { .. } => Err(EIO),
            NodeKind::File { source, oid, .. } => {
                let mut reader = source
                    .open_blob(oid)
                    .map_err(|error| self.store_error_errno(error))?;
                reader.seek(SeekFrom::Start(start)).map_err(|_| EIO)?;
                read_limited(&mut reader, limit)
            }
            NodeKind::GitlinkFile { oid, .. } => {
                let contents = gitlink_contents(oid);
                let start = usize::try_from(start).map_err(|_| EIO)?;
                let end = start.saturating_add(limit).min(contents.len());
                Ok(contents
                    .get(start..end)
                    .map_or_else(Vec::new, |slice| slice.to_vec()))
            }
        }
    }

    fn readlink_bytes(&self, node: &Node) -> Result<Vec<u8>, i32> {
        match &node.kind {
            NodeKind::Symlink { source, oid, .. } => self.read_symlink_target(source, oid),
            _ => Err(EINVAL),
        }
    }

    fn read_symlink_target(
        &self,
        source: &Arc<GixRepository>,
        oid: &GitObjectId,
    ) -> Result<Vec<u8>, i32> {
        source
            .read_symlink_target(oid)
            .map_err(|error| self.store_error_errno(error))
    }

    fn node_object_id<'a>(&self, node: &'a Node) -> Option<&'a GitObjectId> {
        match &node.kind {
            NodeKind::Root
            | NodeKind::AtResolver
            | NodeKind::AtPrefix
            | NodeKind::CommitsResolver
            | NodeKind::HistoryResolver
            | NodeKind::HistoryPrefix
            | NodeKind::HistoryDirectory { .. }
            | NodeKind::RefsRoot
            | NodeKind::RefIndex { .. } => None,
            NodeKind::Directory { tree, .. } => Some(&tree.oid),
            NodeKind::File { oid, .. }
            | NodeKind::Symlink { oid, .. }
            | NodeKind::GitlinkDirectory { oid, .. }
            | NodeKind::GitlinkFile { oid, .. } => Some(oid),
        }
    }

    fn node_revision_commit<'a>(&self, node: &'a Node) -> Option<&'a GitObjectId> {
        match &node.kind {
            NodeKind::Root
            | NodeKind::AtResolver
            | NodeKind::AtPrefix
            | NodeKind::CommitsResolver
            | NodeKind::HistoryResolver
            | NodeKind::HistoryPrefix
            | NodeKind::HistoryDirectory { .. }
            | NodeKind::RefsRoot
            | NodeKind::RefIndex { .. } => None,
            NodeKind::Directory { context, .. }
            | NodeKind::File { context, .. }
            | NodeKind::Symlink { context, .. }
            | NodeKind::GitlinkDirectory { context, .. }
            | NodeKind::GitlinkFile { context, .. } => Some(&context.commit.oid),
        }
    }

    fn node_git_mode(&self, node: &Node) -> Option<u32> {
        match &node.kind {
            NodeKind::File { mode, .. } => Some(*mode),
            NodeKind::Symlink { .. } => Some(0o120000),
            NodeKind::Root
            | NodeKind::AtResolver
            | NodeKind::AtPrefix
            | NodeKind::CommitsResolver
            | NodeKind::HistoryResolver
            | NodeKind::HistoryPrefix
            | NodeKind::HistoryDirectory { .. }
            | NodeKind::RefsRoot
            | NodeKind::RefIndex { .. }
            | NodeKind::Directory { .. }
            | NodeKind::GitlinkDirectory { .. }
            | NodeKind::GitlinkFile { .. } => None,
        }
    }

    fn node_git_type(&self, node: &Node) -> &'static str {
        match node.kind {
            NodeKind::File { .. } => "blob",
            NodeKind::Symlink { .. } => "symlink",
            NodeKind::GitlinkDirectory { .. } | NodeKind::GitlinkFile { .. } => "gitlink",
            NodeKind::Root
            | NodeKind::AtResolver
            | NodeKind::AtPrefix
            | NodeKind::CommitsResolver
            | NodeKind::HistoryResolver
            | NodeKind::HistoryPrefix
            | NodeKind::HistoryDirectory { .. }
            | NodeKind::RefsRoot
            | NodeKind::RefIndex { .. }
            | NodeKind::Directory { .. } => "tree",
        }
    }

    fn node_lfs_state(&self, node: &Node) -> Result<Option<LfsState>, i32> {
        match &node.kind {
            NodeKind::File { source, oid, .. } => source
                .lfs_state(oid)
                .map_err(|error| self.store_error_errno(error)),
            _ => Ok(None),
        }
    }

    fn xattr_value(&self, node: &Node, name: &[u8]) -> Result<Vec<u8>, i32> {
        match name {
            XATTR_OID => self
                .node_object_id(node)
                .map(|oid| oid.as_hex().as_bytes().to_vec())
                .ok_or(ENODATA),
            XATTR_MODE => self.node_git_mode(node).map(format_git_mode).ok_or(ENODATA),
            XATTR_TYPE => Ok(self.node_git_type(node).as_bytes().to_vec()),
            XATTR_COMMIT => self
                .node_revision_commit(node)
                .map(|oid| oid.as_hex().as_bytes().to_vec())
                .ok_or(ENODATA),
            XATTR_LFS => match self.node_lfs_state(node)? {
                Some(LfsState::Pointer) => Ok(b"pointer".to_vec()),
                Some(LfsState::Resolved) => Ok(b"resolved".to_vec()),
                None => Err(ENODATA),
            },
            _ => Err(ENODATA),
        }
    }

    fn list_xattr_names(&self, node: &Node) -> Result<Vec<u8>, i32> {
        let mut names = Vec::new();
        if self.node_object_id(node).is_some() {
            names.extend_from_slice(XATTR_OID);
            names.push(0);
        }
        if self.node_git_mode(node).is_some() {
            names.extend_from_slice(XATTR_MODE);
            names.push(0);
        }
        names.extend_from_slice(XATTR_TYPE);
        names.push(0);
        if self.node_revision_commit(node).is_some() {
            names.extend_from_slice(XATTR_COMMIT);
            names.push(0);
        }
        if self.node_lfs_state(node)?.is_some() {
            names.extend_from_slice(XATTR_LFS);
            names.push(0);
        }
        Ok(names)
    }

    fn reply_xattr_bytes(&self, reply: ReplyXattr, data: &[u8], size: u32) {
        let requested = match usize::try_from(size) {
            Ok(requested) => requested,
            Err(_) => {
                reply.error(ERANGE);
                return;
            }
        };

        if size == 0 {
            match u32::try_from(data.len()) {
                Ok(length) => reply.size(length),
                Err(_) => reply.error(ERANGE),
            }
            return;
        }

        if requested < data.len() {
            reply.error(ERANGE);
            return;
        }

        reply.data(data);
    }

    fn store_error_errno(&self, error: StoreError) -> i32 {
        let message = error.to_string();
        let should_log = match self.logged_backend_errors.lock() {
            Ok(mut logged) => logged.insert(message.clone()),
            Err(poisoned) => poisoned.into_inner().insert(message.clone()),
        };
        if should_log {
            tracing::warn!(cause = %message, "filesystem request failed against the Git backend");
        }
        EIO
    }

    fn readdir_entries(&mut self, ino: u64) -> Result<Vec<DirEntrySpec>, i32> {
        let record = self.inodes.get(ino).cloned().ok_or(ENOENT)?;
        let parent_key = parent_key(&record.key);
        let mut entries = Vec::new();

        entries.push(DirEntrySpec {
            ino: record.ino,
            offset: 1,
            kind: FileType::Directory,
            name: OsString::from("."),
        });

        let resolved_parent = self.resolve_node(&parent_key);
        let parent = self.inodes.remember_seen(parent_key, || resolved_parent)?;
        entries.push(DirEntrySpec {
            ino: parent.ino,
            offset: 2,
            kind: FileType::Directory,
            name: OsString::from(".."),
        });

        match &record.node.kind {
            NodeKind::Root => {
                self.push_named_children(&record.key, &ROOT_ENTRY_NAMES, 3, &mut entries)?;
            }
            NodeKind::AtResolver
            | NodeKind::AtPrefix
            | NodeKind::CommitsResolver
            | NodeKind::HistoryResolver
            | NodeKind::HistoryPrefix => {}
            NodeKind::HistoryDirectory { versions } => {
                let mut offset = 3_i64;
                for version in versions.iter() {
                    let key = child_key(&record.key, &version.name);
                    let resolved_child = self.resolve_node(&key);
                    let child = self.inodes.remember_seen(key, || resolved_child)?;
                    entries.push(DirEntrySpec {
                        ino: child.ino,
                        offset,
                        kind: node_file_type(&child.node),
                        name: OsString::from_vec(version.name.clone()),
                    });
                    offset = offset.saturating_add(1);
                }
            }
            NodeKind::RefsRoot => {
                self.push_named_children(&record.key, &REFS_ENTRY_NAMES, 3, &mut entries)?;
            }
            NodeKind::RefIndex {
                namespace,
                components,
            } => {
                let names = self.next_reference_components(*namespace, components)?;
                let mut offset = 3_i64;
                for name in names {
                    let key = child_key(&record.key, &name);
                    let resolved_child = self.resolve_node(&key);
                    let child = self.inodes.remember_seen(key, || resolved_child)?;
                    entries.push(DirEntrySpec {
                        ino: child.ino,
                        offset,
                        kind: node_file_type(&child.node),
                        name: OsString::from_vec(name),
                    });
                    offset = offset.saturating_add(1);
                }
            }
            NodeKind::Directory { source, tree, .. } => {
                let mut offset = 3_i64;
                for tree_entry in self.tree_entries(source, tree)? {
                    let key = child_key(&record.key, &tree_entry.name);
                    let resolved_child = self.resolve_node(&key);
                    let child = self.inodes.remember_seen(key, || resolved_child)?;
                    entries.push(DirEntrySpec {
                        ino: child.ino,
                        offset,
                        kind: node_file_type(&child.node),
                        name: OsString::from_vec(tree_entry.name),
                    });
                    offset = offset.saturating_add(1);
                }
            }
            NodeKind::GitlinkDirectory { .. } => {
                let key = child_key(&record.key, b".gitlink");
                let resolved_child = self.resolve_node(&key);
                let child = self.inodes.remember_seen(key, || resolved_child)?;
                entries.push(DirEntrySpec {
                    ino: child.ino,
                    offset: 3,
                    kind: node_file_type(&child.node),
                    name: OsString::from(".gitlink"),
                });
            }
            NodeKind::File { .. } | NodeKind::Symlink { .. } | NodeKind::GitlinkFile { .. } => {
                return Err(ENOTDIR);
            }
        }

        self.inodes.evict_unreferenced();
        Ok(entries)
    }

    fn push_named_children(
        &mut self,
        parent_key: &NodeKey,
        names: &[&[u8]],
        start_offset: i64,
        entries: &mut Vec<DirEntrySpec>,
    ) -> Result<(), i32> {
        let mut offset = start_offset;
        for name in names {
            let key = child_key(parent_key, name);
            let resolved_child = self.resolve_node(&key);
            let child = self.inodes.remember_seen(key, || resolved_child)?;
            entries.push(DirEntrySpec {
                ino: child.ino,
                offset,
                kind: node_file_type(&child.node),
                name: OsString::from_vec(name.to_vec()),
            });
            offset = offset.saturating_add(1);
        }
        Ok(())
    }
}

impl Filesystem for HeadFilesystem {
    fn init(&mut self, _req: &Request<'_>, _config: &mut KernelConfig) -> Result<(), i32> {
        Ok(())
    }

    fn lookup(&mut self, _req: &Request<'_>, parent: u64, name: &OsStr, reply: ReplyEntry) {
        match self.lookup_record(parent, name).and_then(|record| {
            let attr = self.file_attr(&record)?;
            let ttl = self.node_ttl(&record.node);
            Ok((record.generation, ttl, attr))
        }) {
            Ok((generation, ttl, attr)) => reply.entry(&ttl, &attr, generation),
            Err(errno) => reply.error(errno),
        }
    }

    fn forget(&mut self, _req: &Request<'_>, ino: u64, nlookup: u64) {
        self.inodes.forget(ino, nlookup);
    }

    fn batch_forget(&mut self, _req: &Request<'_>, nodes: &[fuse_forget_one]) {
        for node in nodes {
            self.inodes.forget(node.nodeid, node.nlookup);
        }
    }

    fn getattr(&mut self, _req: &Request<'_>, ino: u64, _fh: Option<u64>, reply: ReplyAttr) {
        match self.inodes.get(ino).ok_or(ENOENT).and_then(|record| {
            let attr = self.file_attr(record)?;
            let ttl = self.node_ttl(&record.node);
            Ok((ttl, attr))
        }) {
            Ok((ttl, attr)) => reply.attr(&ttl, &attr),
            Err(errno) => reply.error(errno),
        }
    }

    fn setattr(
        &mut self,
        _req: &Request<'_>,
        _ino: u64,
        _mode: Option<u32>,
        _uid: Option<u32>,
        _gid: Option<u32>,
        _size: Option<u64>,
        _atime: Option<fuser::TimeOrNow>,
        _mtime: Option<fuser::TimeOrNow>,
        _ctime: Option<SystemTime>,
        _fh: Option<u64>,
        _crtime: Option<SystemTime>,
        _chgtime: Option<SystemTime>,
        _bkuptime: Option<SystemTime>,
        _flags: Option<u32>,
        reply: ReplyAttr,
    ) {
        reply.error(EROFS);
    }

    fn readlink(&mut self, _req: &Request<'_>, ino: u64, reply: ReplyData) {
        match self
            .inodes
            .get(ino)
            .ok_or(ENOENT)
            .and_then(|record| self.readlink_bytes(&record.node))
        {
            Ok(data) => reply.data(&data),
            Err(errno) => reply.error(errno),
        }
    }

    fn mknod(
        &mut self,
        _req: &Request<'_>,
        _parent: u64,
        _name: &OsStr,
        _mode: u32,
        _umask: u32,
        _rdev: u32,
        reply: ReplyEntry,
    ) {
        reply.error(EROFS);
    }

    fn mkdir(
        &mut self,
        _req: &Request<'_>,
        _parent: u64,
        _name: &OsStr,
        _mode: u32,
        _umask: u32,
        reply: ReplyEntry,
    ) {
        reply.error(EROFS);
    }

    fn unlink(&mut self, _req: &Request<'_>, _parent: u64, _name: &OsStr, reply: ReplyEmpty) {
        reply.error(EROFS);
    }

    fn rmdir(&mut self, _req: &Request<'_>, _parent: u64, _name: &OsStr, reply: ReplyEmpty) {
        reply.error(EROFS);
    }

    fn symlink(
        &mut self,
        _req: &Request<'_>,
        _parent: u64,
        _link_name: &OsStr,
        _target: &Path,
        reply: ReplyEntry,
    ) {
        reply.error(EROFS);
    }

    fn rename(
        &mut self,
        _req: &Request<'_>,
        _parent: u64,
        _name: &OsStr,
        _newparent: u64,
        _newname: &OsStr,
        _flags: u32,
        reply: ReplyEmpty,
    ) {
        reply.error(EROFS);
    }

    fn link(
        &mut self,
        _req: &Request<'_>,
        _ino: u64,
        _newparent: u64,
        _newname: &OsStr,
        reply: ReplyEntry,
    ) {
        reply.error(EROFS);
    }

    fn open(&mut self, _req: &Request<'_>, ino: u64, flags: i32, reply: ReplyOpen) {
        let access_mode = flags & O_ACCMODE;
        if access_mode == O_WRONLY || access_mode == O_RDWR || flags & (O_APPEND | O_TRUNC) != 0 {
            reply.error(EACCES);
            return;
        }

        match self.inodes.get(ino).map(|record| &record.node.kind) {
            Some(NodeKind::Root)
            | Some(NodeKind::AtResolver)
            | Some(NodeKind::AtPrefix)
            | Some(NodeKind::CommitsResolver)
            | Some(NodeKind::HistoryResolver)
            | Some(NodeKind::HistoryPrefix)
            | Some(NodeKind::HistoryDirectory { .. })
            | Some(NodeKind::RefsRoot)
            | Some(NodeKind::RefIndex { .. })
            | Some(NodeKind::Directory { .. })
            | Some(NodeKind::GitlinkDirectory { .. }) => reply.error(EISDIR),
            Some(NodeKind::File { .. }) | Some(NodeKind::GitlinkFile { .. }) => reply.opened(0, 0),
            Some(NodeKind::Symlink { .. }) => reply.error(EACCES),
            None => reply.error(ENOENT),
        }
    }

    fn read(
        &mut self,
        _req: &Request<'_>,
        ino: u64,
        _fh: u64,
        offset: i64,
        size: u32,
        _flags: i32,
        _lock_owner: Option<u64>,
        reply: ReplyData,
    ) {
        match self
            .inodes
            .get(ino)
            .ok_or(ENOENT)
            .and_then(|record| self.read_bytes(&record.node, offset, size))
        {
            Ok(data) => reply.data(&data),
            Err(errno) => reply.error(errno),
        }
    }

    fn write(
        &mut self,
        _req: &Request<'_>,
        _ino: u64,
        _fh: u64,
        _offset: i64,
        _data: &[u8],
        _write_flags: u32,
        _flags: i32,
        _lock_owner: Option<u64>,
        reply: ReplyWrite,
    ) {
        reply.error(EROFS);
    }

    fn flush(
        &mut self,
        _req: &Request<'_>,
        _ino: u64,
        _fh: u64,
        _lock_owner: u64,
        reply: ReplyEmpty,
    ) {
        reply.ok();
    }

    fn release(
        &mut self,
        _req: &Request<'_>,
        _ino: u64,
        _fh: u64,
        _flags: i32,
        _lock_owner: Option<u64>,
        _flush: bool,
        reply: ReplyEmpty,
    ) {
        reply.ok();
    }

    fn fsync(
        &mut self,
        _req: &Request<'_>,
        _ino: u64,
        _fh: u64,
        _datasync: bool,
        reply: ReplyEmpty,
    ) {
        reply.ok();
    }

    fn opendir(&mut self, _req: &Request<'_>, ino: u64, _flags: i32, reply: ReplyOpen) {
        match self.inodes.get(ino).map(|record| &record.node.kind) {
            Some(kind) if is_directory_like(kind) => reply.opened(0, 0),
            Some(_) => reply.error(ENOTDIR),
            None => reply.error(ENOENT),
        }
    }

    fn readdir(
        &mut self,
        _req: &Request<'_>,
        ino: u64,
        _fh: u64,
        offset: i64,
        mut reply: ReplyDirectory,
    ) {
        let start = match usize::try_from(offset) {
            Ok(start) => start,
            Err(_) => {
                reply.error(EINVAL);
                return;
            }
        };

        match self.readdir_entries(ino) {
            Ok(entries) => {
                for entry in entries.into_iter().skip(start) {
                    if reply.add(entry.ino, entry.offset, entry.kind, entry.name) {
                        break;
                    }
                }
                reply.ok();
            }
            Err(errno) => reply.error(errno),
        }
    }

    fn releasedir(
        &mut self,
        _req: &Request<'_>,
        _ino: u64,
        _fh: u64,
        _flags: i32,
        reply: ReplyEmpty,
    ) {
        reply.ok();
    }

    fn fsyncdir(
        &mut self,
        _req: &Request<'_>,
        _ino: u64,
        _fh: u64,
        _datasync: bool,
        reply: ReplyEmpty,
    ) {
        reply.ok();
    }

    fn statfs(&mut self, _req: &Request<'_>, _ino: u64, reply: ReplyStatfs) {
        let files = u64::try_from(self.inodes.len()).unwrap_or(u64::MAX).max(1);
        reply.statfs(1, 0, 0, files, 0, BLOCK_SIZE, 255, BLOCK_SIZE);
    }

    fn setxattr(
        &mut self,
        _req: &Request<'_>,
        _ino: u64,
        _name: &OsStr,
        _value: &[u8],
        _flags: i32,
        _position: u32,
        reply: ReplyEmpty,
    ) {
        reply.error(EROFS);
    }

    fn getxattr(
        &mut self,
        _req: &Request<'_>,
        ino: u64,
        name: &OsStr,
        size: u32,
        reply: ReplyXattr,
    ) {
        match self
            .inodes
            .get(ino)
            .ok_or(ENOENT)
            .and_then(|record| self.xattr_value(&record.node, name.as_bytes()))
        {
            Ok(value) => self.reply_xattr_bytes(reply, &value, size),
            Err(errno) => reply.error(errno),
        }
    }

    fn listxattr(&mut self, _req: &Request<'_>, ino: u64, size: u32, reply: ReplyXattr) {
        match self.inodes.get(ino).ok_or(ENOENT) {
            Ok(record) => match self.list_xattr_names(&record.node) {
                Ok(names) => self.reply_xattr_bytes(reply, &names, size),
                Err(errno) => reply.error(errno),
            },
            Err(errno) => reply.error(errno),
        }
    }

    fn removexattr(&mut self, _req: &Request<'_>, _ino: u64, _name: &OsStr, reply: ReplyEmpty) {
        reply.error(EROFS);
    }

    fn access(&mut self, _req: &Request<'_>, ino: u64, mask: i32, reply: ReplyEmpty) {
        if self.inodes.get(ino).is_none() {
            reply.error(ENOENT);
            return;
        }

        if mask & W_OK != 0 {
            reply.error(EACCES);
            return;
        }

        reply.ok();
    }

    fn create(
        &mut self,
        _req: &Request<'_>,
        _parent: u64,
        _name: &OsStr,
        _mode: u32,
        _umask: u32,
        _flags: i32,
        reply: ReplyCreate,
    ) {
        reply.error(EROFS);
    }

    fn getlk(
        &mut self,
        _req: &Request<'_>,
        _ino: u64,
        _fh: u64,
        _lock_owner: u64,
        _start: u64,
        _end: u64,
        _typ: i32,
        _pid: u32,
        reply: ReplyLock,
    ) {
        reply.error(ENOSYS);
    }

    fn setlk(
        &mut self,
        _req: &Request<'_>,
        _ino: u64,
        _fh: u64,
        _lock_owner: u64,
        _start: u64,
        _end: u64,
        _typ: i32,
        _pid: u32,
        _sleep: bool,
        reply: ReplyEmpty,
    ) {
        reply.error(ENOSYS);
    }

    fn bmap(
        &mut self,
        _req: &Request<'_>,
        _ino: u64,
        _blocksize: u32,
        _idx: u64,
        reply: ReplyBmap,
    ) {
        reply.error(ENOSYS);
    }

    fn ioctl(
        &mut self,
        _req: &Request<'_>,
        _ino: u64,
        _fh: u64,
        _flags: u32,
        _cmd: u32,
        _in_data: &[u8],
        _out_size: u32,
        reply: ReplyIoctl,
    ) {
        reply.error(ENOSYS);
    }

    fn fallocate(
        &mut self,
        _req: &Request<'_>,
        _ino: u64,
        _fh: u64,
        _offset: i64,
        _length: i64,
        _mode: i32,
        reply: ReplyEmpty,
    ) {
        reply.error(EROFS);
    }

    fn lseek(
        &mut self,
        _req: &Request<'_>,
        _ino: u64,
        _fh: u64,
        _offset: i64,
        _whence: i32,
        reply: ReplyLseek,
    ) {
        reply.error(ENOSYS);
    }

    fn copy_file_range(
        &mut self,
        _req: &Request<'_>,
        _ino_in: u64,
        _fh_in: u64,
        _offset_in: i64,
        _ino_out: u64,
        _fh_out: u64,
        _offset_out: i64,
        _len: u64,
        _flags: u32,
        reply: ReplyWrite,
    ) {
        reply.error(EROFS);
    }
}

fn is_directory_like(kind: &NodeKind) -> bool {
    matches!(
        kind,
        NodeKind::Root
            | NodeKind::AtResolver
            | NodeKind::AtPrefix
            | NodeKind::CommitsResolver
            | NodeKind::HistoryResolver
            | NodeKind::HistoryPrefix
            | NodeKind::HistoryDirectory { .. }
            | NodeKind::RefsRoot
            | NodeKind::RefIndex { .. }
            | NodeKind::Directory { .. }
            | NodeKind::GitlinkDirectory { .. }
    )
}

fn classify_revision_error(
    filesystem: &HeadFilesystem,
    _spec: &[u8],
    error: StoreError,
) -> Result<RevisionLookup, i32> {
    match error {
        StoreError::RevisionNotFound { source, .. }
        | StoreError::AmbiguousRevision { source, .. }
        | StoreError::ResolveRevision { source, .. } => {
            Ok(RevisionLookup::Missing(source.to_string()))
        }
        other => Err(filesystem.store_error_errno(other)),
    }
}

fn node_from_entry(
    source: Arc<GixRepository>,
    entry: &TreeEntry,
    context: RevisionContext,
) -> Node {
    match entry.kind {
        GitObjectKind::Tree => Node {
            kind: NodeKind::Directory {
                source,
                tree: TreeInfo {
                    oid: entry.oid.clone(),
                },
                context,
            },
        },
        GitObjectKind::Blob if entry.mode == 0o120000 => Node {
            kind: NodeKind::Symlink {
                source,
                oid: entry.oid.clone(),
                context,
            },
        },
        GitObjectKind::Blob => Node {
            kind: NodeKind::File {
                source,
                oid: entry.oid.clone(),
                mode: entry.mode,
                context,
            },
        },
        GitObjectKind::Commit => Node {
            kind: NodeKind::GitlinkDirectory {
                oid: entry.oid.clone(),
                context,
            },
        },
    }
}

fn node_file_type(node: &Node) -> FileType {
    match node.kind {
        NodeKind::Root
        | NodeKind::AtResolver
        | NodeKind::AtPrefix
        | NodeKind::CommitsResolver
        | NodeKind::HistoryResolver
        | NodeKind::HistoryPrefix
        | NodeKind::HistoryDirectory { .. }
        | NodeKind::RefsRoot
        | NodeKind::RefIndex { .. }
        | NodeKind::Directory { .. }
        | NodeKind::GitlinkDirectory { .. } => FileType::Directory,
        NodeKind::File { .. } | NodeKind::GitlinkFile { .. } => FileType::RegularFile,
        NodeKind::Symlink { .. } => FileType::Symlink,
    }
}

fn node_permissions(node: &Node) -> u16 {
    match &node.kind {
        NodeKind::Root
        | NodeKind::AtResolver
        | NodeKind::AtPrefix
        | NodeKind::CommitsResolver
        | NodeKind::HistoryResolver
        | NodeKind::HistoryPrefix
        | NodeKind::HistoryDirectory { .. }
        | NodeKind::RefsRoot
        | NodeKind::RefIndex { .. }
        | NodeKind::Directory { .. }
        | NodeKind::GitlinkDirectory { .. } => 0o555,
        NodeKind::File { mode, .. } => {
            if *mode == 0o100755 {
                0o555
            } else {
                0o444
            }
        }
        NodeKind::Symlink { .. } => 0o777,
        NodeKind::GitlinkFile { .. } => 0o444,
    }
}

fn format_git_mode(mode: u32) -> Vec<u8> {
    format!("{mode:06o}").into_bytes()
}

fn history_versions(entries: &[HistoryEntry]) -> Arc<[HistoryVersion]> {
    let width = entries.len().to_string().len().max(4);
    let versions = entries
        .iter()
        .enumerate()
        .map(|(index, entry)| HistoryVersion {
            name: history_version_name(
                index.saturating_add(1),
                width,
                &entry.commit.oid,
                &entry.path,
            ),
            entry: entry.clone(),
        })
        .collect::<Vec<_>>();
    Arc::from(versions)
}

fn history_version_name(
    ordinal: usize,
    width: usize,
    commit: &GitObjectId,
    path: &[u8],
) -> Vec<u8> {
    let mut name = format!("{ordinal:0width$}-").into_bytes();
    let short_len = commit.as_hex().len().min(7);
    name.extend_from_slice(&commit.as_hex().as_bytes()[..short_len]);
    name.extend_from_slice(history_version_suffix(path));
    name
}

fn history_version_suffix(path: &[u8]) -> &[u8] {
    let basename = path.rsplit(|byte| *byte == b'/').next().unwrap_or(path);
    if basename.first().is_some_and(|byte| *byte == b'.') {
        return basename;
    }
    match basename.iter().position(|byte| *byte == b'.') {
        Some(index) => basename.get(index..).unwrap_or_default(),
        None => &[],
    }
}

fn validate_tree_entry_name(name: &[u8]) -> Result<(), i32> {
    if !is_valid_tree_entry_name(name) {
        return Err(EIO);
    }
    Ok(())
}

fn child_key(parent: &NodeKey, name: &[u8]) -> NodeKey {
    let mut components = match parent {
        NodeKey::Root => Vec::new(),
        NodeKey::Path(components) => components.clone(),
    };
    components.push(name.to_vec());
    NodeKey::Path(components)
}

fn parent_key(key: &NodeKey) -> NodeKey {
    match key {
        NodeKey::Root => NodeKey::Root,
        NodeKey::Path(components) => {
            let mut parent = components.clone();
            parent.pop();
            if parent.is_empty() {
                NodeKey::Root
            } else {
                NodeKey::Path(parent)
            }
        }
    }
}

fn ttl_for_revision_spec(spec: &[u8]) -> TtlClass {
    if is_hex_revision_spec(spec) {
        TtlClass::Long
    } else {
        TtlClass::Short
    }
}

fn gitlink_contents(oid: &GitObjectId) -> Vec<u8> {
    let mut contents = oid.as_hex().as_bytes().to_vec();
    contents.push(b'\n');
    contents
}

fn read_limited(reader: &mut dyn Read, limit: usize) -> Result<Vec<u8>, i32> {
    let mut limited = reader.take(u64::try_from(limit).map_err(|_| EIO)?);
    let mut data = Vec::new();
    limited.read_to_end(&mut data).map_err(|_| EIO)?;
    Ok(data)
}

fn display_bytes(bytes: &[u8]) -> String {
    String::from_utf8_lossy(bytes).into_owned()
}

fn commit_time_to_system_time(seconds: i64) -> SystemTime {
    if seconds >= 0 {
        match u64::try_from(seconds) {
            Ok(seconds) => UNIX_EPOCH + Duration::from_secs(seconds),
            Err(_) => UNIX_EPOCH,
        }
    } else {
        UNIX_EPOCH - Duration::from_secs(seconds.unsigned_abs())
    }
}

fn current_uid() -> u32 {
    // Safety: `geteuid()` is thread-safe, side-effect free, and requires no preconditions.
    unsafe { libc::geteuid() }
}

fn current_gid() -> u32 {
    // Safety: `getegid()` is thread-safe, side-effect free, and requires no preconditions.
    unsafe { libc::getegid() }
}

fn size_to_blocks(size: u64) -> u64 {
    if size == 0 {
        0
    } else {
        1 + (size - 1) / 512
    }
}
