/*
 * 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.
 */

#![feature(trait_alias)]

use std::collections::BTreeMap;
use std::collections::HashMap;
use std::str::FromStr;

use anyhow::format_err;
use anyhow::Error;
use bonsai_hg_mapping::BonsaiHgMapping;
use bonsai_hg_mapping::BonsaiHgMappingRef;
use bookmarks::BookmarkKey;
use bookmarks::BookmarkUpdateReason;
use bookmarks::Bookmarks;
use bookmarks::BookmarksRef;
use bytes::Bytes;
use bytes::BytesMut;
use changeset_fetcher::ChangesetFetcher;
use changeset_fetcher::ChangesetFetcherArc;
use changesets::Changesets;
use changesets::ChangesetsRef;
use changesets_creation::save_changesets;
use context::CoreContext;
use filestore::FetchKey;
use filestore::FilestoreConfig;
use filestore::FilestoreConfigRef;
use filestore::StoreRequest;
use fsnodes::RootFsnodeId;
use futures::future;
use futures::stream;
use futures::stream::TryStreamExt;
use manifest::ManifestOps;
use maplit::btreemap;
use mercurial_types::HgChangesetId;
use mononoke_types::BonsaiChangesetMut;
use mononoke_types::ChangesetId;
use mononoke_types::DateTime;
use mononoke_types::FileChange;
use mononoke_types::FileType;
use mononoke_types::MPath;
use repo_blobstore::RepoBlobstore;
use repo_blobstore::RepoBlobstoreArc;
use repo_derived_data::RepoDerivedData;
use repo_derived_data::RepoDerivedDataRef;

pub mod drawdag;
pub mod random;

pub trait Repo = BonsaiHgMappingRef
    + BookmarksRef
    + ChangesetsRef
    + ChangesetFetcherArc
    + FilestoreConfigRef
    + RepoBlobstoreArc
    + RepoDerivedDataRef
    + Send
    + Sync;

#[facet::container]
#[derive(Clone)]
/// This BasicTestRepo provides enough functionality for the methods in tests_utils to
/// be used. Please don't add new facets to this type, instead create your own test repo type
/// for your tests. You can list out all of the facets individually, or embed BasicTestRepo in your
/// new struct and use delegate to make its facets accessible.
pub struct BasicTestRepo {
    #[facet]
    pub repo_blobstore: RepoBlobstore,

    #[facet]
    pub changesets: dyn Changesets,

    #[facet]
    pub changeset_fetcher: dyn ChangesetFetcher,

    #[facet]
    pub bonsai_hg_mapping: dyn BonsaiHgMapping,

    #[facet]
    pub bookmarks: dyn Bookmarks,

    #[facet]
    pub filestore_config: FilestoreConfig,

    #[facet]
    pub repo_derived_data: RepoDerivedData,
}

pub async fn list_working_copy_utf8(
    ctx: &CoreContext,
    repo: &impl Repo,
    cs_id: ChangesetId,
) -> Result<HashMap<MPath, String>, Error> {
    let wc = list_working_copy(ctx, repo, cs_id).await?;

    wc.into_iter()
        .map(|(path, content)| Ok((path, String::from_utf8(content.to_vec())?)))
        .collect()
}

pub async fn list_working_copy_utf8_with_types(
    ctx: &CoreContext,
    repo: &impl Repo,
    cs_id: ChangesetId,
) -> Result<HashMap<MPath, (String, FileType)>, Error> {
    let wc = list_working_copy_with_types(ctx, repo, cs_id).await?;

    wc.into_iter()
        .map(|(path, (content, ty))| Ok((path, (String::from_utf8(content.to_vec())?, ty))))
        .collect()
}

pub async fn list_working_copy(
    ctx: &CoreContext,
    repo: &impl Repo,
    cs_id: ChangesetId,
) -> Result<HashMap<MPath, Bytes>, Error> {
    let wc = list_working_copy_with_types(ctx, repo, cs_id).await?;

    Ok(wc
        .into_iter()
        .map(|(path, (bytes, _ty))| (path, bytes))
        .collect())
}

pub async fn list_working_copy_with_types(
    ctx: &CoreContext,
    repo: &impl Repo,
    cs_id: ChangesetId,
) -> Result<HashMap<MPath, (Bytes, FileType)>, Error> {
    let root_fsnode_id = repo
        .repo_derived_data()
        .derive::<RootFsnodeId>(ctx, cs_id)
        .await?;

    root_fsnode_id
        .fsnode_id()
        .list_leaf_entries(ctx.clone(), repo.repo_blobstore_arc())
        .map_ok(|(path, file)| async move {
            let content_id = *file.content_id();
            let file_type = *file.file_type();
            let maybe_content = filestore::fetch(
                repo.repo_blobstore(),
                ctx.clone(),
                &FetchKey::Canonical(content_id),
            )
            .await?;
            let s = match maybe_content {
                Some(s) => s,
                None => {
                    return Err(format_err!(
                        "cannot fetch content for {} {}",
                        path,
                        content_id
                    ));
                }
            };
            let bytes = s
                .try_fold(BytesMut::new(), |mut bytes, new_bytes| {
                    bytes.extend_from_slice(&new_bytes);
                    future::ready(Ok(bytes))
                })
                .await?;
            Ok((path, (bytes.freeze(), file_type)))
        })
        .try_buffer_unordered(100)
        .try_collect()
        .await
}

/// Helper to create bonsai changesets in a repo
pub struct CreateCommitContext<'a, R: Repo> {
    ctx: &'a CoreContext,
    repo: &'a R,
    parents: Vec<CommitIdentifier>,
    files: BTreeMap<MPath, CreateFileContext>,
    message: Option<String>,
    author: Option<String>,
    author_date: Option<DateTime>,
    committer: Option<String>,
    committer_date: Option<DateTime>,
    extra: BTreeMap<String, Vec<u8>>,
}

impl<'a, R: Repo> CreateCommitContext<'a, R> {
    pub fn new(
        ctx: &'a CoreContext,
        repo: &'a R,
        parents: Vec<impl Into<CommitIdentifier>>,
    ) -> Self {
        let parents: Vec<_> = parents.into_iter().map(|x| x.into()).collect();
        Self {
            ctx,
            repo,
            parents,
            files: BTreeMap::new(),
            message: None,
            author: None,
            author_date: None,
            committer: None,
            committer_date: None,
            extra: btreemap! {},
        }
    }

    /// Creates commit with no parents (this is created to avoid specifying generic parameters
    /// in CreateCommitContext::new() when `parents` parameter is an empty vector)
    pub fn new_root(ctx: &'a CoreContext, repo: &'a R) -> Self {
        Self {
            ctx,
            repo,
            parents: vec![],
            files: BTreeMap::new(),
            message: None,
            author: None,
            author_date: None,
            committer: None,
            committer_date: None,
            extra: btreemap! {},
        }
    }

    pub fn add_parent(mut self, id: impl Into<CommitIdentifier>) -> Self {
        self.parents.push(id.into());
        self
    }

    pub fn add_extra(mut self, key: impl Into<String>, value: impl Into<Vec<u8>>) -> Self {
        self.extra.insert(key.into(), value.into());
        self
    }

    pub fn add_file(mut self, path: impl TryInto<MPath>, content: impl Into<Vec<u8>>) -> Self {
        self.files.insert(
            path.try_into().ok().expect("Invalid path"),
            CreateFileContext::FromHelper(content.into(), FileType::Regular, None),
        );
        self
    }

    pub fn add_files<P: TryInto<MPath>, C: Into<Vec<u8>>, I: IntoIterator<Item = (P, C)>>(
        mut self,
        path_contents: I,
    ) -> Self {
        for (path, content) in path_contents {
            self = self.add_file(path, content);
        }
        self
    }

    pub fn delete_file(mut self, path: impl TryInto<MPath>) -> Self {
        self.files.insert(
            path.try_into().ok().expect("Invalid path"),
            CreateFileContext::Deleted,
        );
        self
    }

    pub fn forget_file(mut self, path: impl TryInto<MPath>) -> Self {
        let path = path.try_into().ok().expect("Invalid path");
        self.files.remove(&path);
        self
    }

    pub fn add_file_with_type(
        mut self,
        path: impl TryInto<MPath>,
        content: impl Into<Vec<u8>>,
        t: FileType,
    ) -> Self {
        self.files.insert(
            path.try_into().ok().expect("Invalid path"),
            CreateFileContext::FromHelper(content.into(), t, None),
        );
        self
    }

    pub fn add_file_with_copy_info(
        mut self,
        path: impl TryInto<MPath>,
        content: impl Into<Vec<u8>>,
        (parent, parent_path): (impl Into<CommitIdentifier>, impl TryInto<MPath>),
    ) -> Self {
        let copy_info = (
            parent_path.try_into().ok().expect("Invalid path"),
            parent.into(),
        );
        self.files.insert(
            path.try_into().ok().expect("Invalid path"),
            CreateFileContext::FromHelper(content.into(), FileType::Regular, Some(copy_info)),
        );
        self
    }

    pub fn add_file_change(mut self, path: impl TryInto<MPath>, file_change: FileChange) -> Self {
        self.files.insert(
            path.try_into().ok().expect("Invalid path"),
            CreateFileContext::FromFileChange(file_change),
        );
        self
    }

    pub fn set_message(mut self, message: impl Into<String>) -> Self {
        self.message = Some(message.into());
        self
    }

    pub fn set_author(mut self, author: impl Into<String>) -> Self {
        self.author = Some(author.into());
        self
    }

    pub fn set_author_date(mut self, author_date: DateTime) -> Self {
        self.author_date = Some(author_date);
        self
    }

    pub fn set_committer(mut self, committer: impl Into<String>) -> Self {
        self.committer = Some(committer.into());
        self
    }

    pub fn set_committer_date(mut self, committer_date: DateTime) -> Self {
        self.committer_date = Some(committer_date);
        self
    }

    pub async fn create_commit_object(self) -> Result<BonsaiChangesetMut, Error> {
        let parents = future::try_join_all(self.parents.into_iter().map({
            let ctx = self.ctx;
            let repo = self.repo;
            move |p| resolve_cs_id(ctx, repo, p)
        }))
        .await?;

        let files = future::try_join_all(self.files.into_iter().map({
            let ctx = self.ctx;
            let repo = self.repo;
            let parents = &parents;
            move |(path, create_file_context)| async move {
                let file_change = create_file_context
                    .into_file_change(ctx, repo, parents)
                    .await?;

                Result::<_, Error>::Ok((path, file_change))
            }
        }))
        .await?;

        let author_date = match self.author_date {
            Some(author_date) => author_date,
            None => DateTime::from_timestamp(0, 0)?,
        };

        let mut bcs = BonsaiChangesetMut {
            parents,
            author: self.author.unwrap_or_else(|| String::from("author")),
            author_date,
            committer: self.committer,
            committer_date: self.committer_date,
            message: self.message.unwrap_or_else(|| String::from("message")),
            hg_extra: self.extra.into(),
            git_extra_headers: None,
            git_tree_hash: None,
            file_changes: Default::default(),
            is_snapshot: false,
            git_annotated_tag: None,
        };

        for (path, file_change) in files {
            bcs.file_changes.insert(path, file_change);
        }

        Ok(bcs)
    }

    pub async fn commit(self) -> Result<ChangesetId, Error> {
        let ctx = self.ctx;
        let repo = self.repo;
        let bcs = self.create_commit_object().await?;
        let bcs = bcs.freeze()?;
        let bcs_id = bcs.get_changeset_id();
        save_changesets(ctx, repo, vec![bcs]).await?;
        Ok(bcs_id)
    }
}

enum CreateFileContext {
    FromHelper(Vec<u8>, FileType, Option<(MPath, CommitIdentifier)>),
    FromFileChange(FileChange),
    Deleted,
}

impl CreateFileContext {
    async fn into_file_change(
        self,
        ctx: &CoreContext,
        repo: &impl Repo,
        parents: &[ChangesetId],
    ) -> Result<FileChange, Error> {
        let file_change = match self {
            Self::FromHelper(content, file_type, copy_info) => {
                let content = Bytes::copy_from_slice(content.as_ref());

                let meta = filestore::store(
                    repo.repo_blobstore(),
                    repo.filestore_config().clone(),
                    ctx,
                    &StoreRequest::new(content.len().try_into().unwrap()),
                    stream::once(async move { Ok(content) }),
                )
                .await?;

                let copy_info = match copy_info {
                    Some((path, cs_id)) => {
                        let cs_id = resolve_cs_id(ctx, repo, cs_id).await?;

                        if !parents.contains(&cs_id) {
                            return Err(format_err!(
                                "CopyInfo at {:?} references invalid parent: {:?}",
                                &path,
                                &cs_id
                            ));
                        }

                        Some((path, cs_id))
                    }
                    None => None,
                };

                FileChange::tracked(meta.content_id, file_type, meta.total_size, copy_info)
            }
            Self::FromFileChange(file_change) => file_change,
            Self::Deleted => FileChange::Deletion,
        };

        Ok(file_change)
    }
}

/// Returns helper that can be moved to move/delete/create a bookmark
pub fn bookmark<R: Repo + Clone>(
    ctx: &CoreContext,
    repo: &R,
    book_ident: impl Into<BookmarkIdentifier>,
) -> UpdateBookmarkContext<R> {
    UpdateBookmarkContext {
        ctx: ctx.clone(),
        repo: repo.clone(),
        book_ident: book_ident.into(),
    }
}

pub struct UpdateBookmarkContext<R: Repo> {
    ctx: CoreContext,
    repo: R,
    book_ident: BookmarkIdentifier,
}

impl<R: Repo> UpdateBookmarkContext<R> {
    pub async fn set_to(self, cs_ident: impl Into<CommitIdentifier>) -> Result<BookmarkKey, Error> {
        use BookmarkIdentifier::*;
        let bookmark = match self.book_ident {
            Bookmark(bookmark) => bookmark,
            String(s) => BookmarkKey::new(s)?,
        };

        let cs_id = resolve_cs_id(&self.ctx, &self.repo, cs_ident).await?;
        let mut book_txn = self.repo.bookmarks().create_transaction(self.ctx);
        book_txn.force_set(&bookmark, cs_id, BookmarkUpdateReason::TestMove)?;
        book_txn.commit().await?;
        Ok(bookmark)
    }

    pub async fn create_publishing(
        self,
        cs_ident: impl Into<CommitIdentifier>,
    ) -> Result<BookmarkKey, Error> {
        use BookmarkIdentifier::*;
        let bookmark = match self.book_ident {
            Bookmark(bookmark) => bookmark,
            String(s) => BookmarkKey::new(s)?,
        };

        let cs_id = resolve_cs_id(&self.ctx, &self.repo, cs_ident).await?;
        let mut book_txn = self.repo.bookmarks().create_transaction(self.ctx);
        book_txn.create_publishing(&bookmark, cs_id, BookmarkUpdateReason::TestMove)?;
        book_txn.commit().await?;
        Ok(bookmark)
    }

    pub async fn create_pull_default(
        self,
        cs_ident: impl Into<CommitIdentifier>,
    ) -> Result<BookmarkKey, Error> {
        use BookmarkIdentifier::*;
        let bookmark = match self.book_ident {
            Bookmark(bookmark) => bookmark,
            String(s) => BookmarkKey::new(s)?,
        };

        let cs_id = resolve_cs_id(&self.ctx, &self.repo, cs_ident).await?;
        let mut book_txn = self.repo.bookmarks().create_transaction(self.ctx);
        book_txn.create(&bookmark, cs_id, BookmarkUpdateReason::TestMove)?;
        book_txn.commit().await?;
        Ok(bookmark)
    }

    pub async fn create_scratch(
        self,
        cs_ident: impl Into<CommitIdentifier>,
    ) -> Result<BookmarkKey, Error> {
        use BookmarkIdentifier::*;
        let bookmark = match self.book_ident {
            Bookmark(bookmark) => bookmark,
            String(s) => BookmarkKey::new(s)?,
        };

        let cs_id = resolve_cs_id(&self.ctx, &self.repo, cs_ident).await?;
        let mut book_txn = self.repo.bookmarks().create_transaction(self.ctx);
        book_txn.create_scratch(&bookmark, cs_id)?;
        book_txn.commit().await?;
        Ok(bookmark)
    }

    pub async fn delete(self) -> Result<(), Error> {
        use BookmarkIdentifier::*;
        let bookmark = match self.book_ident {
            Bookmark(bookmark) => bookmark,
            String(s) => BookmarkKey::new(s)?,
        };

        let mut book_txn = self.repo.bookmarks().create_transaction(self.ctx);
        book_txn.force_delete(&bookmark, BookmarkUpdateReason::TestMove)?;
        book_txn.commit().await?;
        Ok(())
    }
}

pub enum CommitIdentifier {
    Bonsai(ChangesetId),
    Hg(HgChangesetId),
    String(String),
    Bookmark(BookmarkKey),
}

impl From<ChangesetId> for CommitIdentifier {
    fn from(bcs_id: ChangesetId) -> Self {
        Self::Bonsai(bcs_id)
    }
}

impl From<HgChangesetId> for CommitIdentifier {
    fn from(hg_cs_id: HgChangesetId) -> Self {
        Self::Hg(hg_cs_id)
    }
}

impl From<&str> for CommitIdentifier {
    fn from(s: &str) -> Self {
        Self::String(s.to_string())
    }
}

impl From<String> for CommitIdentifier {
    fn from(s: String) -> Self {
        Self::String(s)
    }
}

impl From<&BookmarkKey> for CommitIdentifier {
    fn from(bookmark: &BookmarkKey) -> Self {
        Self::Bookmark(bookmark.clone())
    }
}

impl From<BookmarkKey> for CommitIdentifier {
    fn from(bookmark: BookmarkKey) -> Self {
        Self::Bookmark(bookmark)
    }
}

pub enum BookmarkIdentifier {
    String(String),
    Bookmark(BookmarkKey),
}

impl From<&str> for BookmarkIdentifier {
    fn from(s: &str) -> Self {
        Self::String(s.to_string())
    }
}

impl From<String> for BookmarkIdentifier {
    fn from(s: String) -> Self {
        Self::String(s)
    }
}

impl From<&BookmarkKey> for BookmarkIdentifier {
    fn from(bookmark: &BookmarkKey) -> Self {
        Self::Bookmark(bookmark.clone())
    }
}

impl From<BookmarkKey> for BookmarkIdentifier {
    fn from(bookmark: BookmarkKey) -> Self {
        Self::Bookmark(bookmark)
    }
}

pub async fn store_files<T: AsRef<str>>(
    ctx: &CoreContext,
    files: BTreeMap<&str, Option<T>>,
    repo: &impl RepoBlobstoreArc,
) -> BTreeMap<MPath, FileChange> {
    let mut res = btreemap! {};

    for (path, content) in files {
        let path = MPath::new(path).unwrap();
        match content {
            Some(content) => {
                let content = content.as_ref();
                let size = content.len() as u64;
                let content_id = filestore::store(
                    &repo.repo_blobstore_arc(),
                    FilestoreConfig::no_chunking_filestore(),
                    ctx,
                    &StoreRequest::new(size),
                    stream::iter(vec![anyhow::Ok(Bytes::copy_from_slice(content.as_bytes()))]),
                )
                .await
                .unwrap()
                .content_id;

                let file_change = FileChange::tracked(content_id, FileType::Regular, size, None);
                res.insert(path, file_change);
            }
            None => {
                res.insert(path, FileChange::Deletion);
            }
        }
    }
    res
}

pub async fn store_rename(
    ctx: &CoreContext,
    copy_src: (MPath, ChangesetId),
    path: &str,
    content: &str,
    repo: &impl RepoBlobstoreArc,
) -> (MPath, FileChange) {
    let path = MPath::new(path).unwrap();
    let size = content.len() as u64;
    let content_id = filestore::store(
        &repo.repo_blobstore_arc(),
        FilestoreConfig::no_chunking_filestore(),
        ctx,
        &StoreRequest::new(size),
        stream::iter(vec![anyhow::Ok(Bytes::copy_from_slice(content.as_bytes()))]),
    )
    .await
    .unwrap()
    .content_id;

    let file_change = FileChange::tracked(content_id, FileType::Regular, size, Some(copy_src));
    (path, file_change)
}

pub async fn resolve_cs_id(
    ctx: &CoreContext,
    repo: &(impl BookmarksRef + BonsaiHgMappingRef),
    cs_ident: impl Into<CommitIdentifier>,
) -> Result<ChangesetId, Error> {
    use CommitIdentifier::*;
    match cs_ident.into() {
        Bonsai(cs_id) => Ok(cs_id),
        Hg(hg_cs_id) => {
            let maybe_cs_id = repo
                .bonsai_hg_mapping()
                .get_bonsai_from_hg(ctx, hg_cs_id)
                .await?;
            maybe_cs_id.ok_or_else(|| format_err!("{} not found", hg_cs_id))
        }
        Bookmark(bookmark) => {
            let maybe_cs_id = repo.bookmarks().get(ctx.clone(), &bookmark).await?;
            maybe_cs_id.ok_or_else(|| format_err!("{} not found", bookmark))
        }
        String(hash_or_bookmark) => {
            if let Ok(name) = BookmarkKey::new(hash_or_bookmark.clone()) {
                if let Ok(Some(csid)) = repo.bookmarks().get(ctx.clone(), &name).await {
                    return Ok(csid);
                }
            }

            if let Ok(hg_cs_id) = HgChangesetId::from_str(&hash_or_bookmark) {
                if let Ok(Some(cs_id)) = repo
                    .bonsai_hg_mapping()
                    .get_bonsai_from_hg(ctx, hg_cs_id)
                    .await
                {
                    return Ok(cs_id);
                }
            }

            if let Ok(cs_id) = ChangesetId::from_str(&hash_or_bookmark) {
                return Ok(cs_id);
            }
            Err(format_err!(
                "invalid (hash|bookmark) or does not exist in this repository: {}",
                hash_or_bookmark
            ))
        }
    }
}

pub async fn create_commit(
    ctx: CoreContext,
    repo: impl Repo,
    parents: Vec<ChangesetId>,
    file_changes: BTreeMap<MPath, FileChange>,
) -> ChangesetId {
    let bcs = BonsaiChangesetMut {
        parents,
        author: "author".to_string(),
        author_date: DateTime::from_timestamp(0, 0).unwrap(),
        committer: None,
        committer_date: None,
        message: "message".to_string(),
        hg_extra: Default::default(),
        git_extra_headers: None,
        git_tree_hash: None,
        file_changes: file_changes.into(),
        is_snapshot: false,
        git_annotated_tag: None,
    }
    .freeze()
    .unwrap();

    let bcs_id = bcs.get_changeset_id();
    save_changesets(&ctx, &repo, vec![bcs]).await.unwrap();
    bcs_id
}

pub async fn create_commit_with_date(
    ctx: CoreContext,
    repo: impl Repo,
    parents: Vec<ChangesetId>,
    file_changes: BTreeMap<MPath, FileChange>,
    author_date: DateTime,
) -> ChangesetId {
    let bcs = BonsaiChangesetMut {
        parents,
        author: "author".to_string(),
        author_date,
        committer: None,
        committer_date: None,
        message: "message".to_string(),
        hg_extra: Default::default(),
        git_extra_headers: None,
        git_tree_hash: None,
        file_changes: file_changes.into(),
        is_snapshot: false,
        git_annotated_tag: None,
    }
    .freeze()
    .unwrap();

    let bcs_id = bcs.get_changeset_id();
    save_changesets(&ctx, &repo, vec![bcs]).await.unwrap();
    bcs_id
}


/*
 * 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.
 */

//! Utilities to generate reasonably looking stack of changesets
use std::collections::BTreeMap;

use anyhow::Error;
use anyhow::Result;
use blobstore::Storable;
use changesets::ChangesetsRef;
use changesets_creation::save_changesets;
use context::CoreContext;
use futures::future;
use futures::stream;
use futures::TryStreamExt;
use mononoke_types::BlobstoreValue;
use mononoke_types::BonsaiChangesetMut;
use mononoke_types::ChangesetId;
use mononoke_types::DateTime;
use mononoke_types::FileChange;
use mononoke_types::FileContents;
use mononoke_types::FileType;
use mononoke_types::MPath;
use mononoke_types::MPathElement;
use rand::seq::SliceRandom;
use rand::Rng;
use rand_distr::Binomial;
use rand_distr::Uniform;
use repo_blobstore::RepoBlobstoreRef;

pub async fn create_random_stack(
    ctx: &CoreContext,
    repo: &(impl RepoBlobstoreRef + ChangesetsRef),
    rng: &mut impl Rng,
    parent: Option<ChangesetId>,
    changes_count: impl IntoIterator<Item = usize>,
) -> Result<(ChangesetId, Box<GenManifest>), Error> {
    let settings = GenSettings::default();
    let mut manifest = GenManifest::new();
    let changeset_id = manifest
        .gen_stack(ctx, repo, rng, &settings, parent, changes_count)
        .await?;
    Ok((changeset_id, manifest))
}

#[derive(Clone, Copy)]
pub struct GenSettings {
    /// probablity of descending one level deeper when generating change
    pub p_dir_descend: f64,
    /// probablity to create directory or reuse existing when descending manifest
    pub p_dir_create: f64,
    /// probability to create file or modify existing
    pub p_file_create: f64,
    /// probability to delete file instead of modifying
    pub p_file_delete: f64,
}

impl Default for GenSettings {
    fn default() -> Self {
        Self {
            p_dir_descend: 0.7,
            p_dir_create: 0.2,
            p_file_create: 0.3,
            p_file_delete: 0.1,
        }
    }
}

pub struct GenManifest {
    dirs: BTreeMap<MPathElement, Box<GenManifest>>,
    files: BTreeMap<MPathElement, String>,
}

#[derive(Default, Debug, Clone, Copy)]
pub struct Size {
    width: usize,
    depth: usize,
    files: usize,
}

impl GenManifest {
    fn new() -> Box<Self> {
        Box::new(Self {
            dirs: BTreeMap::new(),
            files: BTreeMap::new(),
        })
    }

    pub fn size(&self) -> Size {
        let children_size =
            self.dirs
                .values()
                .map(|child| child.size())
                .fold(Size::default(), |acc_size, size| Size {
                    files: acc_size.files + size.files,
                    depth: std::cmp::max(acc_size.depth, size.depth),
                    width: std::cmp::max(acc_size.width, size.width),
                });
        Size {
            depth: children_size.depth + 1,
            width: std::cmp::max(children_size.width, self.files.len() + self.dirs.len()),
            files: children_size.files + self.files.len(),
        }
    }

    async fn gen_stack(
        &mut self,
        ctx: &CoreContext,
        repo: &(impl RepoBlobstoreRef + ChangesetsRef),
        rng: &mut impl Rng,
        settings: &GenSettings,
        parent: Option<ChangesetId>,
        changes_count: impl IntoIterator<Item = usize>,
    ) -> Result<ChangesetId, Error> {
        let mut parents: Vec<_> = parent.into_iter().collect();
        let mut changesets = Vec::new();
        async move {
            let blobstore = repo.repo_blobstore();
            let store_changes = stream::FuturesUnordered::new();
            for changes_size in changes_count {
                // generate file changes
                let mut file_changes = BTreeMap::new();
                while file_changes.len() < changes_size {
                    let (path, content) = self.gen_change(rng, settings, Vec::new());
                    match content {
                        None => {
                            file_changes.insert(path, FileChange::Deletion);
                        }
                        Some(content) => {
                            let content = FileContents::new_bytes(content);
                            let size = content.size();
                            let blob = content.into_blob();
                            let id = *blob.id();
                            store_changes.push(blob.store(ctx, blobstore));
                            file_changes.insert(
                                path,
                                FileChange::tracked(id, FileType::Regular, size, None),
                            );
                        }
                    }
                }
                // generate changeset
                let bonsai = BonsaiChangesetMut {
                    parents: std::mem::take(&mut parents),
                    author: "author".to_string(),
                    author_date: DateTime::from_timestamp(0, 0).unwrap(),
                    committer: None,
                    committer_date: None,
                    message: "message".to_string(),
                    hg_extra: Default::default(),
                    git_extra_headers: None,
                    git_tree_hash: None,
                    file_changes: file_changes.into(),
                    is_snapshot: false,
                    git_annotated_tag: None,
                }
                .freeze()
                .expect("generated bonsai failed to freeze");
                parents.push(bonsai.get_changeset_id());
                changesets.push(bonsai);
            }

            match parents.into_iter().next() {
                None => Err(Error::msg("empty changes iterator")),
                Some(csid) => {
                    store_changes.try_for_each(|_| future::ok(())).await?;
                    save_changesets(ctx, repo, changesets).await?;
                    Ok(csid)
                }
            }
        }
        .await
    }

    fn gen_change(
        &mut self,
        rng: &mut impl Rng,
        settings: &GenSettings,
        mut prefix: Vec<MPathElement>,
    ) -> (MPath, Option<String>) {
        if rng.gen_bool(settings.p_dir_descend) {
            let dirname = if rng.gen_bool(settings.p_dir_create) {
                gen_filename(rng)
            } else {
                let dirs = self.dirs.keys().collect::<Vec<_>>();
                dirs.choose(rng)
                    .map_or_else(|| gen_filename(rng), |&d| d.clone())
            };
            prefix.push(dirname.clone());
            self.dirs
                .entry(dirname)
                .or_insert_with(Self::new)
                .gen_change(rng, settings, prefix)
        } else {
            let (filename, new) = if rng.gen_bool(settings.p_file_create) {
                (gen_filename(rng), true)
            } else {
                let files = self.files.keys().collect::<Vec<_>>();
                files
                    .choose(rng)
                    .map_or_else(|| (gen_filename(rng), true), |&k| (k.clone(), false))
            };
            prefix.push(filename.clone());
            let data = if !new && rng.gen_bool(settings.p_file_delete) {
                self.files.remove(&filename);
                None
            } else {
                let data = gen_ascii(16, rng);
                self.files.insert(filename, data.clone());
                Some(data)
            };
            (MPath::try_from(prefix).expect("prefix is empty"), data)
        }
    }
}

fn gen_ascii(len: usize, rng: &mut impl Rng) -> String {
    let chars = b"_abcdefghijklmnopqrstuvwxyz";
    let bytes = rng
        .sample_iter(&Uniform::from(0..chars.len()))
        .take(len)
        .map(|i| chars[i])
        .collect();
    String::from_utf8(bytes).expect("ascii conversion failed")
}

fn gen_filename(rng: &mut impl Rng) -> MPathElement {
    let len = rng.sample(Binomial::new(20, 0.3).expect("Binomial::new failed")) as usize;
    MPathElement::new(gen_ascii(len + 3, rng).into()).expect("failed to create mpath element")
}


/*
 * 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.
 */

#[cfg(fbcode_build)]
mod facebook;
#[cfg(not(fbcode_build))]
mod oss;

use std::sync::Arc;

use anyhow::Result;
use async_trait::async_trait;

pub type ArcGlobalTimeWindowCounter = Arc<dyn GlobalTimeWindowCounter + Send + Sync + 'static>;
pub type BoxGlobalTimeWindowCounter = Box<dyn GlobalTimeWindowCounter + Send + Sync + 'static>;

#[async_trait]
pub trait GlobalTimeWindowCounter {
    async fn get(&self, time_window: u32) -> Result<f64>;

    fn bump(&self, value: f64);
}

pub struct GlobalTimeWindowCounterBuilder;


/*
 * 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 anyhow::Result;
use async_trait::async_trait;
use fbinit::FacebookInit;

use crate::BoxGlobalTimeWindowCounter;
use crate::GlobalTimeWindowCounter;
use crate::GlobalTimeWindowCounterBuilder;

struct AlwaysZeroCounter {}

#[async_trait]
impl GlobalTimeWindowCounter for AlwaysZeroCounter {
    async fn get(&self, _time_window: u32) -> Result<f64> {
        Ok(0.0)
    }

    fn bump(&self, _value: f64) {}
}

impl GlobalTimeWindowCounterBuilder {
    pub fn build(
        _fb: FacebookInit,
        _category: impl AsRef<str>,
        _key: impl AsRef<str>,
        _min_time_window: u32,
        _max_time_window: u32,
    ) -> BoxGlobalTimeWindowCounter {
        Box::new(AlwaysZeroCounter {})
    }
}


/*
 * 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::fmt;

use anyhow::Error;
use anyhow::Result;
use bonsai_git_mapping::BonsaiGitMappingRef;
use bonsai_globalrev_mapping::BonsaiGlobalrevMappingRef;
use bonsai_hg_mapping::BonsaiHgMappingRef;
use bonsai_svnrev_mapping::BonsaiSvnrevMappingRef;
use bookmarks::BookmarkKey;
use bookmarks::BookmarkUpdateReason;
use context::CoreContext;
use futures::stream;
use futures::stream::StreamExt;
use futures::stream::TryStreamExt;
use mononoke_types::ChangesetId;
use mononoke_types::DateTime;
use mononoke_types::Timestamp;

use crate::commit_id::IdentityScheme;

pub trait Repo =
    BonsaiHgMappingRef + BonsaiGitMappingRef + BonsaiGlobalrevMappingRef + BonsaiSvnrevMappingRef;

pub struct BookmarkLogEntry {
    timestamp: Timestamp,
    bookmark: BookmarkKey,
    reason: BookmarkUpdateReason,
    ids: Vec<(IdentityScheme, String)>,
    bundle_id: Option<u64>,
}

impl BookmarkLogEntry {
    pub async fn new(
        ctx: &CoreContext,
        repo: &impl Repo,
        timestamp: Timestamp,
        bookmark: BookmarkKey,
        reason: BookmarkUpdateReason,
        changeset_id: Option<ChangesetId>,
        bundle_id: Option<u64>,
        schemes: &[IdentityScheme],
    ) -> Result<Self> {
        let ids = if let Some(changeset_id) = changeset_id {
            stream::iter(schemes.iter().copied())
                .map(|scheme| {
                    Ok::<_, Error>(async move {
                        match scheme.map_commit_id(ctx, repo, changeset_id).await? {
                            Some(commit_id) => Ok(Some((scheme, commit_id))),
                            None => Ok(None),
                        }
                    })
                })
                .try_buffered(10)
                .try_filter_map(|commit_id| async move { Ok(commit_id) })
                .try_collect()
                .await?
        } else {
            Vec::new()
        };
        Ok(BookmarkLogEntry {
            timestamp,
            bookmark,
            reason,
            ids,
            bundle_id,
        })
    }
}

impl fmt::Display for BookmarkLogEntry {
    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
        if let Some(bundle_id) = self.bundle_id {
            write!(fmt, "{} ", bundle_id)?;
        }
        write!(fmt, "({})", self.bookmark)?;
        match self.ids.as_slice() {
            [] => {}
            [(_, id)] => write!(fmt, " {}", id)?,
            ids => {
                for (scheme, id) in ids {
                    write!(fmt, " {}={}", scheme, id)?;
                }
            }
        }
        write!(
            fmt,
            " {} {}",
            self.reason,
            DateTime::from(self.timestamp).as_chrono().to_rfc3339()
        )?;
        Ok(())
    }
}


/*
 * 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.
 */

mononoke_app::subcommands! {
    mod blobstore;
    mod blobstore_bulk_unlink;
    mod blobstore_unlink;
    mod bookmarks;
    mod changelog;
    mod commit;
    mod commit_graph;
    mod convert;
    mod fetch;
    mod filestore;
    mod hg_sync;
    mod mutable_renames;
    mod redaction;
    mod repo;
    mod repos;
    mod ephemeral_store;
    mod dump_changesets;
    mod async_requests;
    mod mutable_counters;
}


/*
 * 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 anyhow::anyhow;
use anyhow::Context;
use anyhow::Result;
use bonsai_git_mapping::BonsaiGitMappingRef;
use bonsai_globalrev_mapping::BonsaiGlobalrevMappingRef;
use bonsai_hg_mapping::BonsaiHgMappingRef;
use bonsai_svnrev_mapping::BonsaiSvnrevMappingRef;
use clap::ValueEnum;
use context::CoreContext;
use futures::future::join;
use mercurial_types::HgChangesetId;
use mononoke_types::hash::GitSha1;
use mononoke_types::ChangesetId;
use mononoke_types::Globalrev;
use mononoke_types::Svnrev;
use strum::Display;

pub trait Repo =
    BonsaiHgMappingRef + BonsaiGitMappingRef + BonsaiGlobalrevMappingRef + BonsaiSvnrevMappingRef;

#[derive(Copy, Clone, Eq, PartialEq, ValueEnum, Display)]
#[strum(serialize_all = "kebab_case")]
pub enum IdentityScheme {
    /// Mononoke bonsai hash
    Bonsai,

    /// Mercurial hash
    Hg,

    /// Git SHA-1 hash
    Git,

    /// Globalrev
    Globalrev,

    /// Subversion revision (legacy)
    Svnrev,
}

impl IdentityScheme {
    /// Parse a commit id of this scheme from a string.
    pub async fn parse_commit_id(
        &self,
        ctx: &CoreContext,
        repo: &impl Repo,
        commit_id: &str,
    ) -> Result<ChangesetId> {
        let cs_id = match self {
            IdentityScheme::Bonsai => commit_id
                .parse::<ChangesetId>()
                .context("Invalid bonsai changeset id")?,
            IdentityScheme::Hg => {
                let hg_cs_id = commit_id
                    .parse::<HgChangesetId>()
                    .context("Invalid hg changeset id")?;
                repo.bonsai_hg_mapping()
                    .get_bonsai_from_hg(ctx, hg_cs_id)
                    .await?
                    .ok_or_else(|| anyhow!("hg-bonsai mapping not found for {}", hg_cs_id))?
            }
            IdentityScheme::Git => {
                let git_id = commit_id
                    .parse::<GitSha1>()
                    .context("Invalid git changeset id")?;
                repo.bonsai_git_mapping()
                    .get_bonsai_from_git_sha1(ctx, git_id)
                    .await?
                    .ok_or_else(|| anyhow!("git-bonsai mapping not found for {}", git_id))?
            }
            IdentityScheme::Globalrev => {
                let globalrev = commit_id
                    .parse::<Globalrev>()
                    .context("Invalid globalrev")?;
                repo.bonsai_globalrev_mapping()
                    .get_bonsai_from_globalrev(ctx, globalrev)
                    .await?
                    .ok_or_else(|| {
                        anyhow!("globalrev-bonsai mapping not found for {}", globalrev)
                    })?
            }
            IdentityScheme::Svnrev => {
                let svnrev = commit_id.parse::<Svnrev>().context("Invalid svnrev")?;
                repo.bonsai_svnrev_mapping()
                    .get_bonsai_from_svnrev(ctx, svnrev)
                    .await?
                    .ok_or_else(|| anyhow!("svnrev-bonsai mapping not found for {}", svnrev))?
            }
        };
        Ok(cs_id)
    }

    /// Map a commit id into a string for this identity scheme.
    ///
    /// Returns `None` if this commit does not exist in that scheme.
    pub async fn map_commit_id(
        &self,
        ctx: &CoreContext,
        repo: &impl Repo,
        cs_id: ChangesetId,
    ) -> Result<Option<String>> {
        let commit_id = match self {
            IdentityScheme::Bonsai => Some(cs_id.to_string()),
            IdentityScheme::Hg => repo
                .bonsai_hg_mapping()
                .get_hg_from_bonsai(ctx, cs_id)
                .await?
                .as_ref()
                .map(ToString::to_string),
            IdentityScheme::Git => repo
                .bonsai_git_mapping()
                .get_git_sha1_from_bonsai(ctx, cs_id)
                .await?
                .as_ref()
                .map(ToString::to_string),
            IdentityScheme::Globalrev => repo
                .bonsai_globalrev_mapping()
                .get_globalrev_from_bonsai(ctx, cs_id)
                .await?
                .as_ref()
                .map(ToString::to_string),
            IdentityScheme::Svnrev => repo
                .bonsai_svnrev_mapping()
                .get_svnrev_from_bonsai(ctx, cs_id)
                .await?
                .as_ref()
                .map(ToString::to_string),
        };
        Ok(commit_id)
    }
}

/// Parse a general commit ID from a string
///
/// The string can either be of the form <scheme>=<id>, or just
/// a bare id, in which case the scheme will be inferred.
///
/// For inferred schemes, globalrevs should be prefixed by 'm', and svnrevs
/// should be prefixed by 's'.
///
/// Hash types are inferred from their length (64 characters for
/// 32-byte bonsai hashes, 40 characters for 20-byte Mercurial or
/// Git hashes).  For Mercurial and Git, whichever one exists is
/// selected.  In the unlikely event that the hash refers to both
/// a Mercurial commit and a Git commit, the Mercurial commit is
/// returned.
pub async fn parse_commit_id(
    ctx: &CoreContext,
    repo: &impl Repo,
    commit_id: &str,
) -> Result<ChangesetId> {
    if let Some((scheme, id)) = commit_id.split_once('=') {
        let scheme = IdentityScheme::from_str(scheme, /* ignore_case */ true).map_err(|e| {
            anyhow!(
                "Failed to parse commit identity scheme '{}': {}",
                scheme.to_string(),
                e
            )
        })?;
        scheme.parse_commit_id(ctx, repo, id).await
    } else if let Some(globalrev) = commit_id.strip_prefix('m') {
        IdentityScheme::Globalrev
            .parse_commit_id(ctx, repo, globalrev)
            .await
    } else if let Some(svnrev) = commit_id.strip_prefix('s') {
        IdentityScheme::Svnrev
            .parse_commit_id(ctx, repo, svnrev)
            .await
    } else if commit_id.len() == 64 {
        IdentityScheme::Bonsai
            .parse_commit_id(ctx, repo, commit_id)
            .await
    } else if commit_id.len() == 40 {
        match join(
            IdentityScheme::Hg.parse_commit_id(ctx, repo, commit_id),
            IdentityScheme::Git.parse_commit_id(ctx, repo, commit_id),
        )
        .await
        {
            (Ok(cs_id), _) => Ok(cs_id),
            (Err(_), Ok(cs_id)) => Ok(cs_id),
            (Err(e), Err(_)) => Err(e),
        }
    } else {
        Err(anyhow!("Invalid commit id: {}", commit_id))
    }
}

/// Print a commit id in the selected schemes.
///
/// If a single scheme is requested, just the commit id is printed, and
/// an error is returned if the commit does not exist in that scheme.
///
/// Otherwise, each commit id is prefixed by the name of the scheme, and
/// schemes for which the commit does not exist are omitted.
///
/// If no schemes are selected, prints the bonsai hash.
pub async fn print_commit_id(
    ctx: &CoreContext,
    repo: &impl Repo,
    schemes: &[IdentityScheme],
    cs_id: ChangesetId,
) -> Result<()> {
    match schemes {
        [] => {
            println!("{}", cs_id);
        }
        [scheme] => {
            let commit_id = scheme
                .map_commit_id(ctx, repo, cs_id)
                .await?
                .ok_or_else(|| {
                    anyhow!(
                        "bonsai-{} mapping not found for {}",
                        scheme.to_string(),
                        cs_id
                    )
                })?;
            println!("{}", commit_id);
        }
        schemes => {
            for scheme in schemes {
                if let Some(commit_id) = scheme.map_commit_id(ctx, repo, cs_id).await? {
                    println!("{}: {}", scheme, commit_id);
                }
            }
        }
    }
    Ok(())
}


/*
 * 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.
 */

#![feature(trait_alias)]

use anyhow::Result;
use clap::Parser;
use cmdlib_scrubbing::ScrubAppExtension;
use fbinit::FacebookInit;
use mononoke_app::fb303::AliveService;
use mononoke_app::fb303::Fb303AppExtension;
use mononoke_app::MononokeApp;
use mononoke_app::MononokeAppBuilder;

mod bookmark_log_entry;
mod commands;
mod commit_id;
#[cfg(fbcode_build)]
mod facebook;

/// Administrate Mononoke
#[derive(Parser)]
struct AdminArgs {
    #[clap(long)]
    use_monitoring: bool,
}

#[fbinit::main]
fn main(fb: FacebookInit) -> Result<()> {
    #[cfg(not(fbcode_build))]
    let subcommands = commands::subcommands();

    #[cfg(fbcode_build)]
    let subcommands = {
        let mut subcommands = commands::subcommands();
        subcommands.extend(facebook::commands::subcommands());
        subcommands
    };

    let app = MononokeAppBuilder::new(fb)
        .with_app_extension(ScrubAppExtension::new())
        .with_app_extension(Fb303AppExtension {})
        .build_with_subcommands::<AdminArgs>(subcommands)?;

    let args: AdminArgs = app.args()?;
    if args.use_monitoring {
        app.run_with_monitoring_and_logging(async_main, "admin", AliveService)
    } else {
        app.run_basic(async_main)
    }
}

async fn async_main(app: MononokeApp) -> Result<()> {
    #[cfg(not(fbcode_build))]
    commands::dispatch(app).await?;

    #[cfg(fbcode_build)]
    {
        if commands::subcommand_is_in_scope(&app) {
            commands::dispatch(app).await?;
        } else if facebook::commands::subcommand_is_in_scope(&app) {
            facebook::commands::dispatch(app).await?;
        }
    }

    Ok(())
}


/*
 * 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.
 */

mod abort;
mod list;
mod requeue;
mod show;

use std::sync::Arc;

use anyhow::Context;
use anyhow::Result;
use clap::Parser;
use clap::Subcommand;
use context::SessionContainer;
use megarepo_api::MegarepoApi;
use mononoke_app::args::RepoArgs;
use mononoke_app::MononokeApp;

use crate::commands::async_requests::abort::AsyncRequestsAbortArgs;
use crate::commands::async_requests::list::AsyncRequestsListArgs;
use crate::commands::async_requests::requeue::AsyncRequestsRequeueArgs;
use crate::commands::async_requests::show::AsyncRequestsShowArgs;

/// View and manage the SCS async requests (used by megarepo)
#[derive(Parser)]
pub struct CommandArgs {
    /// The repository name or ID
    #[clap(flatten)]
    repo: RepoArgs,
    /// The subcommand for async requests
    #[clap(subcommand)]
    subcommand: AsyncRequestsSubcommand,
}

#[derive(Subcommand)]
pub enum AsyncRequestsSubcommand {
    /// Lists asynchronous requests (by default the ones active
    /// now or updated within last 5 mins).
    List(AsyncRequestsListArgs),
    /// Shows request details.
    Show(AsyncRequestsShowArgs),
    /// Changes the request status to new so it's picked up
    /// by workers again.
    Requeue(AsyncRequestsRequeueArgs),
    /// Changes the request status to ready and put error as result.
    /// (this won't stop any currently running workers immediately)
    Abort(AsyncRequestsAbortArgs),
}

pub async fn run(app: MononokeApp, args: CommandArgs) -> Result<()> {
    let mononoke = Arc::new(
        app.open_managed_repo_arg(&args.repo)
            .await
            .context("Failed to initialize Mononoke API")?
            .make_mononoke_api()?,
    );
    let megarepo = MegarepoApi::new(&app, mononoke)
        .await
        .context("Failed to initialize MegarepoApi")?;
    let session = SessionContainer::new_with_defaults(app.environment().fb);
    let ctx = session.new_context(
        app.logger().clone(),
        app.environment().scuba_sample_builder.clone(),
    );

    match args.subcommand {
        AsyncRequestsSubcommand::List(list_args) => {
            list::list_requests(list_args, ctx, megarepo).await?
        }
        AsyncRequestsSubcommand::Show(show_args) => {
            show::show_request(show_args, ctx, megarepo).await?
        }
        AsyncRequestsSubcommand::Requeue(requeue_args) => {
            requeue::requeue_request(requeue_args, ctx, megarepo).await?
        }
        AsyncRequestsSubcommand::Abort(abort_args) => {
            abort::abort_request(abort_args, ctx, megarepo).await?
        }
    }
    Ok(())
}


/*
 * 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.
 */

mod fetch;
mod fetch_many;
mod upload;

use anyhow::Context;
use anyhow::Result;
use clap::Parser;
use clap::Subcommand;
use fetch::BlobstoreFetchArgs;
use fetch_many::BlobstoreFetchManyArgs;
use mononoke_app::args::RepoBlobstoreArgs;
use mononoke_app::MononokeApp;
use upload::BlobstoreUploadArgs;

/// Directly access blobstore keys
#[derive(Parser)]
pub struct CommandArgs {
    #[clap(flatten)]
    repo_blobstore_args: RepoBlobstoreArgs,

    #[clap(subcommand)]
    subcommand: BlobstoreSubcommand,
}

#[derive(Subcommand)]
pub enum BlobstoreSubcommand {
    /// Fetch a blob from the blobstore
    Fetch(BlobstoreFetchArgs),
    /// Upload a blob to the blobstore
    Upload(BlobstoreUploadArgs),
    /// Fetches multiple blobs, and outputs how many were present, not present or errored.
    /// Most useful to use with scrub to repair blobs.
    FetchMany(BlobstoreFetchManyArgs),
}

pub async fn run(app: MononokeApp, args: CommandArgs) -> Result<()> {
    let ctx = app.new_basic_context();

    let blobstore = app
        .open_blobstore(&args.repo_blobstore_args)
        .await
        .context("Failed to open blobstore")?;

    match args.subcommand {
        BlobstoreSubcommand::Fetch(fetch_args) => {
            fetch::fetch(&ctx, &blobstore, fetch_args).await?
        }
        BlobstoreSubcommand::FetchMany(args) => {
            fetch_many::fetch_many(&ctx, &blobstore, args).await?
        }
        BlobstoreSubcommand::Upload(upload_args) => {
            upload::upload(&ctx, &blobstore, upload_args).await?
        }
    }

    Ok(())
}


/*
 * 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::fs;
use std::fs::metadata;
use std::fs::File;
use std::io;
use std::io::BufRead;
use std::path::Path;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::Arc;
use std::time::Instant;

use anyhow::Context;
use anyhow::Error;
use anyhow::Result;
use blobstore::BlobstoreUnlinkOps;
use clap::ArgAction;
use clap::Parser;
use mononoke_app::args::RepoArg;
use mononoke_app::MononokeApp;
use mononoke_types::RepositoryId;
use regex::Regex;

use crate::commands::blobstore_unlink::get_blobstores;

#[derive(Parser)]
pub struct CommandArgs {
    /// The directory that contains all the key files
    #[arg(short, long)]
    keys_dir: String,

    /// If we're dry running the command, print out the blobstore keys to be deleted
    #[arg(short, long, default_value_t = true, action = ArgAction::Set)]
    dry_run: bool,

    /// regex that is used to check if the key is suppose to be deleted
    #[arg(short, long)]
    sanitise_regex: String,
}

#[allow(dead_code)]
struct BlobstoreBulkUnlinker {
    app: MononokeApp,
    keys_dir: String,
    dry_run: bool,
    sanitise_regex: String,
    repo_to_blobstores: HashMap<RepositoryId, Vec<Arc<dyn BlobstoreUnlinkOps>>>,
}

impl BlobstoreBulkUnlinker {
    fn new(
        app: MononokeApp,
        keys_dir: String,
        dry_run: bool,
        sanitise_regex: String,
    ) -> BlobstoreBulkUnlinker {
        BlobstoreBulkUnlinker {
            app,
            keys_dir,
            dry_run,
            sanitise_regex,
            repo_to_blobstores: HashMap::new(),
        }
    }

    fn read_lines<P>(&self, file_path: P) -> io::Result<io::Lines<io::BufReader<File>>>
    where
        P: AsRef<Path>,
    {
        let file = File::open(file_path)?;
        Ok(io::BufReader::new(file).lines())
    }

    fn extract_repo_id_from_key(&self, key: &str) -> Result<RepositoryId, Error> {
        let re = Regex::new(r".*repo([0-9]+)..*")?;
        let caps = re
            .captures(key)
            .with_context(|| format!("Failed to capture lambda for key {}", key))?;
        let repo_id_str = caps.get(1).map_or("", |m| m.as_str());
        RepositoryId::from_str(repo_id_str)
    }

    fn extract_blobstore_key_from(&self, key: &str) -> Result<String, Error> {
        let re = Regex::new(r".*(repo[0-9]+..*)")?;
        let caps = re
            .captures(key)
            .with_context(|| format!("Failed to capture lambda for key {}", key))?;
        let blobstore_key = caps.get(1).map_or("", |m| m.as_str());
        Ok(blobstore_key.to_string())
    }

    async fn get_blobstores_from_repo_id(
        &mut self,
        repo_id: RepositoryId,
    ) -> Result<&Vec<Arc<dyn BlobstoreUnlinkOps>>> {
        use std::collections::hash_map::Entry::Vacant;
        if let Vacant(e) = self.repo_to_blobstores.entry(repo_id) {
            let (_repo_name, repo_config) = self.app.repo_config(&RepoArg::Id(repo_id))?;
            let blobstores = get_blobstores(
                self.app.fb,
                repo_config.storage_config,
                None,
                self.app.environment().readonly_storage,
                &self.app.environment().blobstore_options,
                self.app.config_store(),
            )
            .await?;

            e.insert(blobstores);
        }
        return Ok(self.repo_to_blobstores.get(&repo_id).unwrap());
    }

    fn sanitise_check(&self, key: &str) -> Result<()> {
        let re = Regex::new(&self.sanitise_regex).unwrap();
        if !re.is_match(key) {
            panic!(
                "Key {} does not match the sanitise checking regex {}",
                key, &self.sanitise_regex
            );
        }
        Ok(())
    }

    async fn unlink_the_key_from_blobstore(&mut self, key: &str) -> Result<()> {
        let context = self.app.new_basic_context().clone();

        if let (Ok(repo_id), Ok(blobstore_key)) = (
            self.extract_repo_id_from_key(key),
            self.extract_blobstore_key_from(key),
        ) {
            // do a sanitising check before we start deleting
            self.sanitise_check(&blobstore_key).unwrap();

            if self.dry_run {
                println!("\tUnlink key: {}", key);
                return Ok(());
            }

            let blobstores = self.get_blobstores_from_repo_id(repo_id).await?;

            let mut num_errors = 0;
            let num_blobstores = blobstores.len();
            for blobstore in blobstores {
                match blobstore.unlink(&context, &blobstore_key).await {
                    Ok(_) => {}
                    Err(e) => {
                        num_errors += 1;
                        let error_msg = e.to_string();
                        if !error_msg.contains("does not exist in the blobstore")
                            && !error_msg.contains("[404] Path not found")
                        {
                            eprintln!(
                                "Failed to unlink key {} in one underlying blobstore, error: {}.",
                                blobstore_key, error_msg
                            );
                        }
                    }
                }
            }

            if num_errors == num_blobstores {
                eprintln!(
                    "For key {}, no blobstore contained this key.",
                    blobstore_key
                );
            }
        } else {
            eprintln!("Skip invalid key: {}", key);
            return Ok(());
        }

        Ok(())
    }

    async fn bulk_unlink_keys_in_file(
        &mut self,
        path: &PathBuf,
        cur: usize,
        total_file_count: usize,
    ) -> Result<()> {
        let md = metadata(path.clone()).unwrap();
        if !md.is_file() {
            println!("Skip path: {} because it is not a file.", path.display(),);
            return Ok(());
        }

        println!(
            "Processing keys in file (with dry-run={}): {}",
            self.dry_run,
            path.display()
        );

        if let Ok(lines) = self.read_lines(path) {
            let now = Instant::now();
            for line in lines {
                if let Ok(key) = line {
                    self.unlink_the_key_from_blobstore(&key).await?;
                }
            }
            let elapsed = now.elapsed();
            println!(
                "Progress: {:.3}%\tprocessing took {:.2?}",
                (cur + 1) as f32 * 100.0 / total_file_count as f32,
                elapsed
            );
        }

        Ok(())
    }

    async fn start_unlink(&mut self) -> Result<()> {
        let entries = fs::read_dir(self.keys_dir.clone())?
            .map(|res| res.map(|e| e.path()))
            .collect::<Result<Vec<_>, io::Error>>()?;

        let total_file_count = entries.len();
        for (cur, entry) in entries.iter().enumerate() {
            self.bulk_unlink_keys_in_file(entry, cur, total_file_count)
                .await?;
        }
        Ok(())
    }
}

pub async fn run(app: MononokeApp, args: CommandArgs) -> Result<()> {
    let keys_dir = args.keys_dir;
    let dry_run = args.dry_run;
    let sanitise_regex = args.sanitise_regex;

    if dry_run {
        println!(
            "Running the bulk deletion with a dry-run mode. Please use --dry-run false to perform the real deletion."
        );
    }

    let mut unlinker = BlobstoreBulkUnlinker::new(app, keys_dir.clone(), dry_run, sanitise_regex);
    unlinker.start_unlink().await?;

    Ok(())
}


/*
 * 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::io::Write;
use std::sync::Arc;

use anyhow::bail;
use anyhow::format_err;
use anyhow::Error;
use anyhow::Result;
use blobstore::BlobstoreUnlinkOps;
use blobstore_factory::make_files_blobstore;
use blobstore_factory::make_manifold_blobstore;
use blobstore_factory::make_sql_blobstore;
use blobstore_factory::BlobstoreOptions;
use blobstore_factory::ReadOnlyStorage;
use cached_config::ConfigStore;
use clap::Parser;
use fbinit::FacebookInit;
use metaconfig_types::BlobConfig;
use metaconfig_types::BlobstoreId;
use metaconfig_types::StorageConfig;
use mononoke_app::args::AsRepoArg;
use mononoke_app::args::RepoArgs;
use mononoke_app::MononokeApp;
use BlobConfig::*;

/// Unlink blobstore keys
///
/// Currently only works for SqlBlob.
#[derive(Parser)]
pub struct CommandArgs {
    #[clap(flatten)]
    repo_args: RepoArgs,

    /// If the repo's blobstore is multiplexed, use this inner blobstore
    #[clap(long)]
    inner_blobstore_id: Option<u64>,

    /// Key of the blob to unlink
    key: String,
}

fn remove_wrapper_blobconfigs(mut blob_config: BlobConfig) -> BlobConfig {
    // Pack is a wrapper store - remove it
    while let BlobConfig::Pack { ref blobconfig, .. } = blob_config {
        blob_config = BlobConfig::clone(blobconfig);
    }
    blob_config
}

fn get_blobconfig(blob_config: BlobConfig, inner_blobstore_id: Option<u64>) -> Result<BlobConfig> {
    match inner_blobstore_id {
        None => Ok(blob_config),
        Some(inner_blobstore_id) => match blob_config {
            BlobConfig::MultiplexedWal { blobstores, .. } => {
                let seeked_id = BlobstoreId::new(inner_blobstore_id);
                blobstores
                    .into_iter()
                    .find_map(|(blobstore_id, _, blobstore)| {
                        if blobstore_id == seeked_id {
                            Some(remove_wrapper_blobconfigs(blobstore))
                        } else {
                            None
                        }
                    })
                    .ok_or_else(|| {
                        format_err!("could not find a blobstore with id {}", inner_blobstore_id)
                    })
            }
            _ => Err(format_err!(
                "inner-blobstore-id supplied but blobstore is not multiplexed"
            )),
        },
    }
}

async fn get_single_blobstore_impl(
    fb: FacebookInit,
    storage_config: StorageConfig,
    inner_blobstore_id: Option<u64>,
    readonly_storage: ReadOnlyStorage,
    blobstore_options: &BlobstoreOptions,
    config_store: &ConfigStore,
) -> Result<Arc<dyn BlobstoreUnlinkOps>, Error> {
    let blobconfig = get_blobconfig(storage_config.blobstore, inner_blobstore_id)?;

    let blobstore = match blobconfig {
        // Physical blobstores
        Sqlite { .. } | Mysql { .. } => make_sql_blobstore(
            fb,
            blobconfig,
            readonly_storage,
            blobstore_options,
            config_store,
        )
        .await
        .map(|store| Arc::new(store) as Arc<dyn BlobstoreUnlinkOps>)?,
        Manifold { .. } | ManifoldWithTtl { .. } => {
            make_manifold_blobstore(fb, blobconfig, blobstore_options)
                .await
                .map(|store| Arc::new(store) as Arc<dyn BlobstoreUnlinkOps>)?
        }
        Files { .. } => make_files_blobstore(blobconfig, blobstore_options)
            .await
            .map(|store| Arc::new(store) as Arc<dyn BlobstoreUnlinkOps>)?,
        _ => {
            bail!(
                "Unlink is not implemented for this blobstore with inner_blobstore_id = {:?}",
                inner_blobstore_id
            )
        }
    };

    Ok(blobstore)
}

async fn get_multiple_blobstores(
    fb: FacebookInit,
    storage_config: StorageConfig,
    blobconfig: BlobConfig,
    readonly_storage: ReadOnlyStorage,
    blobstore_options: &BlobstoreOptions,
    config_store: &ConfigStore,
) -> Result<Vec<Arc<dyn BlobstoreUnlinkOps>>, Error> {
    let blobstores = match blobconfig {
        MultiplexedWal { blobstores, .. } => {
            let mut underlying_blobstores: Vec<Arc<dyn BlobstoreUnlinkOps>> = Vec::new();
            writeln!(
                std::io::stdout(),
                "This MultiplexedWal blobstore has the following inner stores:"
            )?;
            for record in blobstores {
                let underlying_blobstore = get_single_blobstore_impl(
                    fb,
                    storage_config.clone(),
                    Some(record.0.into()),
                    readonly_storage,
                    blobstore_options,
                    config_store,
                )
                .await?;
                underlying_blobstores.push(underlying_blobstore);
                writeln!(std::io::stdout(), "Blobstore inner_id: {}", record.0)?;
            }
            underlying_blobstores
        }
        _ => {
            bail!("Only the MultiplexedWal type BlobConfig is allowd to pass into this funciton")
        }
    };
    Ok(blobstores)
}

/// This function works as follows:
///  * If inner_blobstore_id is given, then we will only return the single inner blobstore,
///    within the corresponding multiplexed store
///  * If the inner_blobstore_id is not given, we will return all the inner blobstores,
///    within the corresponding multiplexed store
///
/// Regarding the implementation, get_blobconfig function will return the single inner
/// blobstore if inner_blobstore_id is given. Then we construct a blobstore for it, and return.
/// Otherwise, the get_blobconfig function returns the multiplexed blobstore config. We then
/// lookup all the inner blobstore ids, and construct them eventually.
///
/// We haven't implemented this function to be a recursive function, because we are currently
/// not supporting nested multiplexed blobstores.
pub async fn get_blobstores(
    fb: FacebookInit,
    storage_config: StorageConfig,
    inner_blobstore_id: Option<u64>,
    readonly_storage: ReadOnlyStorage,
    blobstore_options: &BlobstoreOptions,
    config_store: &ConfigStore,
) -> Result<Vec<Arc<dyn BlobstoreUnlinkOps>>, Error> {
    let blobconfig = get_blobconfig(storage_config.blobstore.clone(), inner_blobstore_id)?;
    let blobstores = match blobconfig {
        // Physical blobstores
        Sqlite { .. } | Mysql { .. } | Manifold { .. } | ManifoldWithTtl { .. } | Files { .. } => {
            let single_store = get_single_blobstore_impl(
                fb,
                storage_config,
                inner_blobstore_id,
                readonly_storage,
                blobstore_options,
                config_store,
            )
            .await?;
            vec![single_store]
        }
        MultiplexedWal { .. } => {
            get_multiple_blobstores(
                fb,
                storage_config,
                blobconfig,
                readonly_storage,
                blobstore_options,
                config_store,
            )
            .await?
        }
        _ => {
            bail!("Unlink is not implemented for this blobstore")
        }
    };
    Ok(blobstores)
}

pub async fn run(app: MononokeApp, args: CommandArgs) -> Result<()> {
    let ctx = app.new_basic_context();

    let repo_arg = args.repo_args.as_repo_arg();
    let (_repo_name, repo_config) = app.repo_config(repo_arg)?;
    let blobstores = get_blobstores(
        app.fb,
        repo_config.storage_config,
        args.inner_blobstore_id,
        app.environment().readonly_storage,
        &app.environment().blobstore_options,
        app.config_store(),
    )
    .await?;

    for blobstore in blobstores {
        match blobstore.unlink(&ctx, &args.key).await {
            Ok(_) => {
                writeln!(
                    std::io::stdout(),
                    "Unlinking key {} successfully in one underlying blobstore",
                    args.key
                )?;
            }
            Err(e) => {
                writeln!(
                    std::io::stdout(),
                    "Failed to unlink key {} in one underlying blobstore, error: {}.",
                    args.key,
                    e
                )?;
            }
        }
    }

    Ok(())
}


/*
 * 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.
 */

mod delete;
mod get;
mod list;
mod log;
mod set;

use anyhow::Context;
use anyhow::Result;
use bonsai_git_mapping::BonsaiGitMapping;
use bonsai_globalrev_mapping::BonsaiGlobalrevMapping;
use bonsai_hg_mapping::BonsaiHgMapping;
use bonsai_svnrev_mapping::BonsaiSvnrevMapping;
use bonsai_tag_mapping::BonsaiTagMapping;
use bookmarks::BookmarkUpdateLog;
use bookmarks::Bookmarks;
use clap::Parser;
use clap::Subcommand;
use delete::BookmarksDeleteArgs;
use get::BookmarksGetArgs;
use list::BookmarksListArgs;
use log::BookmarksLogArgs;
use mononoke_app::args::RepoArgs;
use mononoke_app::MononokeApp;
use repo_cross_repo::RepoCrossRepo;
use repo_identity::RepoIdentity;
use set::BookmarksSetArgs;

/// Manage repository bookmarks
///
/// This is a low-level command providing direct access to the bookmarks
/// data store.  It allows modifications that would not ordinarily be
/// possible through normal bookmark movement requests.  You should prefer
/// using normal bookmark movements (via 'hg push' or 'scsc') unless the
/// modification you are making needs to be a low-level one.
#[derive(Parser)]
pub struct CommandArgs {
    #[clap(flatten)]
    repo: RepoArgs,

    #[clap(subcommand)]
    subcommand: BookmarksSubcommand,
}

#[facet::container]
pub struct Repo {
    #[facet]
    repo_identity: RepoIdentity,

    #[facet]
    bookmarks: dyn Bookmarks,

    #[facet]
    bookmark_update_log: dyn BookmarkUpdateLog,

    #[facet]
    bonsai_hg_mapping: dyn BonsaiHgMapping,

    #[facet]
    bonsai_git_mapping: dyn BonsaiGitMapping,

    #[facet]
    bonsai_tag_mapping: dyn BonsaiTagMapping,

    #[facet]
    bonsai_globalrev_mapping: dyn BonsaiGlobalrevMapping,

    #[facet]
    bonsai_svnrev_mapping: dyn BonsaiSvnrevMapping,

    #[facet]
    repo_cross_repo: RepoCrossRepo,
}

#[derive(Subcommand)]
pub enum BookmarksSubcommand {
    /// Get the changeset of a bookmark
    Get(BookmarksGetArgs),
    /// List bookmarks
    List(BookmarksListArgs),
    /// Show the log of changesets for a bookmark
    Log(BookmarksLogArgs),
    /// Set a bookmark to a specific changeset
    ///
    /// This is a low-level command that writes directly to the bookmark
    /// store.  Prefer using ordinary methods to modify bookmarks where
    /// possible.
    Set(BookmarksSetArgs),
    /// Delete a bookmark
    ///
    /// This is a low-level command that writes directly to the bookmark
    /// store.  Prefer using ordinary methods to modify bookmarks where
    /// possible.
    Delete(BookmarksDeleteArgs),
}

pub async fn run(app: MononokeApp, args: CommandArgs) -> Result<()> {
    let ctx = app.new_basic_context();

    let repo: Repo = app
        .open_repo(&args.repo)
        .await
        .context("Failed to open repo")?;

    match args.subcommand {
        BookmarksSubcommand::Get(get_args) => get::get(&ctx, &repo, get_args).await?,
        BookmarksSubcommand::Log(log_args) => log::log(&ctx, &repo, log_args).await?,
        BookmarksSubcommand::List(list_args) => list::list(&ctx, &repo, list_args).await?,
        BookmarksSubcommand::Set(set_args) => set::set(&ctx, &repo, set_args).await?,
        BookmarksSubcommand::Delete(delete_args) => {
            delete::delete(&ctx, &repo, delete_args).await?
        }
    }

    Ok(())
}


/*
 * 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.
 */

mod graph;
mod list_ancestors;

use anyhow::Context;
use anyhow::Result;
use bonsai_git_mapping::BonsaiGitMapping;
use bonsai_globalrev_mapping::BonsaiGlobalrevMapping;
use bonsai_hg_mapping::BonsaiHgMapping;
use bonsai_svnrev_mapping::BonsaiSvnrevMapping;
use changeset_fetcher::ChangesetFetcher;
use changesets::Changesets;
use clap::Parser;
use clap::Subcommand;
use metaconfig_types::RepoConfig;
use mononoke_app::args::RepoArgs;
use mononoke_app::MononokeApp;
use repo_blobstore::RepoBlobstore;
use repo_identity::RepoIdentity;

use self::graph::ChangelogGraphArgs;
use self::list_ancestors::ChangelogListAncestorsArgs;

/// Manipulate changelogs
#[derive(Parser)]
pub struct CommandArgs {
    #[clap(flatten)]
    repo_args: RepoArgs,

    #[clap(subcommand)]
    subcommand: ChangelogSubcommand,
}

#[facet::container]
pub struct Repo {
    #[facet]
    repo_identity: RepoIdentity,

    #[facet]
    repo_blobstore: RepoBlobstore,

    #[facet]
    repo_config: RepoConfig,

    #[facet]
    bonsai_hg_mapping: dyn BonsaiHgMapping,

    #[facet]
    bonsai_git_mapping: dyn BonsaiGitMapping,

    #[facet]
    bonsai_globalrev_mapping: dyn BonsaiGlobalrevMapping,

    #[facet]
    bonsai_svnrev_mapping: dyn BonsaiSvnrevMapping,

    #[facet]
    changesets: dyn Changesets,

    #[facet]
    changeset_fetcher: dyn ChangesetFetcher,
}

#[derive(Subcommand)]
pub enum ChangelogSubcommand {
    /// Display parts of the commit DAG
    Graph(ChangelogGraphArgs),

    /// List ancestors of a commit
    ListAncestors(ChangelogListAncestorsArgs),
}

pub async fn run(app: MononokeApp, args: CommandArgs) -> Result<()> {
    let ctx = app.new_basic_context();

    let repo: Repo = app
        .open_repo(&args.repo_args)
        .await
        .context("Failed to open repo")?;

    match args.subcommand {
        ChangelogSubcommand::Graph(graph_args) => graph::graph(&ctx, &repo, graph_args).await?,
        ChangelogSubcommand::ListAncestors(list_ancestors_args) => {
            list_ancestors::list_ancestors(&ctx, &repo, list_ancestors_args).await?
        }
    }

    Ok(())
}


/*
 * 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.
 */

mod pushrebase;
mod rebase;
mod split;

use anyhow::bail;
use anyhow::Context;
use anyhow::Result;
use bonsai_git_mapping::BonsaiGitMapping;
use bonsai_globalrev_mapping::BonsaiGlobalrevMapping;
use bonsai_hg_mapping::BonsaiHgMapping;
use bonsai_svnrev_mapping::BonsaiSvnrevMapping;
use bookmarks::Bookmarks;
use changeset_fetcher::ChangesetFetcher;
use changesets::Changesets;
use clap::Parser;
use clap::Subcommand;
use commit_graph::CommitGraph;
use commit_graph::CommitGraphRef;
use context::CoreContext;
use futures::StreamExt;
use futures::TryStreamExt;
use metaconfig_types::RepoConfig;
use mononoke_app::args::RepoArgs;
use mononoke_app::MononokeApp;
use mononoke_types::ChangesetId;
use pushrebase_mutation_mapping::PushrebaseMutationMapping;
use repo_blobstore::RepoBlobstore;
use repo_bookmark_attrs::RepoBookmarkAttrs;
use repo_cross_repo::RepoCrossRepo;
use repo_derived_data::RepoDerivedData;
use repo_identity::RepoIdentity;

use self::pushrebase::CommitPushrebaseArgs;
use self::rebase::CommitRebaseArgs;
use self::split::CommitSplitArgs;

/// Manipulate commits
#[derive(Parser)]
pub struct CommandArgs {
    #[clap(flatten)]
    repo_args: RepoArgs,

    #[clap(subcommand)]
    subcommand: CommitSubcommand,
}

#[facet::container]
pub struct Repo {
    #[facet]
    repo_identity: RepoIdentity,

    #[facet]
    repo_config: RepoConfig,

    #[facet]
    bonsai_hg_mapping: dyn BonsaiHgMapping,

    #[facet]
    bonsai_git_mapping: dyn BonsaiGitMapping,

    #[facet]
    bonsai_globalrev_mapping: dyn BonsaiGlobalrevMapping,

    #[facet]
    bonsai_svnrev_mapping: dyn BonsaiSvnrevMapping,

    #[facet]
    repo_blobstore: RepoBlobstore,

    #[facet]
    changesets: dyn Changesets,

    #[facet]
    changeset_fetcher: dyn ChangesetFetcher,

    #[facet]
    commit_graph: CommitGraph,

    #[facet]
    bookmarks: dyn Bookmarks,

    #[facet]
    bookmark_attrs: RepoBookmarkAttrs,

    #[facet]
    repo_derived_data: RepoDerivedData,

    #[facet]
    pushrebase_mutation_mapping: dyn PushrebaseMutationMapping,

    #[facet]
    repo_cross_repo: RepoCrossRepo,
}

#[derive(Subcommand)]
pub enum CommitSubcommand {
    /// Split a large commit into a stack
    ///
    /// Attempts to maintain limits on the number of files and size of all the
    /// files in each of the commits, however these limits are not strictly
    /// enforced, i.e. some of the commits might have larger sizes or more
    /// files, e.g. if a single file is larger than the limit, or if there are
    /// a large number of grouped copy sources and their destinations.
    ///
    /// The stack is printed in order from ancestor to descendant.
    Split(CommitSplitArgs),

    /// Rebase a commit
    ///
    /// Rebases a commit from its current parent to a new parent.  This is a
    /// low-level command and does not perform any validation on the rebase.
    /// The caller must ensure that the result of this rebase is valid.
    Rebase(CommitRebaseArgs),

    /// Pushrebase a commit
    ///
    /// Rebases a commit from its current bookmark onto a bookmark, and moves
    /// that bookmark to the newly rebased commit.
    Pushrebase(CommitPushrebaseArgs),
}

pub async fn run(app: MononokeApp, args: CommandArgs) -> Result<()> {
    let ctx = app.new_basic_context();

    let repo: Repo = app
        .open_repo(&args.repo_args)
        .await
        .context("Failed to open repo")?;

    match args.subcommand {
        CommitSubcommand::Split(split_args) => split::split(&ctx, &repo, split_args).await?,
        CommitSubcommand::Rebase(rebase_args) => rebase::rebase(&ctx, &repo, rebase_args).await?,
        CommitSubcommand::Pushrebase(pushrebase_args) => {
            pushrebase::pushrebase(&ctx, &repo, pushrebase_args).await?
        }
    }

    Ok(())
}

async fn resolve_stack(
    ctx: &CoreContext,
    repo: &Repo,
    bottom: ChangesetId,
    top: ChangesetId,
) -> Result<Vec<ChangesetId>> {
    repo.commit_graph()
        .range_stream(ctx, bottom, top)
        .await?
        .map(|csid| async move {
            let parents = repo
                .commit_graph()
                .changeset_parents_required(ctx, csid)
                .await?;
            if parents.len() > 1 {
                bail!("rebasing stacks with merges is not supported");
            }
            Ok(csid)
        })
        .buffered(100)
        .try_collect::<Vec<_>>()
        .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 anyhow::Context;
use anyhow::Result;
use bonsai_git_mapping::BonsaiGitMapping;
use bonsai_globalrev_mapping::BonsaiGlobalrevMapping;
use bonsai_hg_mapping::BonsaiHgMapping;
use bonsai_svnrev_mapping::BonsaiSvnrevMapping;
use clap::Parser;
use mononoke_app::args::RepoArgs;
use mononoke_app::MononokeApp;

use crate::commit_id::parse_commit_id;
use crate::commit_id::print_commit_id;
use crate::commit_id::IdentityScheme;

/// Convert commit identity between identity schemes
#[derive(Parser)]
pub struct CommandArgs {
    #[clap(flatten)]
    repo: RepoArgs,

    /// Identity scheme to convert from
    #[clap(long, short = 'f', value_enum, value_name = "SCHEME")]
    from: Option<IdentityScheme>,

    /// Identity scheme to
    #[clap(
        long,
        short = 't',
        value_enum,
        value_name = "SCHEME",
        required = true,
        use_value_delimiter = true
    )]
    to: Vec<IdentityScheme>,

    /// Source commit id
    id: String,
}

#[facet::container]
pub struct Repo {
    #[facet]
    bonsai_hg_mapping: dyn BonsaiHgMapping,

    #[facet]
    bonsai_git_mapping: dyn BonsaiGitMapping,

    #[facet]
    bonsai_globalrev_mapping: dyn BonsaiGlobalrevMapping,

    #[facet]
    bonsai_svnrev_mapping: dyn BonsaiSvnrevMapping,
}

pub async fn run(app: MononokeApp, args: CommandArgs) -> Result<()> {
    let ctx = app.new_basic_context();

    let repo: Repo = app
        .open_repo(&args.repo)
        .await
        .context("Failed to open repo")?;

    let cs_id = match args.from {
        Some(scheme) => scheme.parse_commit_id(&ctx, &repo, &args.id).await?,
        None => parse_commit_id(&ctx, &repo, &args.id).await?,
    };

    print_commit_id(&ctx, &repo, &args.to, cs_id).await?;

    Ok(())
}


/*
 * 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::num::NonZeroU64;
use std::path::Path;

use anyhow::Result;
use bonsai_git_mapping::BonsaiGitMapping;
use bonsai_globalrev_mapping::BonsaiGlobalrevMapping;
use bonsai_hg_mapping::BonsaiHgMapping;
use bonsai_svnrev_mapping::BonsaiSvnrevMapping;
use bulkops::Direction;
use bulkops::PublicChangesetBulkFetch;
use bytes::Bytes;
use changeset_fetcher::ChangesetFetcher;
use changesets::deserialize_cs_entries;
use changesets::serialize_cs_entries;
use changesets::ChangesetEntry;
use changesets::Changesets;
use changesets::ChangesetsArc;
use changesets::ChangesetsRef;
use clap::Args;
use clap::Parser;
use clap::Subcommand;
use clap::ValueEnum;
use commit_graph::CommitGraph;
use commit_graph::CommitGraphRef;
use context::CoreContext;
use futures::future;
use futures::stream;
use futures::StreamExt;
use futures::TryStreamExt;
use itertools::Itertools;
use mononoke_app::args::RepoArgs;
use mononoke_app::MononokeApp;
use mononoke_types::ChangesetId;
use phases::Phases;
use phases::PhasesArc;
use repo_identity::RepoIdentity;

use crate::commit_id::parse_commit_id;

#[derive(Debug, Clone, Copy, ValueEnum)]
enum Format {
    /// Thrift serialized ChangesetEntry, with info about repo, parents and generation number.
    Thrift,
    /// One plaintext bonsai id per line, without any repo information
    Plaintext,
}

/// Dump all public changeset entries to a file.
#[derive(Parser)]
pub struct CommandArgs {
    /// Which repo to dump changesets from.
    #[clap(flatten)]
    repo: RepoArgs,

    /// File name where commits will be saved.
    #[clap(long)]
    out_filename: String,
    /// Merge commits from this file into the final output. User is responsible for
    /// avoiding duplicate commits between files and database fetch. Can be repeated.
    #[clap(long)]
    merge_file: Vec<String>,
    /// What format to write to the file.
    #[clap(long, value_enum, default_value_t=Format::Thrift)]
    output_format: Format,
    /// What format to read files with. Plaintext files can be in hg format.
    #[clap(long, value_enum, default_value_t=Format::Thrift)]
    input_format: Format,

    #[clap(subcommand)]
    subcommand: DumpChangesetsSubcommand,
}

#[derive(Subcommand)]
pub enum DumpChangesetsSubcommand {
    /// Fetch all public changesets before dumping.
    FetchPublic(FetchPublicArgs),
    /// Don't do any extra fetching of changesets, useful for merging dumps and changing formats.
    Convert(ConvertArgs),
    /// Fetch all commits until given heads
    FixedHeads(FixedHeadsArgs),
}

#[derive(Args)]
pub struct FetchPublicArgs {
    /// Start fetching from this commit rather than the beginning of time.
    #[clap(long)]
    start_commit: Option<String>,
    /// Start fetching from the last commit in this file, for incremental updates.
    #[clap(long)]
    start_from_file_end: Option<String>,
    /// Only look at this many commits. Notice that this may output less than LIMIT
    /// commits if there are non-public commits, but it's a good way to do this command
    /// incrementally.
    #[clap(long)]
    limit: Option<NonZeroU64>,
}

#[derive(Args)]
pub struct ConvertArgs {}

#[derive(Args)]
pub struct FixedHeadsArgs {
    /// Heads to list commits until
    #[clap(long)]
    head: Vec<String>,
}

#[facet::container]
pub struct Repo {
    #[facet]
    bonsai_hg_mapping: dyn BonsaiHgMapping,

    #[facet]
    bonsai_git_mapping: dyn BonsaiGitMapping,

    #[facet]
    bonsai_globalrev_mapping: dyn BonsaiGlobalrevMapping,

    #[facet]
    bonsai_svnrev_mapping: dyn BonsaiSvnrevMapping,

    #[facet]
    changesets: dyn Changesets,

    #[facet]
    commit_graph: CommitGraph,

    #[facet]
    repo_identity: RepoIdentity,

    #[facet]
    phases: dyn Phases,

    #[facet]
    changeset_fetcher: dyn ChangesetFetcher,
}

impl Format {
    fn serialize(self, entries: Vec<ChangesetEntry>) -> Bytes {
        match self {
            Self::Thrift => serialize_cs_entries(entries),
            Self::Plaintext => Bytes::from(entries.into_iter().map(|e| e.cs_id).join("\n")),
        }
    }

    async fn deserialize<'a>(
        self,
        ctx: &'a CoreContext,
        repo: &'a Repo,
        data: &'a Bytes,
    ) -> Result<Vec<ChangesetEntry>> {
        match self {
            Self::Thrift => deserialize_cs_entries(data),
            Self::Plaintext => {
                let ids: Vec<ChangesetId> = stream::iter(
                    String::from_utf8(data.iter().cloned().collect())?
                        .split_whitespace()
                        .map(|s| parse_commit_id(ctx, repo, s))
                        // prevent compiler bug
                        .collect::<Vec<_>>(),
                )
                .buffered(500)
                .try_collect()
                .await?;
                repo.changesets().get_many(ctx, ids).await
            }
        }
    }
}

impl DumpChangesetsSubcommand {
    async fn fetch_extra_changesets(
        self,
        ctx: &CoreContext,
        repo: &Repo,
        input_format: Format,
    ) -> Result<Vec<ChangesetEntry>> {
        match self {
            Self::Convert(_) => Ok(vec![]),
            Self::FetchPublic(args) => args.fetch_extra_changesets(ctx, repo, input_format).await,
            Self::FixedHeads(args) => args.fetch_extra_changesets(ctx, repo).await,
        }
    }
}

impl FixedHeadsArgs {
    async fn fetch_extra_changesets(
        self,
        ctx: &CoreContext,
        repo: &Repo,
    ) -> Result<Vec<ChangesetEntry>> {
        let changesets = repo.changesets();

        let mut css = repo
            .commit_graph()
            .ancestors_difference_stream(
                ctx,
                self.head
                    .into_iter()
                    .map(|id| id.parse())
                    .collect::<Result<_>>()?,
                vec![],
            )
            .await?
            .try_chunks(1000)
            .map_err(|stream::TryChunksError(_chunk, err)| err)
            .map_ok(|ids| async move {
                let css = changesets.get_many(ctx, ids).await?;
                Ok(stream::iter(css.into_iter().map(anyhow::Ok)))
            })
            .try_buffered(5)
            .try_flatten()
            .try_collect::<Vec<_>>()
            .await?;

        css.sort_by_key(|entry| entry.gen);
        Ok(css)
    }
}

impl FetchPublicArgs {
    async fn fetch_extra_changesets(
        self,
        ctx: &CoreContext,
        repo: &Repo,
        input_format: Format,
    ) -> Result<Vec<ChangesetEntry>> {
        let fetcher = PublicChangesetBulkFetch::new(repo.changesets_arc(), repo.phases_arc());

        let start_commit = {
            if let Some(path) = self.start_from_file_end {
                load_last_commit(ctx, repo, path.as_ref(), input_format).await?
            } else if let Some(start_commit) = self.start_commit {
                Some(parse_commit_id(ctx, repo, &start_commit).await?)
            } else {
                None
            }
        };

        let mut bounds = fetcher
            .get_repo_bounds_after_commits(ctx, start_commit.into_iter().collect())
            .await?;

        if let Some(limit) = self.limit {
            bounds.1 = bounds.1.min(bounds.0 + limit.get());
        }

        fetcher
            .fetch_bounded(ctx, Direction::OldestFirst, Some(bounds))
            .try_collect()
            .await
    }
}

pub async fn run(app: MononokeApp, args: CommandArgs) -> Result<()> {
    let ctx = app.new_basic_context();
    let repo: Repo = app.open_repo(&args.repo).await?;
    let input_format = args.input_format;

    let css = {
        let (mut file_css, db_css): (Vec<_>, Vec<_>) = future::try_join(
            stream::iter(
                args.merge_file
                    .iter()
                    .map(|path| load_file_contents(&ctx, &repo, path.as_ref(), input_format))
                    // prevent compiler bug
                    .collect::<Vec<_>>(),
            )
            .buffered(2)
            .try_concat(),
            args.subcommand
                .fetch_extra_changesets(&ctx, &repo, input_format),
        )
        .await?;
        file_css.extend(db_css.into_iter());
        file_css
    };

    let serialized = args.output_format.serialize(css);
    tokio::fs::write(args.out_filename, serialized).await?;

    Ok(())
}

async fn load_file_contents(
    ctx: &CoreContext,
    repo: &Repo,
    filename: &Path,
    format: Format,
) -> Result<Vec<ChangesetEntry>> {
    let file_contents = Bytes::from(tokio::fs::read(filename).await?);
    format.deserialize(ctx, repo, &file_contents).await
}

async fn load_last_commit(
    ctx: &CoreContext,
    repo: &Repo,
    filename: &Path,
    format: Format,
) -> Result<Option<ChangesetId>> {
    Ok(load_file_contents(ctx, repo, filename, format)
        .await?
        .last()
        .map(|e| e.cs_id))
}


/*
 * 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.
 */

mod cleanup;
mod info;
mod list;

use anyhow::Result;
use clap::Parser;
use clap::Subcommand;
use cleanup::EphemeralStoreCleanUpArgs;
use ephemeral_blobstore::EphemeralBlobstoreError;
use ephemeral_blobstore::RepoEphemeralStore;
use info::EphemeralStoreInfoArgs;
use list::EphemeralStoreListArgs;
use mononoke_app::args::RepoArgs;
use mononoke_app::MononokeApp;
use repo_identity::RepoIdentity;

/// Cleanup, list or describe the contents of ephemeral store.
#[derive(Parser)]
pub struct CommandArgs {
    /// The repository name or ID. Any changesets provided for
    /// subcommands will use this repoID for scoping.
    #[clap(flatten)]
    repo: RepoArgs,

    /// The subcommand for ephemeral store.
    #[clap(subcommand)]
    subcommand: EphemeralStoreSubcommand,
}

#[facet::container]
pub struct Repo {
    #[facet]
    repo_ephemeral_store: RepoEphemeralStore,
    #[facet]
    repo_identity: RepoIdentity,
}

#[derive(Subcommand)]
pub enum EphemeralStoreSubcommand {
    /// Cleanup expired bubbles within the ephemeral store.
    Cleanup(EphemeralStoreCleanUpArgs),
    /// Describe metadata associated with a bubble within the ephemeral store.
    Info(EphemeralStoreInfoArgs),
    /// List out the keys of the blobs in a bubble within the ephemeral store.
    List(EphemeralStoreListArgs),
}

pub async fn run(app: MononokeApp, args: CommandArgs) -> Result<()> {
    let ctx = app.new_basic_context();
    let repo: Repo = app.open_repo(&args.repo).await?;

    let subcommand_result = match args.subcommand {
        EphemeralStoreSubcommand::Cleanup(cleanup_args) => {
            cleanup::clean_bubbles(&ctx, &repo, cleanup_args).await
        }
        EphemeralStoreSubcommand::Info(info_args) => info::bubble_info(&repo, info_args).await,
        EphemeralStoreSubcommand::List(list_args) => list::list_keys(&ctx, &repo, list_args).await,
    };
    match subcommand_result {
        Err(e) => match e.downcast_ref::<EphemeralBlobstoreError>() {
            // If this repo does not have an ephemeral store, then this is a no-op
            Some(EphemeralBlobstoreError::NoEphemeralBlobstore(repo_id)) => {
                println!(
                    "Ephemeral store doesn't exist for repo {}, exiting.",
                    repo_id
                );
                Ok(())
            }
            // For any other error, return as-is
            _ => Err(e),
        },
        _ => Ok(()),
    }
}


/*
 * 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::io::Write;

use anyhow::anyhow;
use anyhow::Context;
use anyhow::Result;
use blobstore::Loadable;
use bonsai_hg_mapping::BonsaiHgMapping;
use bonsai_hg_mapping::BonsaiHgMappingRef;
use bookmarks::Bookmarks;
use clap::Parser;
use clap::ValueEnum;
use cmdlib_displaying::display_content;
use cmdlib_displaying::display_hg_manifest;
use cmdlib_displaying::DisplayChangeset;
use context::CoreContext;
use ephemeral_blobstore::BubbleId;
use ephemeral_blobstore::RepoEphemeralStore;
use ephemeral_blobstore::RepoEphemeralStoreRef;
use manifest::Entry;
use manifest::ManifestOps;
use mercurial_types::HgChangesetId;
use mononoke_app::args::ChangesetArgs;
use mononoke_app::args::RepoArgs;
use mononoke_app::MononokeApp;
use mononoke_types::MPath;
use repo_blobstore::RepoBlobstore;
use repo_blobstore::RepoBlobstoreRef;

/// Fetch commit, tree or file data.
#[derive(Parser)]
pub struct CommandArgs {
    #[clap(flatten)]
    repo_args: RepoArgs,

    #[clap(flatten)]
    changeset_args: ChangesetArgs,

    /// Fetch from within this ephemeral bubble
    #[clap(long)]
    bubble_id: Option<BubbleId>,

    /// Path of the tree or file to fetch
    #[clap(long, short = 'p')]
    path: Option<String>,

    /// Format as JSON. Currently works only for changesets.
    #[clap(long)]
    json: bool,

    /// Manifest type to use to find trees or files.
    #[clap(long, short = 'k', value_enum, default_value_t = ManifestKind::Hg)]
    manifest_kind: ManifestKind,
}

#[facet::container]
pub struct Repo {
    #[facet]
    bonsai_hg_mapping: dyn BonsaiHgMapping,

    #[facet]
    bookmarks: dyn Bookmarks,

    #[facet]
    repo_blobstore: RepoBlobstore,

    #[facet]
    repo_ephemeral_store: RepoEphemeralStore,
}

#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)]
pub enum ManifestKind {
    Hg,
    // TODO: Add unode manifest, fsnode and skmf support
}

pub async fn run(app: MononokeApp, args: CommandArgs) -> Result<()> {
    let ctx = app.new_basic_context();

    let repo: Repo = app
        .open_repo(&args.repo_args)
        .await
        .context("Failed to open repo")?;

    let changeset_id = args
        .changeset_args
        .resolve_changeset(&ctx, &repo)
        .await
        .context("Failed to resolve changeset")?
        .ok_or_else(|| anyhow!("Changeset not found"))?;

    let blobstore = match args.bubble_id {
        None => repo.repo_blobstore().clone(),
        Some(bubble_id) => {
            let bubble = repo
                .repo_ephemeral_store()
                .open_bubble(bubble_id)
                .await
                .with_context(|| format!("Failed to open bubble {}", bubble_id))?;
            bubble.wrap_repo_blobstore(repo.repo_blobstore().clone())
        }
    };

    match &args.path {
        None => {
            let cs = changeset_id
                .load(&ctx, &blobstore)
                .await
                .with_context(|| format!("Failed to load changeset {}", changeset_id))?;
            let display_cs =
                DisplayChangeset::try_from(&cs).context("Failed to display changeset")?;

            if args.json {
                let json_cs =
                    serde_json::to_string(&display_cs).context("Failed to convert to JSON")?;
                println!("{}", json_cs);
            } else {
                println!("{}", display_cs);
            }
        }

        Some(path) => match args.manifest_kind {
            ManifestKind::Hg => {
                let hg_changeset_id = repo
                    .bonsai_hg_mapping()
                    .get_hg_from_bonsai(&ctx, changeset_id)
                    .await
                    .context("Failed to get corresponding Hg changeset")?
                    .ok_or_else(|| anyhow!("No Hg changeset for {}", changeset_id))?;
                display_hg_entry(&ctx, &blobstore, hg_changeset_id, path).await?;
            }
        },
    }

    Ok(())
}

async fn display_hg_entry(
    ctx: &CoreContext,
    blobstore: &RepoBlobstore,
    hg_changeset_id: HgChangesetId,
    path: &str,
) -> Result<()> {
    let hg_cs = hg_changeset_id
        .load(ctx, blobstore)
        .await
        .context("Failed to load Hg changeset")?;
    let entry = if path.is_empty() {
        Entry::Tree(hg_cs.manifestid())
    } else {
        let mpath = MPath::new(path).with_context(|| format!("Invalid path: {}", path))?;
        hg_cs
            .manifestid()
            .find_entry(ctx.clone(), blobstore.clone(), Some(mpath))
            .await
            .context("Failed to traverse manifest")?
            .ok_or_else(|| anyhow!("Path does not exist: {}", path))?
    };
    match entry {
        Entry::Leaf((file_type, id)) => {
            let envelope = id
                .load(ctx, blobstore)
                .await
                .context("Failed to load envelope")?;
            let metadata = filestore::get_metadata(blobstore, ctx, &envelope.content_id().into())
                .await
                .context("Failed to load metadata")?
                .ok_or_else(|| {
                    anyhow!(
                        "Content id {} for file {} in {} not found",
                        id,
                        path,
                        hg_changeset_id,
                    )
                })?;
            writeln!(std::io::stdout(), "File-Type: {}", file_type)?;
            writeln!(std::io::stdout(), "Size: {}", metadata.total_size)?;
            writeln!(std::io::stdout(), "Content-Id: {}", metadata.content_id)?;
            writeln!(std::io::stdout(), "Sha1: {}", metadata.sha1)?;
            writeln!(std::io::stdout(), "Sha256: {}", metadata.sha256)?;
            writeln!(std::io::stdout(), "Git-Sha1: {}", metadata.git_sha1)?;

            let content = filestore::fetch_concat(blobstore, ctx, envelope.content_id())
                .await
                .context("Failed to load content")?;
            display_content(std::io::stdout(), content)?;
        }
        Entry::Tree(id) => {
            let manifest = id
                .load(ctx, blobstore)
                .await
                .context("Failed to load manifest")?;
            display_hg_manifest(std::io::stdout(), &manifest)?;
        }
    }
    Ok(())
}