/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::collections::BTreeSet;
use std::collections::HashMap;
use std::collections::HashSet;
use std::fmt;
use std::future::Future;

use anyhow::anyhow;
use basename_suffix_skeleton_manifest::RootBasenameSuffixSkeletonManifest;
use blobrepo_hg::BlobRepoHg;
use blobstore::Loadable;
use bonsai_git_mapping::BonsaiGitMappingRef;
use bonsai_globalrev_mapping::BonsaiGlobalrevMappingRef;
use bonsai_svnrev_mapping::BonsaiSvnrevMappingRef;
use bookmarks::BookmarkKey;
use bytes::Bytes;
use changeset_info::ChangesetInfo;
use changesets::ChangesetsRef;
use chrono::DateTime;
use chrono::FixedOffset;
use cloned::cloned;
use commit_graph::AncestorsStreamBuilder;
use commit_graph::CommitGraphArc;
use commit_graph::CommitGraphRef;
use context::CoreContext;
use deleted_manifest::DeletedManifestOps;
use deleted_manifest::RootDeletedManifestIdCommon;
use deleted_manifest::RootDeletedManifestV2Id;
use derived_data::BonsaiDerived;
use derived_data_manager::BonsaiDerivable;
use fsnodes::RootFsnodeId;
use futures::future;
use futures::future::try_join;
use futures::stream;
use futures::stream::BoxStream;
use futures::stream::Stream;
use futures::stream::StreamExt;
use futures::stream::TryStreamExt;
use futures_lazy_shared::LazyShared;
use hooks::CrossRepoPushSource;
use hooks::HookOutcome;
use hooks::PushAuthoredBy;
use itertools::EitherOrBoth;
use manifest::Diff as ManifestDiff;
use manifest::Entry as ManifestEntry;
use manifest::ManifestOps;
use manifest::ManifestOrderedOps;
use manifest::PathOrPrefix;
use mercurial_types::Globalrev;
use mononoke_types::BonsaiChangeset;
use mononoke_types::FileChange;
pub use mononoke_types::Generation;
use mononoke_types::MPath;
use mononoke_types::MPathElement;
use mononoke_types::SkeletonManifestId;
use mononoke_types::Svnrev;
use repo_blobstore::RepoBlobstoreArc;
use repo_blobstore::RepoBlobstoreRef;
use repo_derived_data::RepoDerivedDataArc;
use skeleton_manifest::RootSkeletonManifestId;
use smallvec::SmallVec;
use sorted_vector_map::SortedVectorMap;
use tunables::tunables;
use unodes::RootUnodeManifestId;
use vec1::Vec1;

use crate::changeset_path::ChangesetPathContentContext;
use crate::changeset_path::ChangesetPathContext;
use crate::changeset_path::ChangesetPathHistoryContext;
use crate::changeset_path_diff::ChangesetPathDiffContext;
use crate::errors::MononokeError;
use crate::path::is_related_to;
use crate::path::MononokePath;
use crate::repo::RepoContext;
use crate::specifiers::ChangesetId;
use crate::specifiers::GitSha1;
use crate::specifiers::HgChangesetId;

#[derive(Clone, Debug)]
enum PathMutableHistory {
    /// Checking the mutable history datastore shows no changes
    NoChange,
    /// Change of parent and path
    PathAndChangeset(ChangesetId, MononokePath),
}

impl PathMutableHistory {
    /// Get the mutable parents (if any) of this path
    pub fn get_parent_cs_id(&self) -> Option<ChangesetId> {
        match self {
            Self::NoChange => None,
            Self::PathAndChangeset(cs_id, _) => Some(*cs_id),
        }
    }

    /// Is this path overridden by mutable history?
    fn is_override(&self) -> bool {
        match self {
            Self::NoChange => false,
            Self::PathAndChangeset(_, _) => true,
        }
    }

    /// Extract the copy_from information relating to this entry, if any
    fn get_copy_from(&self) -> Option<(ChangesetId, &MononokePath)> {
        match self {
            Self::NoChange => None,
            Self::PathAndChangeset(cs_id, path) => Some((*cs_id, path)),
        }
    }
}

#[derive(Clone)]
pub struct ChangesetContext {
    repo: RepoContext,
    id: ChangesetId,
    bonsai_changeset: LazyShared<Result<BonsaiChangeset, MononokeError>>,
    changeset_info: LazyShared<Result<ChangesetInfo, MononokeError>>,
    root_unode_manifest_id: LazyShared<Result<RootUnodeManifestId, MononokeError>>,
    root_fsnode_id: LazyShared<Result<RootFsnodeId, MononokeError>>,
    root_skeleton_manifest_id: LazyShared<Result<RootSkeletonManifestId, MononokeError>>,
    root_deleted_manifest_v2_id: LazyShared<Result<RootDeletedManifestV2Id, MononokeError>>,
    root_basename_suffix_skeleton_manifest:
        LazyShared<Result<RootBasenameSuffixSkeletonManifest, MononokeError>>,
    /// None if no mutable history, else map from supplied paths to data fetched
    mutable_history: Option<HashMap<MononokePath, PathMutableHistory>>,
}

#[derive(Default)]
pub struct ChangesetHistoryOptions {
    pub until_timestamp: Option<i64>,
    pub descendants_of: Option<ChangesetId>,
    pub exclude_changeset_and_ancestors: Option<ChangesetId>,
}

#[derive(Clone)]
pub enum ChangesetFileOrdering {
    Unordered,
    Ordered { after: Option<MononokePath> },
}

#[derive(PartialEq, Eq, PartialOrd, Ord)]
pub enum ChangesetDiffItem {
    TREES,
    FILES,
}

impl fmt::Debug for ChangesetContext {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(
            f,
            "ChangesetContext(repo={:?} id={:?})",
            self.repo().name(),
            self.id()
        )
    }
}

fn to_vec1<X>(maybe_vec: Option<Vec<X>>) -> Option<Vec1<X>> {
    maybe_vec.and_then(|v| Vec1::try_from_vec(v).ok())
}

/// A context object representing a query to a particular commit in a repo.
impl ChangesetContext {
    /// Construct a new `MononokeChangeset`.  The changeset must exist
    /// in the repo.
    pub(crate) fn new(repo: RepoContext, id: ChangesetId) -> Self {
        let bonsai_changeset = LazyShared::new_empty();
        let changeset_info = LazyShared::new_empty();
        let root_unode_manifest_id = LazyShared::new_empty();
        let root_fsnode_id = LazyShared::new_empty();
        let root_skeleton_manifest_id = LazyShared::new_empty();
        let root_deleted_manifest_v2_id = LazyShared::new_empty();
        let root_basename_suffix_skeleton_manifest = LazyShared::new_empty();
        Self {
            repo,
            id,
            changeset_info,
            bonsai_changeset,
            root_unode_manifest_id,
            root_fsnode_id,
            root_skeleton_manifest_id,
            root_deleted_manifest_v2_id,
            root_basename_suffix_skeleton_manifest,
            mutable_history: None,
        }
    }

    /// The context for this query.
    pub(crate) fn ctx(&self) -> &CoreContext {
        self.repo.ctx()
    }

    /// Adds copy information from mutable renames as an override to replace
    /// the Bonsai copy information
    pub async fn add_mutable_renames(
        &mut self,
        paths: impl Iterator<Item = MononokePath>,
    ) -> Result<(), MononokeError> {
        let mutable_renames = &self.repo.mutable_renames();
        let ctx = self.repo.ctx();
        let cs_id = self.id;

        let copy_info = stream::iter(paths.map(move |path| async move {
            let maybe_rename_entry = mutable_renames
                .get_rename(ctx, cs_id, path.as_mpath().cloned())
                .await?;
            let rename = match maybe_rename_entry {
                Some(entry) => {
                    let cs_id = entry.src_cs_id();
                    let path = MononokePath::new(entry.src_path().cloned());
                    PathMutableHistory::PathAndChangeset(cs_id, path)
                }
                None => PathMutableHistory::NoChange,
            };
            Ok::<_, MononokeError>((path, rename))
        }))
        .buffered(100)
        .try_collect()
        .await?;

        self.mutable_history = Some(copy_info);
        Ok(())
    }

    /// The `RepoContext` for this query.
    pub fn repo(&self) -> &RepoContext {
        &self.repo
    }

    /// The canonical bonsai changeset ID for the changeset.
    pub fn id(&self) -> ChangesetId {
        self.id
    }

    /// Deconstruct the changeset into RepoContext and ChangesetId.
    pub fn into_repo_and_id(self) -> (RepoContext, ChangesetId) {
        let Self { repo, id, .. } = self;
        (repo, id)
    }

    /// The Mercurial ID for the changeset.
    pub async fn hg_id(&self) -> Result<Option<HgChangesetId>, MononokeError> {
        let mapping = self
            .repo()
            .blob_repo()
            .get_hg_bonsai_mapping(self.ctx().clone(), self.id)
            .await?;
        Ok(mapping.get(0).map(|(hg_cs_id, _)| *hg_cs_id))
    }

    /// The Globalrev for the changeset.
    pub async fn globalrev(&self) -> Result<Option<Globalrev>, MononokeError> {
        let mapping = self
            .repo()
            .blob_repo()
            .bonsai_globalrev_mapping()
            .get_globalrev_from_bonsai(self.ctx(), self.id)
            .await?;
        Ok(mapping.into_iter().next())
    }

    /// The SVN revision number for the changeset.
    pub async fn svnrev(&self) -> Result<Option<Svnrev>, MononokeError> {
        let mapping = self
            .repo()
            .blob_repo()
            .bonsai_svnrev_mapping()
            .get_svnrev_from_bonsai(self.ctx(), self.id)
            .await?;
        Ok(mapping)
    }

    /// The git Sha1 for the changeset (if available).
    pub async fn git_sha1(&self) -> Result<Option<GitSha1>, MononokeError> {
        Ok(self
            .repo()
            .blob_repo()
            .bonsai_git_mapping()
            .get_git_sha1_from_bonsai(self.ctx(), self.id)
            .await?)
    }

    /// Derive a derivable data type for this changeset.
    // Desugared async syntax so we can return a future with static lifetime.
    fn derive<Derivable: BonsaiDerivable>(
        &self,
    ) -> impl Future<Output = Result<Derivable, MononokeError>> + Send + 'static {
        let ctx = self.ctx().clone();
        let repo_derived_data = self.repo.blob_repo().repo_derived_data_arc();
        let id = self.id;
        async move {
            repo_derived_data
                .derive::<Derivable>(&ctx, id)
                .await
                .map_err(MononokeError::from)
        }
    }

    pub(crate) async fn root_unode_manifest_id(
        &self,
    ) -> Result<RootUnodeManifestId, MononokeError> {
        self.root_unode_manifest_id
            .get_or_init(|| self.derive::<RootUnodeManifestId>())
            .await
    }

    pub(crate) async fn root_fsnode_id(&self) -> Result<RootFsnodeId, MononokeError> {
        self.root_fsnode_id
            .get_or_init(|| self.derive::<RootFsnodeId>())
            .await
    }

    pub(crate) async fn root_basename_suffix_skeleton_manifest(
        &self,
    ) -> Result<RootBasenameSuffixSkeletonManifest, MononokeError> {
        self.root_basename_suffix_skeleton_manifest
            .get_or_init(|| self.derive::<RootBasenameSuffixSkeletonManifest>())
            .await
    }

    pub(crate) async fn root_skeleton_manifest_id(
        &self,
    ) -> Result<RootSkeletonManifestId, MononokeError> {
        self.root_skeleton_manifest_id
            .get_or_init(|| self.derive::<RootSkeletonManifestId>())
            .await
    }

    pub(crate) async fn root_deleted_manifest_v2_id(
        &self,
    ) -> Result<RootDeletedManifestV2Id, MononokeError> {
        self.root_deleted_manifest_v2_id
            .get_or_init(|| self.derive::<RootDeletedManifestV2Id>())
            .await
    }

    /// Query the root directory in the repository at this changeset revision.
    pub async fn root(&self) -> Result<ChangesetPathContentContext, MononokeError> {
        ChangesetPathContentContext::new(self.clone(), None).await
    }

    /// Query a path within the respository. This could be a file or a
    /// directory.
    ///
    /// Returns a path content context, which is a context suitable for
    /// queries about the content at this path.
    pub async fn path_with_content<P>(
        &self,
        path: P,
    ) -> Result<ChangesetPathContentContext, MononokeError>
    where
        P: TryInto<MononokePath>,
        MononokeError: From<P::Error>,
    {
        ChangesetPathContentContext::new(self.clone(), path.try_into()?).await
    }

    /// Query a path within the respository. This could be a file or a
    /// directory.
    ///
    /// Returns a path history context, which is a context suitable for
    /// queries about the history of this path.
    pub async fn path_with_history<P>(
        &self,
        path: P,
    ) -> Result<ChangesetPathHistoryContext, MononokeError>
    where
        P: TryInto<MononokePath>,
        MononokeError: From<P::Error>,
    {
        ChangesetPathHistoryContext::new(self.clone(), path.try_into()?).await
    }

    /// Query a path within the respository. This could be a file or a
    /// directory.
    ///
    /// Returns a path context, which is a context that is only suitable for
    /// queries about the type of item that exists at this path.
    ///
    /// If you need to query the content or history of a path, use
    /// `path_with_content` or `path_with_history` instead.
    pub async fn path<P>(&self, path: P) -> Result<ChangesetPathContext, MononokeError>
    where
        P: TryInto<MononokePath>,
        MononokeError: From<P::Error>,
    {
        ChangesetPathContext::new(self.clone(), path.try_into()?).await
    }

    /// Returns a stream of path history contexts for a set of paths.
    ///
    /// This performs an efficient manifest traversal, and as such returns
    /// contexts only for **paths which exist**.
    pub async fn paths_with_history(
        &self,
        paths: impl Iterator<Item = MononokePath>,
    ) -> Result<impl Stream<Item = Result<ChangesetPathHistoryContext, MononokeError>>, MononokeError>
    {
        Ok(self
            .root_unode_manifest_id()
            .await?
            .manifest_unode_id()
            .find_entries(
                self.ctx().clone(),
                self.repo().blob_repo().repo_blobstore().clone(),
                paths.map(|path| path.into_mpath()),
            )
            .map_err(MononokeError::from)
            .and_then({
                let changeset = self.clone();
                move |(mpath, entry)| {
                    ChangesetPathHistoryContext::new_with_unode_entry(
                        changeset.clone(),
                        MononokePath::new(mpath),
                        entry,
                    )
                }
            }))
    }

    /// Returns a stream of path content contexts for a set of paths.
    ///
    /// This performs an efficient manifest traversal, and as such returns
    /// contexts only for **paths which exist**.
    pub async fn paths_with_content(
        &self,
        paths: impl Iterator<Item = MononokePath>,
    ) -> Result<impl Stream<Item = Result<ChangesetPathContentContext, MononokeError>>, MononokeError>
    {
        Ok(self
            .root_fsnode_id()
            .await?
            .fsnode_id()
            .find_entries(
                self.ctx().clone(),
                self.repo().blob_repo().repo_blobstore().clone(),
                paths.map(|path| path.into_mpath()),
            )
            .map_err(MononokeError::from)
            .and_then({
                let changeset = self.clone();
                move |(mpath, entry)| {
                    cloned!(changeset);
                    async move {
                        ChangesetPathContentContext::new_with_fsnode_entry(
                            changeset.clone(),
                            MononokePath::new(mpath),
                            entry,
                        )
                        .await
                    }
                }
            }))
    }

    /// Returns a stream of path contexts for a set of paths.
    ///
    /// This performs an efficient manifest traversal, and as such returns
    /// contexts only for **paths which exist**.
    pub async fn paths(
        &self,
        paths: impl Iterator<Item = MononokePath>,
    ) -> Result<impl Stream<Item = Result<ChangesetPathContext, MononokeError>>, MononokeError>
    {
        Ok(self
            .root_skeleton_manifest_id()
            .await?
            .skeleton_manifest_id()
            .find_entries(
                self.ctx().clone(),
                self.repo().blob_repo().repo_blobstore().clone(),
                paths.map(|path| path.into_mpath()),
            )
            .map_err(MononokeError::from)
            .and_then({
                let changeset = self.clone();
                move |(mpath, entry)| {
                    ChangesetPathContext::new_with_skeleton_manifest_entry(
                        changeset.clone(),
                        MononokePath::new(mpath),
                        entry,
                    )
                }
            }))
    }

    fn deleted_paths_impl<Root: RootDeletedManifestIdCommon>(
        &self,
        root: Root,
        paths: impl Iterator<Item = MononokePath> + 'static,
    ) -> impl Stream<Item = Result<ChangesetPathHistoryContext, MononokeError>> + '_ {
        root.find_entries(
            self.ctx(),
            self.repo().blob_repo().repo_blobstore(),
            paths.map(|path| path.into_mpath()),
        )
        .map_err(MononokeError::from)
        .and_then({
            let changeset = self.clone();
            move |(mpath, entry)| {
                ChangesetPathHistoryContext::new_with_deleted_manifest::<Root::Manifest>(
                    changeset.clone(),
                    MononokePath::new(mpath),
                    entry,
                )
            }
        })
    }

    /// Returns a stream of path history contexts for a set of paths.
    ///
    /// This performs an efficient manifest traversal, and as such returns
    /// contexts only for **deleted paths which have existed previously**.
    pub async fn deleted_paths(
        &self,
        paths: impl Iterator<Item = MononokePath> + 'static,
    ) -> Result<
        impl Stream<Item = Result<ChangesetPathHistoryContext, MononokeError>> + '_,
        MononokeError,
    > {
        Ok(self.deleted_paths_impl(self.root_deleted_manifest_v2_id().await?, paths))
    }

    /// Get the `BonsaiChangeset` information for this changeset.
    async fn bonsai_changeset(&self) -> Result<BonsaiChangeset, MononokeError> {
        self.bonsai_changeset
            .get_or_init(|| {
                let ctx = self.ctx().clone();
                let blobstore = self.repo.blob_repo().repo_blobstore_arc();
                let id = self.id;
                async move { id.load(&ctx, &blobstore).await.map_err(MononokeError::from) }
            })
            .await
    }

    /// Get the `ChangesetInfo` for this changeset.
    async fn changeset_info(&self) -> Result<ChangesetInfo, MononokeError> {
        if self.repo.derive_changeset_info_enabled() {
            self.changeset_info
                .get_or_init(|| self.derive::<ChangesetInfo>())
                .await
        } else {
            let bonsai = self.bonsai_changeset().await?;
            Ok(ChangesetInfo::new(self.id(), bonsai))
        }
    }

    /// The IDs of the parents of the changeset.
    pub async fn parents(&self) -> Result<Vec<ChangesetId>, MononokeError> {
        Ok(self.changeset_info().await?.parents().collect())
    }

    /// The IDs of mutable parents of the changeset, if any.
    ///
    /// The value can be `None` to indicate that we were given a path
    /// to check, but it had no mutable parents of its own.
    ///
    /// Only returns a non-empty set if add_mutable_renames has been called
    pub fn mutable_parents(&self) -> HashSet<Option<ChangesetId>> {
        self.mutable_history
            .as_ref()
            .map_or(HashSet::new(), |info| {
                info.values()
                    .map(PathMutableHistory::get_parent_cs_id)
                    .collect()
            })
    }

    /// The author of the changeset.
    pub async fn author(&self) -> Result<String, MononokeError> {
        Ok(self.changeset_info().await?.author().to_string())
    }

    /// The date the changeset was authored.
    pub async fn author_date(&self) -> Result<DateTime<FixedOffset>, MononokeError> {
        Ok(self
            .changeset_info()
            .await?
            .author_date()
            .as_chrono()
            .clone())
    }

    /// The committer of the changeset.  May be `None` if the committer
    /// is not tracked.
    pub async fn committer(&self) -> Result<Option<String>, MononokeError> {
        Ok(self
            .changeset_info()
            .await?
            .committer()
            .map(|s| s.to_string()))
    }

    /// The date the changeset was committed.  May be `None` if the
    /// committer is not tracked.
    pub async fn committer_date(&self) -> Result<Option<DateTime<FixedOffset>>, MononokeError> {
        Ok(self
            .changeset_info()
            .await?
            .committer_date()
            .map(|d| d.as_chrono().clone()))
    }

    /// The commit message.
    pub async fn message(&self) -> Result<String, MononokeError> {
        Ok(self.changeset_info().await?.message().to_string())
    }

    /// The generation number of the given changeset
    pub async fn generation(&self) -> Result<Generation, MononokeError> {
        Ok(Generation::new(
            self.repo
                .blob_repo()
                .changesets()
                .get(self.ctx(), self.id)
                .await?
                .ok_or_else(|| {
                    MononokeError::NotAvailable(format!(
                        "Generation number missing for {:?}",
                        &self.id
                    ))
                })?
                .gen,
        ))
    }

    /// All mercurial commit extras as (name, value) pairs.
    pub async fn hg_extras(&self) -> Result<Vec<(String, Vec<u8>)>, MononokeError> {
        Ok(self
            .changeset_info()
            .await?
            .hg_extra()
            .map(|(name, value)| (name.to_string(), Vec::from(value)))
            .collect())
    }

    pub async fn git_extra_headers(
        &self,
    ) -> Result<Option<Vec<(SmallVec<[u8; 24]>, Bytes)>>, MononokeError> {
        Ok(self
            .changeset_info()
            .await?
            .git_extra_headers()
            .map(|headers| {
                headers
                    .map(|(key, value)| (SmallVec::from(key), Bytes::copy_from_slice(value)))
                    .collect()
            }))
    }

    /// File changes associated with the commit.
    pub async fn file_changes(&self) -> Result<SortedVectorMap<MPath, FileChange>, MononokeError> {
        let bonsai = self.bonsai_changeset().await?;
        let bonsai = bonsai.into_mut();
        Ok(bonsai.file_changes)
    }

    /// Returns `true` if this commit is an ancestor of `other_commit`.  A commit is considered its
    /// own ancestor for the purpose of this call.
    pub async fn is_ancestor_of(&self, other_commit: ChangesetId) -> Result<bool, MononokeError> {
        Ok(self
            .repo()
            .repo()
            .commit_graph()
            .is_ancestor(self.ctx(), self.id, other_commit)
            .await?)
    }

    /// Returns the lowest common ancestor of two commits.
    ///
    /// In case of ambiguity (can happen with multiple merges of the same branches) returns the
    /// common ancestor with lowest id out of those with highest generation number.
    pub async fn common_base_with(
        &self,
        other_commit: ChangesetId,
    ) -> Result<Option<ChangesetContext>, MononokeError> {
        let lca = self
            .repo()
            .repo()
            .commit_graph()
            .common_base(self.ctx(), self.id, other_commit)
            .await?;
        Ok(lca.get(0).map(|id| Self::new(self.repo.clone(), *id)))
    }

    pub async fn diff_unordered(
        &self,
        other: &ChangesetContext,
        include_copies_renames: bool,
        path_restrictions: Option<Vec<MononokePath>>,
        diff_items: BTreeSet<ChangesetDiffItem>,
    ) -> Result<Vec<ChangesetPathDiffContext>, MononokeError> {
        self.diff(
            other,
            include_copies_renames,
            path_restrictions,
            diff_items,
            ChangesetFileOrdering::Unordered,
            None,
        )
        .await
    }

    /// Returns differences between this changeset and some other changeset.
    ///
    /// `self` is considered the "new" changeset (so files missing there are "Removed")
    /// `other` is considered the "old" changeset (so files missing there are "Added")
    /// `include_copies_renames` is only available for files when diffing commits with its parent
    /// `path_restrictions` if present will narrow down the diff to given paths
    /// `diff_items` what to include in the output (files, dirs or both)
    pub async fn diff(
        &self,
        other: &ChangesetContext,
        include_copies_renames: bool,
        path_restrictions: Option<Vec<MononokePath>>,
        diff_items: BTreeSet<ChangesetDiffItem>,
        ordering: ChangesetFileOrdering,
        limit: Option<usize>,
    ) -> Result<Vec<ChangesetPathDiffContext>, MononokeError> {
        // Helper to that checks if a path is within the givien path restrictions
        fn within_restrictions(
            path: &MononokePath,
            path_restrictions: &Option<Vec<MononokePath>>,
        ) -> bool {
            path_restrictions.as_ref().map_or(true, |i| {
                i.iter().any(|path_restriction| {
                    is_related_to(path.as_mpath(), path_restriction.as_mpath())
                })
            })
        }

        // map from from_path to to_paths (there may be multiple copies
        // for each from_path, so this maps to a vector of paths)
        let mut copy_path_map = HashMap::new();
        // map from to_path to from_path
        let mut inv_copy_path_map = HashMap::new();
        let file_changes = self.file_changes().await?;
        // For now we only consider copies when comparing with parent, or using mutable history
        if include_copies_renames
            && (self.mutable_history.is_some() || self.parents().await?.contains(&other.id))
        {
            let mut to_paths = HashSet::new();
            if let Some(overrides) = &self.mutable_history {
                for (dst_path, mutable_history) in overrides {
                    if let Some((cs_id, path)) = mutable_history.get_copy_from() {
                        if cs_id == other.id() {
                            copy_path_map
                                .entry(path.clone())
                                .or_insert_with(Vec::new)
                                .push(dst_path.clone());
                            to_paths.insert(dst_path.clone());
                        }
                    }
                }
            }

            for (to_path, file_change) in file_changes.iter() {
                let to_path = MononokePath::new(Some(to_path.clone()));
                let path_is_overriden = self
                    .mutable_history
                    .as_ref()
                    .and_then(|history_map| {
                        history_map
                            .get(&to_path)
                            .map(PathMutableHistory::is_override)
                    })
                    .unwrap_or(false);
                if path_is_overriden {
                    // Mutable history overrides immutable if present
                    continue;
                }
                match file_change {
                    FileChange::Change(tc) => {
                        if let Some((from_path, csid)) = tc.copy_from() {
                            let from_path = MononokePath::new(Some(from_path.clone()));
                            if *csid == other.id {
                                copy_path_map
                                    .entry(from_path)
                                    .or_insert_with(Vec::new)
                                    .push(to_path.clone());
                                to_paths.insert(to_path);
                            }
                        }
                    }
                    FileChange::Deletion
                    | FileChange::UntrackedDeletion
                    | FileChange::UntrackedChange(_) => {}
                }
            }

            let other_root_fsnode_id = other.root_fsnode_id().await?;

            // Prefetch fsnode entries for all "from paths" so that we don't need
            // to refetch them later
            let from_path_to_mf_entry = other_root_fsnode_id
                .fsnode_id()
                .find_entries(
                    self.ctx().clone(),
                    self.repo().blob_repo().repo_blobstore().clone(),
                    copy_path_map.keys().cloned().map(MononokePath::into_mpath),
                )
                .map_ok(|(maybe_from_path, entry)| (MononokePath::new(maybe_from_path), entry))
                .try_collect::<HashMap<_, _>>();

            // At the same time, find out whether the destinations of copies
            // already existed in the parent.
            let to_path_exists_in_parent = other_root_fsnode_id
                .fsnode_id()
                .find_entries(
                    self.ctx().clone(),
                    other.repo().blob_repo().repo_blobstore().clone(),
                    to_paths.into_iter().map(MononokePath::into_mpath),
                )
                .map_ok(|(maybe_to_path, _entry)| MononokePath::new(maybe_to_path))
                .try_collect::<HashSet<_>>();

            let (from_path_to_mf_entry, to_path_exists_in_parent) =
                try_join(from_path_to_mf_entry, to_path_exists_in_parent).await?;

            // Filter out:
            // - Copies where the to_path already existed in the parent.  These don't show up as
            //   copies in the diff view.
            // - Copies where the from_path didn't exist in the parent.  These are indicative of
            //   an invalid bonsai changeset or mutable rename and can be ignored.
            copy_path_map.retain(|from_path, to_paths| {
                to_paths.retain(|to_path| !to_path_exists_in_parent.contains(to_path));
                !to_paths.is_empty() && from_path_to_mf_entry.contains_key(from_path)
            });

            // Build the inverse copy map (from to_path to from_path),
            // which includes the manifest entry for the from_path.
            for (from_path, to_paths) in copy_path_map.iter() {
                let mf_entry = from_path_to_mf_entry.get(from_path).ok_or_else(|| {
                    MononokeError::from(anyhow!(
                        "internal error: cannot find {:?} in parent commit",
                        from_path
                    ))
                })?;
                for to_path in to_paths {
                    inv_copy_path_map.insert(to_path, (from_path, mf_entry.clone()));
                }
            }
        }

        // set of paths from other that were copied in (not moved)
        // We check if `self` contains paths that were source for copy or move in `other`
        // If self does contain a path, then we consider it to be a copy, otherwise
        // it's a move to the first location it was copied to.
        let copied_paths = self
            .root_fsnode_id()
            .await?
            .fsnode_id()
            .find_entries(
                self.ctx().clone(),
                self.repo().blob_repo().repo_blobstore().clone(),
                copy_path_map.keys().cloned().map(MononokePath::into_mpath),
            )
            .map_ok(|(maybe_from_path, _)| MononokePath::new(maybe_from_path))
            .try_collect::<HashSet<_>>()
            .await?;

        let (self_manifest_root, other_manifest_root) =
            try_join(self.root_fsnode_id(), other.root_fsnode_id()).await?;

        let diff_files = diff_items.contains(&ChangesetDiffItem::FILES);
        let diff_trees = diff_items.contains(&ChangesetDiffItem::TREES);

        let recurse_pruner = {
            cloned!(path_restrictions);
            move |tree_diff: &ManifestDiff<_>| match tree_diff {
                ManifestDiff::Added(path, ..)
                | ManifestDiff::Changed(path, ..)
                | ManifestDiff::Removed(path, ..) => {
                    let path = MononokePath::new(path.clone());
                    within_restrictions(&path, &path_restrictions)
                }
            }
        };

        let diff = match ordering {
            ChangesetFileOrdering::Unordered => {
                // We start from "other" as manfest.diff() is backwards
                other_manifest_root
                    .fsnode_id()
                    .filtered_diff(
                        self.ctx().clone(),
                        self.repo().blob_repo().repo_blobstore().clone(),
                        self_manifest_root.fsnode_id().clone(),
                        self.repo().blob_repo().repo_blobstore().clone(),
                        Some,
                        recurse_pruner,
                    )
                    .left_stream()
            }
            ChangesetFileOrdering::Ordered { after } => {
                // We start from "other" as manfest.diff() is backwards
                other_manifest_root
                    .fsnode_id()
                    .filtered_diff_ordered(
                        self.ctx().clone(),
                        self.repo().blob_repo().repo_blobstore().clone(),
                        self_manifest_root.fsnode_id().clone(),
                        self.repo().blob_repo().repo_blobstore().clone(),
                        after.map(MononokePath::into_mpath),
                        Some,
                        recurse_pruner,
                    )
                    .right_stream()
            }
        };

        let change_contexts = diff
            .try_filter_map(|diff_entry| {
                async {
                    let entry = match diff_entry {
                        ManifestDiff::Added(path, entry @ ManifestEntry::Leaf(_)) => {
                            let path = MononokePath::new(path);
                            if !diff_files || !within_restrictions(&path, &path_restrictions) {
                                None
                            } else if let Some((from_path, from_entry)) =
                                inv_copy_path_map.get(&path)
                            {
                                // There's copy information that we can use.
                                if copied_paths.contains(from_path)
                                    || copy_path_map
                                        .get(*from_path)
                                        .and_then(|to_paths| to_paths.first())
                                        != Some(&path)
                                {
                                    // If the source still exists in the current
                                    // commit, or this isn't the first place it
                                    // was copied to, it was a copy.
                                    let from = ChangesetPathContentContext::new_with_fsnode_entry(
                                        other.clone(),
                                        (**from_path).clone(),
                                        *from_entry,
                                    )
                                    .await?;
                                    Some(ChangesetPathDiffContext::Copied(
                                        ChangesetPathContentContext::new_with_fsnode_entry(
                                            self.clone(),
                                            path,
                                            entry,
                                        )
                                        .await?,
                                        from,
                                    ))
                                } else {
                                    // If it doesn't, and this is the first place
                                    // it was copied to, it was a move.
                                    let from = ChangesetPathContentContext::new_with_fsnode_entry(
                                        other.clone(),
                                        (**from_path).clone(),
                                        *from_entry,
                                    )
                                    .await?;
                                    Some(ChangesetPathDiffContext::Moved(
                                        ChangesetPathContentContext::new_with_fsnode_entry(
                                            self.clone(),
                                            path,
                                            entry,
                                        )
                                        .await?,
                                        from,
                                    ))
                                }
                            } else {
                                Some(ChangesetPathDiffContext::Added(
                                    ChangesetPathContentContext::new_with_fsnode_entry(
                                        self.clone(),
                                        path,
                                        entry,
                                    )
                                    .await?,
                                ))
                            }
                        }
                        ManifestDiff::Removed(path, entry @ ManifestEntry::Leaf(_)) => {
                            let path = MononokePath::new(path);
                            #[allow(clippy::if_same_then_else)]
                            if copy_path_map.get(&path).is_some() {
                                // The file is was moved (not removed), it will be covered by a "Moved" entry.
                                None
                            } else if !diff_files || !within_restrictions(&path, &path_restrictions)
                            {
                                None
                            } else {
                                Some(ChangesetPathDiffContext::Removed(
                                    ChangesetPathContentContext::new_with_fsnode_entry(
                                        other.clone(),
                                        path,
                                        entry,
                                    )
                                    .await?,
                                ))
                            }
                        }
                        ManifestDiff::Changed(
                            path,
                            from_entry @ ManifestEntry::Leaf(_),
                            to_entry @ ManifestEntry::Leaf(_),
                        ) => {
                            let path = MononokePath::new(path);
                            if !diff_files || !within_restrictions(&path, &path_restrictions) {
                                None
                            } else {
                                Some(ChangesetPathDiffContext::Changed(
                                    ChangesetPathContentContext::new_with_fsnode_entry(
                                        self.clone(),
                                        path.clone(),
                                        to_entry,
                                    )
                                    .await?,
                                    ChangesetPathContentContext::new_with_fsnode_entry(
                                        other.clone(),
                                        path,
                                        from_entry,
                                    )
                                    .await?,
                                ))
                            }
                        }
                        ManifestDiff::Added(path, entry @ ManifestEntry::Tree(_)) => {
                            let path = MononokePath::new(path);
                            if !diff_trees || !within_restrictions(&path, &path_restrictions) {
                                None
                            } else {
                                Some(ChangesetPathDiffContext::Added(
                                    ChangesetPathContentContext::new_with_fsnode_entry(
                                        self.clone(),
                                        path,
                                        entry,
                                    )
                                    .await?,
                                ))
                            }
                        }
                        ManifestDiff::Removed(path, entry @ ManifestEntry::Tree(_)) => {
                            let path = MononokePath::new(path);
                            if !diff_trees || !within_restrictions(&path, &path_restrictions) {
                                None
                            } else {
                                Some(ChangesetPathDiffContext::Removed(
                                    ChangesetPathContentContext::new_with_fsnode_entry(
                                        self.clone(),
                                        path,
                                        entry,
                                    )
                                    .await?,
                                ))
                            }
                        }
                        ManifestDiff::Changed(
                            path,
                            from_entry @ ManifestEntry::Tree(_),
                            to_entry @ ManifestEntry::Tree(_),
                        ) => {
                            let path = MononokePath::new(path);
                            if !diff_trees || !within_restrictions(&path, &path_restrictions) {
                                None
                            } else {
                                Some(ChangesetPathDiffContext::Changed(
                                    ChangesetPathContentContext::new_with_fsnode_entry(
                                        self.clone(),
                                        path.clone(),
                                        to_entry,
                                    )
                                    .await?,
                                    ChangesetPathContentContext::new_with_fsnode_entry(
                                        other.clone(),
                                        path,
                                        from_entry,
                                    )
                                    .await?,
                                ))
                            }
                        }
                        // We've already covered all practical possiblities as there are no "changed"
                        // between from trees and files as such are represented as removal+addition
                        _ => None,
                    };
                    Ok(entry)
                }
            })
            .take(limit.unwrap_or(usize::MAX))
            .try_collect::<Vec<_>>()
            .await?;
        Ok(change_contexts)
    }

    async fn find_entries(
        &self,
        prefixes: Option<Vec1<MononokePath>>,
        ordering: ChangesetFileOrdering,
    ) -> Result<
        impl Stream<
            Item = Result<(Option<MPath>, ManifestEntry<SkeletonManifestId, ()>), anyhow::Error>,
        >,
        MononokeError,
    > {
        let root = self.root_skeleton_manifest_id().await?;
        let prefixes = match prefixes {
            Some(prefixes) => prefixes
                .into_iter()
                .map(|prefix| PathOrPrefix::Prefix(prefix.into()))
                .collect(),
            None => vec![PathOrPrefix::Prefix(None)],
        };
        let entries = match ordering {
            ChangesetFileOrdering::Unordered => root
                .skeleton_manifest_id()
                .find_entries(
                    self.ctx().clone(),
                    self.repo().blob_repo().repo_blobstore().clone(),
                    prefixes,
                )
                .left_stream(),
            ChangesetFileOrdering::Ordered { after } => root
                .skeleton_manifest_id()
                .find_entries_ordered(
                    self.ctx().clone(),
                    self.repo().blob_repo().repo_blobstore().clone(),
                    prefixes,
                    after.map(MononokePath::into_mpath),
                )
                .right_stream(),
        };
        Ok(entries)
    }

    pub async fn find_files_unordered(
        &self,
        prefixes: Option<Vec<MononokePath>>,
        basenames: Option<Vec<String>>,
    ) -> Result<impl Stream<Item = Result<MononokePath, MononokeError>> + '_, MononokeError> {
        self.find_files(
            prefixes,
            basenames,
            // None for basename_suffixes
            None,
            ChangesetFileOrdering::Unordered,
        )
        .await
    }

    /// Find files after applying filters on the prefix and basename.
    /// A files is returned if the following conditions hold:
    /// - `prefixes` is None, or there is an element of `prefixes` such that the
    ///   element is a prefix of the file path.
    /// - the basename of the file path is in `basenames`, or there is a string
    ///   in `basename_suffixes` that is a suffix of the basename of the file,
    ///   or both `basenames` and `basename_suffixes` are None.
    /// The order that files are returned is based on the parameter `ordering`.
    /// To continue a paginated query, use the parameter `ordering`.
    pub async fn find_files(
        &self,
        prefixes: Option<Vec<MononokePath>>,
        basenames: Option<Vec<String>>,
        basename_suffixes: Option<Vec<String>>,
        ordering: ChangesetFileOrdering,
    ) -> Result<impl Stream<Item = Result<MononokePath, MononokeError>> + '_, MononokeError> {
        let basenames_and_suffixes = match (to_vec1(basenames), to_vec1(basename_suffixes)) {
            (None, None) => None,
            (Some(basenames), None) => Some(EitherOrBoth::Left(basenames)),
            (None, Some(suffixes)) => Some(EitherOrBoth::Right(suffixes)),
            (Some(basenames), Some(suffixes)) => Some(EitherOrBoth::Both(basenames, suffixes)),
        };
        Ok(match basenames_and_suffixes {
            Some(basenames_and_suffixes)
                if !tunables()
                    .disable_basename_suffix_skeleton_manifest()
                    .unwrap_or_default()
                    && (!basenames_and_suffixes.has_right()
                        || tunables().enable_bssm_suffix_query().unwrap_or_default()) =>
            {
                self.find_files_with_bssm(prefixes, basenames_and_suffixes, ordering)
                    .await?
                    .left_stream()
            }
            basenames_and_suffixes => {
                let (basenames, basename_suffixes) = basenames_and_suffixes
                    .map_or((None, None), |b| b.map_any(Some, Some).or_default());
                self.find_files_without_bssm(
                    to_vec1(prefixes),
                    basenames,
                    basename_suffixes,
                    ordering,
                )
                .await?
                .right_stream()
            }
        })
    }

    pub(crate) async fn find_files_with_bssm(
        &self,
        prefixes: Option<Vec<MononokePath>>,
        basenames_and_suffixes: EitherOrBoth<Vec1<String>, Vec1<String>>,
        ordering: ChangesetFileOrdering,
    ) -> Result<impl Stream<Item = Result<MononokePath, MononokeError>> + '_, MononokeError> {
        Ok(self
            .root_basename_suffix_skeleton_manifest()
            .await?
            .find_files_filter_basenames(
                self.ctx(),
                self.repo().blob_repo().repo_blobstore().clone(),
                prefixes
                    .unwrap_or_else(Vec::new)
                    .into_iter()
                    .map(MononokePath::into_mpath)
                    .collect(),
                basenames_and_suffixes,
                match ordering {
                    ChangesetFileOrdering::Unordered => None,
                    ChangesetFileOrdering::Ordered { after } => {
                        Some(after.map(MononokePath::into_mpath))
                    }
                },
            )
            .await
            .map_err(MononokeError::from)?
            .map(|r| match r {
                Ok(p) => Ok(MononokePath::new(p)),
                Err(err) => Err(MononokeError::from(err)),
            }))
    }

    pub(crate) async fn find_files_without_bssm(
        &self,
        prefixes: Option<Vec1<MononokePath>>,
        basenames: Option<Vec1<String>>,
        basename_suffixes: Option<Vec1<String>>,
        ordering: ChangesetFileOrdering,
    ) -> Result<impl Stream<Item = Result<MononokePath, MononokeError>>, MononokeError> {
        // First, find the entries, and filter by file prefix.
        let entries = self.find_entries(prefixes, ordering).await?;
        let mpaths = entries.try_filter_map(|(path, entry)| async move {
            match (path, entry) {
                (Some(mpath), ManifestEntry::Leaf(_)) => Ok(Some(mpath)),
                _ => Ok(None),
            }
        });

        // Now, construct a set of basenames to include.
        // These basenames are of type MPathElement rather than being strings.
        let basenames_as_mpath_elements_set = match basenames {
            Some(basenames) => Some(
                basenames
                    .into_iter()
                    .map(|basename| MPathElement::new(basename.into()))
                    .collect::<Result<HashSet<_>, _>>()
                    .map_err(MononokeError::from)?,
            ),
            None => None,
        };

        // Now, filter by basename. We use "left_stream" and "right_stream" to
        // satisfy the type checker, because filtering a stream creates a
        // different "type". Using left and right streams creates an Either type
        // which satisfies the type checker.
        let mpaths = match (basenames_as_mpath_elements_set, basename_suffixes) {
            // If basenames and suffixes are provided, include basenames in
            // the set basenames_as_mpath_elements_set as well as basenames
            // with a suffix in basename_suffixes.
            (Some(basenames_as_mpath_elements_set), Some(basename_suffixes)) => mpaths
                .try_filter(move |mpath| {
                    let basename = mpath.basename();
                    future::ready(
                        basenames_as_mpath_elements_set.contains(basename)
                            || basename_suffixes
                                .iter()
                                .any(|suffix| basename.has_suffix(suffix.as_bytes())),
                    )
                })
                .left_stream()
                .left_stream(),
            // If no suffixes are provided, only match on basenames that are
            // in the set.
            (Some(basenames_as_mpath_elements_set), None) => mpaths
                .try_filter(move |mpath| {
                    future::ready(basenames_as_mpath_elements_set.contains(mpath.basename()))
                })
                .left_stream()
                .right_stream(),
            (None, Some(basename_suffixes)) =>
            // If only suffixes are provided, match on basenames that have a
            // suffix in basename_suffixes.
            {
                mpaths
                    .try_filter(move |mpath| {
                        let basename = mpath.basename();
                        future::ready(
                            basename_suffixes
                                .iter()
                                .any(|suffix| basename.has_suffix(suffix.as_bytes())),
                        )
                    })
                    .right_stream()
                    .left_stream()
            }
            // Otherwise, there are no basename filters, so do not filter.
            (None, None) => mpaths.right_stream().right_stream(),
        };

        Ok(mpaths
            .map_ok(|mpath| MononokePath::new(Some(mpath)))
            .map_err(MononokeError::from))
    }

    /// Returns a stream of `ChangesetContext` for the history of the repository from this commit.
    pub async fn history(
        &self,
        opts: ChangesetHistoryOptions,
    ) -> Result<BoxStream<'_, Result<ChangesetContext, MononokeError>>, MononokeError> {
        let mut ancestors_stream_builder = AncestorsStreamBuilder::new(
            self.repo().repo().commit_graph_arc(),
            self.ctx().clone(),
            vec![self.id()],
        );

        if let Some(until_timestamp) = opts.until_timestamp {
            ancestors_stream_builder = ancestors_stream_builder.with({
                let ctx = self.ctx().clone();
                let repo = self.repo().clone();
                let cs_info_enabled = repo.derive_changeset_info_enabled();
                move |cs_id| {
                    cloned!(ctx, repo, cs_info_enabled);
                    async move {
                        let info = if cs_info_enabled {
                            ChangesetInfo::derive(&ctx, repo.repo(), cs_id).await?
                        } else {
                            let bonsai = cs_id.load(&ctx, repo.repo().repo_blobstore()).await?;
                            ChangesetInfo::new(cs_id, bonsai)
                        };
                        let date = info.author_date().as_chrono().clone();
                        Ok(date.timestamp() >= until_timestamp)
                    }
                }
            });
        }

        if let Some(descendants_of) = opts.descendants_of {
            ancestors_stream_builder = ancestors_stream_builder.descendants_of(descendants_of);
        }

        if let Some(exclude_changeset_and_ancestors) = opts.exclude_changeset_and_ancestors {
            ancestors_stream_builder = ancestors_stream_builder
                .exclude_ancestors_of(vec![exclude_changeset_and_ancestors]);
        }

        let cs_ids_stream = ancestors_stream_builder.build().await?;

        Ok(cs_ids_stream
            .map_err(MononokeError::from)
            .and_then(move |cs_id| async move {
                Ok::<_, MononokeError>(ChangesetContext::new(self.repo().clone(), cs_id))
            })
            .boxed())
    }

    pub async fn diff_root_unordered(
        &self,
        path_restrictions: Option<Vec<MononokePath>>,
        diff_items: BTreeSet<ChangesetDiffItem>,
    ) -> Result<Vec<ChangesetPathDiffContext>, MononokeError> {
        self.diff_root(
            path_restrictions,
            diff_items,
            ChangesetFileOrdering::Unordered,
            None,
        )
        .await
    }

    /// Returns additions introduced by the root commit, a.k.a the initial commit
    ///
    /// `self` is considered the "root/initial/genesis" changeset
    /// `path_restrictions` if present will narrow down the diff to given paths
    /// `diff_items` what to include in the output (files, dirs or both)
    pub async fn diff_root(
        &self,
        path_restrictions: Option<Vec<MononokePath>>,
        diff_items: BTreeSet<ChangesetDiffItem>,
        ordering: ChangesetFileOrdering,
        limit: Option<usize>,
    ) -> Result<Vec<ChangesetPathDiffContext>, MononokeError> {
        let diff_files = diff_items.contains(&ChangesetDiffItem::FILES);
        let diff_trees = diff_items.contains(&ChangesetDiffItem::TREES);

        self.find_entries(to_vec1(path_restrictions), ordering)
            .await?
            .try_filter_map(|(path, entry)| async move {
                match (path, entry) {
                    (Some(mpath), ManifestEntry::Leaf(_)) if diff_files => Ok(Some(mpath)),
                    (Some(mpath), ManifestEntry::Tree(_)) if diff_trees => Ok(Some(mpath)),
                    _ => Ok(None),
                }
            })
            .map_ok(|mpath| MononokePath::new(Some(mpath)))
            .map_err(MononokeError::from)
            .take(limit.unwrap_or(usize::MAX))
            .and_then(|mp| async move {
                Ok(ChangesetPathDiffContext::Added(
                    ChangesetPathContentContext::new(self.clone(), mp).await?,
                ))
            })
            .try_collect::<Vec<_>>()
            .await
    }

    pub async fn run_hooks(
        &self,
        bookmark: impl AsRef<str>,
        pushvars: Option<&HashMap<String, Bytes>>,
    ) -> Result<Vec<HookOutcome>, MononokeError> {
        Ok(self
            .repo()
            .hook_manager()
            .run_hooks_for_bookmark(
                self.ctx(),
                vec![self.bonsai_changeset().await?].iter(),
                &BookmarkKey::new(bookmark.as_ref())?,
                pushvars,
                CrossRepoPushSource::NativeToThisRepo,
                PushAuthoredBy::User,
            )
            .await?)
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::collections::HashMap;
use std::fmt;

use anyhow::anyhow;
use anyhow::Error;
use async_trait::async_trait;
use blobstore::Blobstore;
use blobstore::Loadable;
use bytes::Bytes;
use changeset_fetcher::ChangesetFetcherArc;
use changeset_info::ChangesetInfo;
use cloned::cloned;
use commit_graph::CommitGraphRef;
use context::CoreContext;
use deleted_manifest::DeletedManifestOps;
use deleted_manifest::RootDeletedManifestIdCommon;
use derived_data::BonsaiDerived;
use filestore::FetchKey;
use futures::future::try_join_all;
use futures::future::TryFutureExt;
use futures::stream;
use futures::stream::BoxStream;
use futures::stream::StreamExt;
use futures::stream::TryStreamExt;
use futures_lazy_shared::LazyShared;
use history_traversal::list_file_history;
use history_traversal::CsAndPath;
use history_traversal::FastlogError;
use history_traversal::FollowMutableFileHistory;
use history_traversal::HistoryAcrossDeletions;
use history_traversal::TraversalOrder;
use history_traversal::Visitor;
use manifest::Entry;
use manifest::ManifestOps;
use mononoke_types::blame_v2::BlameV2;
use mononoke_types::deleted_manifest_common::DeletedManifestCommon;
use mononoke_types::fsnode::FsnodeFile;
use mononoke_types::ChangesetId;
use mononoke_types::ContentMetadataV2;
/// Metadata about a file.
pub use mononoke_types::ContentMetadataV2 as FileMetadata;
use mononoke_types::FileType;
use mononoke_types::FileUnodeId;
use mononoke_types::FsnodeId;
use mononoke_types::ManifestUnodeId;
use mononoke_types::SkeletonManifestId;
use repo_blobstore::RepoBlobstoreRef;

use crate::changeset::ChangesetContext;
use crate::errors::MononokeError;
use crate::file::FileContext;
use crate::path::MononokePath;
use crate::repo::RepoContext;
use crate::tree::TreeContext;

pub struct HistoryEntry {
    pub name: String,
    pub changeset_id: ChangesetId,
}

#[derive(Default)]
pub struct ChangesetPathHistoryOptions {
    pub until_timestamp: Option<i64>,
    pub descendants_of: Option<ChangesetId>,
    pub exclude_changeset_and_ancestors: Option<ChangesetId>,
    pub follow_history_across_deletions: bool,
    pub follow_mutable_file_history: bool,
}

pub enum PathEntry {
    NotPresent,
    Tree(TreeContext),
    File(FileContext, FileType),
}

type UnodeResult = Result<Option<Entry<ManifestUnodeId, FileUnodeId>>, MononokeError>;
type FsnodeResult = Result<Option<Entry<FsnodeId, FsnodeFile>>, MononokeError>;
type SkeletonResult = Result<Option<Entry<SkeletonManifestId, ()>>, MononokeError>;
type LinknodeResult = Result<Option<ChangesetId>, MononokeError>;

/// Context that makes it cheap to fetch content info about a path within a changeset.
///
/// A ChangesetPathContentContext may represent a file, a directory, a path where a
/// file or directory has been deleted, or a path where nothing ever existed.
#[derive(Clone)]
pub struct ChangesetPathContentContext {
    changeset: ChangesetContext,
    path: MononokePath,
    fsnode_id: LazyShared<FsnodeResult>,
}

impl fmt::Debug for ChangesetPathContentContext {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(
            f,
            "ChangesetPathContentContext(repo={:?} id={:?} path={:?})",
            self.repo().name(),
            self.changeset().id(),
            self.path()
        )
    }
}

/// Context that makes it cheap to fetch history info about a path within a changeset.
pub struct ChangesetPathHistoryContext {
    changeset: ChangesetContext,
    path: MononokePath,
    unode_id: LazyShared<UnodeResult>,
    linknode: LazyShared<LinknodeResult>,
}

impl fmt::Debug for ChangesetPathHistoryContext {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(
            f,
            "ChangesetPathHistoryContext(repo={:?} id={:?} path={:?})",
            self.repo().name(),
            self.changeset().id(),
            self.path()
        )
    }
}

/// Context to check if a file or a directory exists in a changeset
pub struct ChangesetPathContext {
    changeset: ChangesetContext,
    path: MononokePath,
    skeleton_manifest_id: LazyShared<SkeletonResult>,
}

impl fmt::Debug for ChangesetPathContext {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(
            f,
            "ChangesetPathContext(repo={:?} id={:?} path={:?})",
            self.repo().name(),
            self.changeset().id(),
            self.path()
        )
    }
}

impl ChangesetPathContentContext {
    pub(crate) async fn new(
        changeset: ChangesetContext,
        path: impl Into<MononokePath>,
    ) -> Result<Self, MononokeError> {
        let path = path.into();
        changeset
            .repo()
            .authorization_context()
            .require_path_read(
                changeset.ctx(),
                changeset.repo().inner_repo(),
                changeset.id(),
                path.as_mpath(),
            )
            .await?;
        Ok(Self {
            changeset,
            path,
            fsnode_id: LazyShared::new_empty(),
        })
    }

    pub(crate) async fn new_with_fsnode_entry(
        changeset: ChangesetContext,
        path: impl Into<MononokePath>,
        fsnode_entry: Entry<FsnodeId, FsnodeFile>,
    ) -> Result<Self, MononokeError> {
        let path = path.into();
        changeset
            .repo()
            .authorization_context()
            .require_path_read(
                changeset.ctx(),
                changeset.repo().inner_repo(),
                changeset.id(),
                path.as_mpath(),
            )
            .await?;
        Ok(Self {
            changeset,
            path,
            fsnode_id: LazyShared::new_ready(Ok(Some(fsnode_entry))),
        })
    }

    /// The `RepoContext` for this query.
    pub fn repo(&self) -> &RepoContext {
        self.changeset.repo()
    }

    /// The `ChangesetContext` for this query.
    pub fn changeset(&self) -> &ChangesetContext {
        &self.changeset
    }

    /// The path for this query.
    pub fn path(&self) -> &MononokePath {
        &self.path
    }

    async fn fsnode_id(&self) -> Result<Option<Entry<FsnodeId, FsnodeFile>>, MononokeError> {
        self.fsnode_id
            .get_or_init(|| {
                cloned!(self.changeset, self.path);
                async move {
                    let ctx = changeset.ctx().clone();
                    let blobstore = changeset.repo().blob_repo().repo_blobstore().clone();
                    let root_fsnode_id = changeset.root_fsnode_id().await?;
                    if let Some(mpath) = path.into() {
                        root_fsnode_id
                            .fsnode_id()
                            .find_entry(ctx, blobstore, Some(mpath))
                            .await
                            .map_err(MononokeError::from)
                    } else {
                        Ok(Some(Entry::Tree(root_fsnode_id.fsnode_id().clone())))
                    }
                }
            })
            .await
    }

    /// Returns `true` if the path exists (as a file or directory) in this commit.
    pub async fn exists(&self) -> Result<bool, MononokeError> {
        // The path exists if there is any kind of fsnode.
        Ok(self.fsnode_id().await?.is_some())
    }

    pub async fn is_file(&self) -> Result<bool, MononokeError> {
        let is_file = match self.fsnode_id().await? {
            Some(Entry::Leaf(_)) => true,
            _ => false,
        };
        Ok(is_file)
    }

    pub async fn is_tree(&self) -> Result<bool, MononokeError> {
        let is_tree = match self.fsnode_id().await? {
            Some(Entry::Tree(_)) => true,
            _ => false,
        };
        Ok(is_tree)
    }

    pub async fn file_type(&self) -> Result<Option<FileType>, MononokeError> {
        let file_type = match self.fsnode_id().await? {
            Some(Entry::Leaf(file)) => Some(*file.file_type()),
            _ => None,
        };
        Ok(file_type)
    }

    /// Returns a `TreeContext` for the tree at this path.  Returns `None` if the path
    /// is not a directory in this commit.
    pub async fn tree(&self) -> Result<Option<TreeContext>, MononokeError> {
        let tree = match self.fsnode_id().await? {
            Some(Entry::Tree(fsnode_id)) => {
                Some(TreeContext::new_authorized(self.repo().clone(), fsnode_id))
            }
            _ => None,
        };
        Ok(tree)
    }

    /// Returns a `FileContext` for the file at this path.  Returns `None` if the path
    /// is not a file in this commit.
    pub async fn file(&self) -> Result<Option<FileContext>, MononokeError> {
        let file = match self.fsnode_id().await? {
            Some(Entry::Leaf(file)) => Some(FileContext::new_authorized(
                self.repo().clone(),
                FetchKey::Canonical(*file.content_id()),
            )),
            _ => None,
        };
        Ok(file)
    }

    pub async fn file_content(&self) -> Result<Option<Bytes>, MononokeError> {
        Ok(match self.file().await? {
            Some(context) => Some(context.content_concat().await?),
            None => None,
        })
    }

    pub async fn file_metadata(&self) -> Result<Option<ContentMetadataV2>, MononokeError> {
        let metadata = match self.file().await? {
            Some(context) => Some(context.metadata().await?),
            None => None,
        };
        Ok(metadata)
    }

    /// Returns a `TreeContext` or `FileContext` and `FileType` for the tree
    /// or file at this path. Returns `NotPresent` if the path is not a file
    /// or directory in this commit.
    pub async fn entry(&self) -> Result<PathEntry, MononokeError> {
        let entry = match self.fsnode_id().await? {
            Some(Entry::Tree(fsnode_id)) => {
                PathEntry::Tree(TreeContext::new_authorized(self.repo().clone(), fsnode_id))
            }
            Some(Entry::Leaf(file)) => PathEntry::File(
                FileContext::new_authorized(
                    self.repo().clone(),
                    FetchKey::Canonical(*file.content_id()),
                ),
                *file.file_type(),
            ),
            _ => PathEntry::NotPresent,
        };
        Ok(entry)
    }
}

impl ChangesetPathHistoryContext {
    pub(crate) async fn new(
        changeset: ChangesetContext,
        path: impl Into<MononokePath>,
    ) -> Result<Self, MononokeError> {
        let path = path.into();
        changeset
            .repo()
            .authorization_context()
            .require_path_read(
                changeset.ctx(),
                changeset.repo().inner_repo(),
                changeset.id(),
                path.as_mpath(),
            )
            .await?;
        Ok(Self {
            changeset,
            path,
            unode_id: LazyShared::new_empty(),
            linknode: LazyShared::new_empty(),
        })
    }

    pub(crate) async fn new_with_unode_entry(
        changeset: ChangesetContext,
        path: impl Into<MononokePath>,
        unode_entry: Entry<ManifestUnodeId, FileUnodeId>,
    ) -> Result<Self, MononokeError> {
        let path = path.into();
        changeset
            .repo()
            .authorization_context()
            .require_path_read(
                changeset.ctx(),
                changeset.repo().inner_repo(),
                changeset.id(),
                path.as_mpath(),
            )
            .await?;
        Ok(Self {
            changeset,
            path,
            unode_id: LazyShared::new_ready(Ok(Some(unode_entry))),
            linknode: LazyShared::new_empty(),
        })
    }

    pub(crate) async fn new_with_deleted_manifest<Manifest: DeletedManifestCommon>(
        changeset: ChangesetContext,
        path: MononokePath,
        deleted_manifest_id: Manifest::Id,
    ) -> Result<Self, MononokeError> {
        changeset
            .repo()
            .authorization_context()
            .require_path_read(
                changeset.ctx(),
                changeset.repo().inner_repo(),
                changeset.id(),
                path.as_mpath(),
            )
            .await?;
        let ctx = changeset.ctx().clone();
        let blobstore = changeset.repo().blob_repo().repo_blobstore().clone();
        Ok(Self {
            changeset,
            path,
            unode_id: LazyShared::new_empty(),
            linknode: LazyShared::new_future(async move {
                let deleted_manifest = deleted_manifest_id.load(&ctx, &blobstore).await?;
                Ok(deleted_manifest.linknode().cloned())
            }),
        })
    }

    /// The `RepoContext` for this query.
    pub fn repo(&self) -> &RepoContext {
        self.changeset.repo()
    }

    /// The `ChangesetContext` for this query.
    pub fn changeset(&self) -> &ChangesetContext {
        &self.changeset
    }

    /// The path for this query.
    pub fn path(&self) -> &MononokePath {
        &self.path
    }

    // pub(crate) for testing
    pub(crate) async fn unode_id(
        &self,
    ) -> Result<Option<Entry<ManifestUnodeId, FileUnodeId>>, MononokeError> {
        self.unode_id
            .get_or_init(|| {
                cloned!(self.changeset, self.path);
                async move {
                    let ctx = changeset.ctx().clone();
                    let blobstore = changeset.repo().blob_repo().repo_blobstore().clone();
                    let root_unode_manifest_id = changeset.root_unode_manifest_id().await?;
                    if let Some(mpath) = path.into() {
                        root_unode_manifest_id
                            .manifest_unode_id()
                            .find_entry(ctx, blobstore, Some(mpath))
                            .await
                            .map_err(MononokeError::from)
                    } else {
                        Ok(Some(Entry::Tree(
                            root_unode_manifest_id.manifest_unode_id().clone(),
                        )))
                    }
                }
            })
            .await
    }

    async fn linknode_from_id(
        ctx: &CoreContext,
        blobstore: &impl Blobstore,
        root: impl RootDeletedManifestIdCommon + 'static,
        path: MononokePath,
    ) -> Result<Option<ChangesetId>, MononokeError> {
        let maybe_id = if let Some(mpath) = path.into() {
            root.find_entry(ctx, blobstore, Some(mpath))
                .await
                .map_err(MononokeError::from)?
        } else {
            Some(root.id().clone())
        };
        if let Some(id) = maybe_id {
            let deleted_manifest = id.load(ctx, blobstore).await?;
            Ok(deleted_manifest.linknode().cloned())
        } else {
            Ok(None)
        }
    }

    async fn linknode(&self) -> Result<Option<ChangesetId>, MononokeError> {
        self.linknode
            .get_or_init(|| {
                cloned!(self.changeset, self.path);
                async move {
                    let ctx = changeset.ctx();
                    let blobstore = changeset.repo().blob_repo().repo_blobstore();
                    Self::linknode_from_id(
                        ctx,
                        blobstore,
                        changeset.root_deleted_manifest_v2_id().await?,
                        path,
                    )
                    .await
                }
            })
            .await
    }

    /// Returns the last commit that modified this path.  If there is nothing
    /// at this path, returns `None`.
    pub async fn last_modified(&self) -> Result<Option<ChangesetContext>, MononokeError> {
        match self.unode_id().await? {
            Some(Entry::Tree(manifest_unode_id)) => {
                let ctx = self.changeset.ctx();
                let repo = self.changeset.repo().blob_repo();
                let manifest_unode = manifest_unode_id.load(ctx, repo.repo_blobstore()).await?;
                let cs_id = manifest_unode.linknode().clone();
                Ok(Some(ChangesetContext::new(self.repo().clone(), cs_id)))
            }
            Some(Entry::Leaf(file_unode_id)) => {
                let ctx = self.changeset.ctx();
                let repo = self.changeset.repo().blob_repo();
                let file_unode = file_unode_id.load(ctx, repo.repo_blobstore()).await?;
                let cs_id = file_unode.linknode().clone();
                Ok(Some(ChangesetContext::new(self.repo().clone(), cs_id)))
            }
            None => Ok(None),
        }
    }

    /// Returns the last commit that deleted this path.  If something exists
    /// at this path, or nothing ever existed at this path, returns `None`.
    pub async fn last_deleted(&self) -> Result<Option<ChangesetContext>, MononokeError> {
        Ok(self
            .linknode()
            .await?
            .map(|cs_id| ChangesetContext::new(self.repo().clone(), cs_id)))
    }

    /// Blame metadata for this path.
    pub async fn blame(&self, follow_mutable_file_history: bool) -> Result<BlameV2, MononokeError> {
        let ctx = self.changeset.ctx();
        let repo = self.changeset.repo().inner_repo();
        let csid = self.changeset.id();
        let path = self.path.as_mpath();
        let (blame, _) =
            history_traversal::blame(ctx, repo, csid, path, follow_mutable_file_history).await?;
        Ok(blame)
    }

    /// Blame metadata for this path, and the content that was blamed.
    pub async fn blame_with_content(
        &self,
        follow_mutable_file_history: bool,
    ) -> Result<(BlameV2, Bytes), MononokeError> {
        let ctx = self.changeset.ctx();
        let repo = self.changeset.repo().inner_repo();
        let csid = self.changeset.id();
        let path = self.path.as_mpath();
        Ok(history_traversal::blame_with_content(
            ctx,
            repo,
            csid,
            path,
            follow_mutable_file_history,
        )
        .await?)
    }

    /// Returns a list of `ChangesetContext` for the file at this path that represents
    /// a history of the path.
    pub async fn history(
        &self,
        opts: ChangesetPathHistoryOptions,
    ) -> Result<BoxStream<'_, Result<ChangesetContext, MononokeError>>, MononokeError> {
        let repo = self.repo().repo().clone();
        let mpath = self.path.as_mpath();

        if let Some(descendants_of) = opts.descendants_of {
            if !repo
                .commit_graph()
                .is_ancestor(
                    self.changeset().ctx(),
                    descendants_of,
                    self.changeset().id(),
                )
                .await?
            {
                return Ok(stream::empty().boxed());
            }
        }

        struct FilterVisitor {
            cs_info_enabled: bool,
            until_timestamp: Option<i64>,
            descendants_of: Option<ChangesetId>,
            exclude_changeset_and_ancestors: Option<ChangesetId>,
            cache: HashMap<(Option<CsAndPath>, Vec<CsAndPath>), Vec<CsAndPath>>,
        }
        impl FilterVisitor {
            async fn _visit(
                &self,
                ctx: &CoreContext,
                repo: &impl history_traversal::Repo,
                _descendant_cs_id: Option<CsAndPath>,
                mut cs_ids: Vec<CsAndPath>,
            ) -> Result<Vec<CsAndPath>, Error> {
                let cs_info_enabled = self.cs_info_enabled;
                if let Some(until_ts) = self.until_timestamp {
                    cs_ids = try_join_all(cs_ids.into_iter().map(|(cs_id, path)| async move {
                        let info = if cs_info_enabled {
                            ChangesetInfo::derive(ctx, repo, cs_id).await
                        } else {
                            let bonsai = cs_id.load(ctx, repo.repo_blobstore()).await?;
                            Ok(ChangesetInfo::new(cs_id, bonsai))
                        }?;
                        let timestamp = info.author_date().as_chrono().timestamp();
                        Ok::<_, Error>((timestamp >= until_ts).then_some((cs_id, path)))
                    }))
                    .await?
                    .into_iter()
                    .filter_map(std::convert::identity)
                    .collect();
                }

                if let Some(descendants_of) = self.descendants_of {
                    cs_ids = try_join_all(cs_ids.into_iter().map(|(cs_id, path)| async move {
                        if repo
                            .commit_graph()
                            .is_ancestor(ctx, descendants_of, cs_id)
                            .await?
                        {
                            anyhow::Ok(Some((cs_id, path)))
                        } else {
                            anyhow::Ok(None)
                        }
                    }))
                    .await?
                    .into_iter()
                    .filter_map(std::convert::identity)
                    .collect();
                }

                if let Some(exclude_changeset_and_ancestors) = self.exclude_changeset_and_ancestors
                {
                    cs_ids = try_join_all(cs_ids.into_iter().map(|(cs_id, path)| async move {
                        if repo
                            .commit_graph()
                            .is_ancestor(ctx, cs_id, exclude_changeset_and_ancestors)
                            .await?
                        {
                            Ok::<_, MononokeError>(None)
                        } else {
                            Ok::<_, MononokeError>(Some((cs_id, path)))
                        }
                    }))
                    .await?
                    .into_iter()
                    .filter_map(std::convert::identity)
                    .collect();
                }
                Ok(cs_ids)
            }
        }
        #[async_trait]
        impl Visitor for FilterVisitor {
            async fn visit(
                &mut self,
                ctx: &CoreContext,
                repo: &impl history_traversal::Repo,
                descendant_cs_id: Option<CsAndPath>,
                cs_ids: Vec<CsAndPath>,
            ) -> Result<Vec<CsAndPath>, Error> {
                if let Some(res) = self
                    .cache
                    .remove(&(descendant_cs_id.clone(), cs_ids.clone()))
                {
                    Ok(res)
                } else {
                    Ok(self._visit(ctx, repo, descendant_cs_id, cs_ids).await?)
                }
            }

            async fn preprocess(
                &mut self,
                ctx: &CoreContext,
                repo: &impl history_traversal::Repo,
                descendant_id_cs_ids: Vec<(Option<CsAndPath>, Vec<CsAndPath>)>,
            ) -> Result<(), Error> {
                try_join_all(
                    descendant_id_cs_ids
                        .into_iter()
                        .map(|(descendant_cs_id, cs_ids)| {
                            self._visit(ctx, repo, descendant_cs_id.clone(), cs_ids.clone())
                                .map_ok(move |res| ((descendant_cs_id, cs_ids), res))
                        }),
                )
                .await?
                .into_iter()
                .for_each(|(k, v)| {
                    self.cache.insert(k, v);
                });
                Ok(())
            }
        }
        let cs_info_enabled = self.repo().derive_changeset_info_enabled();

        let history_across_deletions = if opts.follow_history_across_deletions {
            HistoryAcrossDeletions::Track
        } else {
            HistoryAcrossDeletions::DontTrack
        };

        let history = list_file_history(
            self.changeset.ctx(),
            self.repo().inner_repo(),
            mpath.cloned(),
            self.changeset.id(),
            FilterVisitor {
                cs_info_enabled,
                until_timestamp: opts.until_timestamp,
                descendants_of: opts.descendants_of,
                exclude_changeset_and_ancestors: opts.exclude_changeset_and_ancestors,
                cache: HashMap::new(),
            },
            history_across_deletions,
            if opts.follow_mutable_file_history {
                FollowMutableFileHistory::MutableFileParents
            } else {
                FollowMutableFileHistory::ImmutableCommitParents
            },
            self.repo().mutable_renames().clone(),
            TraversalOrder::new_gen_num_order(
                self.changeset.ctx().clone(),
                repo.changeset_fetcher_arc(),
            ),
        )
        .await
        .map_err(|error| match error {
            FastlogError::InternalError(e) => MononokeError::from(anyhow!(e)),
            FastlogError::DeriveError(e) => MononokeError::from(e),
            FastlogError::LoadableError(e) => MononokeError::from(e),
            FastlogError::Error(e) => MononokeError::from(e),
        })?;

        Ok(history
            .map_err(MononokeError::from)
            .map_ok(move |changeset_id| ChangesetContext::new(self.repo().clone(), changeset_id))
            .boxed())
    }
}

impl ChangesetPathContext {
    pub(crate) async fn new(
        changeset: ChangesetContext,
        path: impl Into<MononokePath>,
    ) -> Result<Self, MononokeError> {
        let path = path.into();
        changeset
            .repo()
            .authorization_context()
            .require_path_read(
                changeset.ctx(),
                changeset.repo().inner_repo(),
                changeset.id(),
                path.as_mpath(),
            )
            .await?;
        Ok(Self {
            changeset,
            path,
            skeleton_manifest_id: LazyShared::new_empty(),
        })
    }

    pub(crate) async fn new_with_skeleton_manifest_entry(
        changeset: ChangesetContext,
        path: impl Into<MononokePath>,
        skeleton_manifest_entry: Entry<SkeletonManifestId, ()>,
    ) -> Result<Self, MononokeError> {
        let path = path.into();
        changeset
            .repo()
            .authorization_context()
            .require_path_read(
                changeset.ctx(),
                changeset.repo().inner_repo(),
                changeset.id(),
                path.as_mpath(),
            )
            .await?;
        Ok(Self {
            changeset,
            path,
            skeleton_manifest_id: LazyShared::new_ready(Ok(Some(skeleton_manifest_entry))),
        })
    }

    /// The `RepoContext` for this query.
    pub fn repo(&self) -> &RepoContext {
        self.changeset.repo()
    }

    /// The `ChangesetContext` for this query.
    pub fn changeset(&self) -> &ChangesetContext {
        &self.changeset
    }

    /// The path for this query.
    pub fn path(&self) -> &MononokePath {
        &self.path
    }

    async fn skeleton_manifest_id(
        &self,
    ) -> Result<Option<Entry<SkeletonManifestId, ()>>, MononokeError> {
        self.skeleton_manifest_id
            .get_or_init(|| {
                cloned!(self.changeset, self.path);
                async move {
                    let ctx = changeset.ctx().clone();
                    let blobstore = changeset.repo().blob_repo().repo_blobstore().clone();
                    let root_skeleton_manifest_id = changeset.root_skeleton_manifest_id().await?;
                    if let Some(mpath) = path.into() {
                        root_skeleton_manifest_id
                            .skeleton_manifest_id()
                            .find_entry(ctx, blobstore, Some(mpath))
                            .await
                            .map_err(MononokeError::from)
                    } else {
                        Ok(Some(Entry::Tree(
                            root_skeleton_manifest_id.skeleton_manifest_id().clone(),
                        )))
                    }
                }
            })
            .await
    }

    /// Returns `true` if the path exists (as a file or directory) in this commit.
    pub async fn exists(&self) -> Result<bool, MononokeError> {
        // The path exists if there is any kind of skeleton manifest entry.
        Ok(self.skeleton_manifest_id().await?.is_some())
    }

    pub async fn is_file(&self) -> Result<bool, MononokeError> {
        let is_file = match self.skeleton_manifest_id().await? {
            Some(Entry::Leaf(_)) => true,
            _ => false,
        };
        Ok(is_file)
    }

    pub async fn is_tree(&self) -> Result<bool, MononokeError> {
        let is_tree = match self.skeleton_manifest_id().await? {
            Some(Entry::Tree(_)) => true,
            _ => false,
        };
        Ok(is_tree)
    }
}