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

use anyhow::Result;
use ascii::AsciiString;
use bookmarks::BookmarkKey;
use metaconfig_types::CommitSyncDirection;
use metaconfig_types::CommonCommitSyncConfig;
use mononoke_types::RepositoryId;
use thiserror::Error;

#[derive(Debug, Eq, Error, PartialEq)]
pub enum ErrorKind {
    #[error("Small repo {0} not found")]
    SmallRepoNotFound(RepositoryId),
}

/// A function to modify bookmark names during the x-repo sync
pub type BookmarkRenamer = Arc<dyn Fn(&BookmarkKey) -> Option<BookmarkKey> + Send + Sync + 'static>;

/// Both forward and reverse `BookmarkRenamer`, encapsulated in a struct
pub struct BookmarkRenamers {
    pub bookmark_renamer: BookmarkRenamer,
    pub reverse_bookmark_renamer: BookmarkRenamer,
}

fn get_prefix_and_common_bookmarks(
    commit_sync_config: &CommonCommitSyncConfig,
    small_repo_id: RepositoryId,
) -> Result<(AsciiString, HashSet<BookmarkKey>)> {
    let common_pushrebase_bookmarks: HashSet<BookmarkKey> = commit_sync_config
        .common_pushrebase_bookmarks
        .iter()
        .cloned()
        .collect();
    let prefix = commit_sync_config
        .small_repos
        .get(&small_repo_id)
        .ok_or(ErrorKind::SmallRepoNotFound(small_repo_id))?
        .bookmark_prefix
        .clone();
    Ok((prefix, common_pushrebase_bookmarks))
}

/// Get a renamer for small-to-large repo sync
pub fn get_small_to_large_renamer(
    commit_sync_config: &CommonCommitSyncConfig,
    small_repo_id: RepositoryId,
) -> Result<BookmarkRenamer> {
    let (prefix, common_pushrebase_bookmarks) =
        get_prefix_and_common_bookmarks(commit_sync_config, small_repo_id)?;
    Ok(Arc::new(move |source_bookmark_name| {
        if common_pushrebase_bookmarks.contains(source_bookmark_name) {
            Some(source_bookmark_name.clone())
        } else {
            let mut prefixed_name = prefix.clone();
            prefixed_name.push_str(source_bookmark_name.as_ascii());
            Some(BookmarkKey::new_ascii(prefixed_name))
        }
    }))
}

/// Get a renamer for a large-to-small repo sync
pub fn get_large_to_small_renamer(
    commit_sync_config: &CommonCommitSyncConfig,
    small_repo_id: RepositoryId,
) -> Result<BookmarkRenamer> {
    let (prefix, common_pushrebase_bookmarks) =
        get_prefix_and_common_bookmarks(commit_sync_config, small_repo_id)?;

    Ok(Arc::new(move |source_bookmark_name| {
        if common_pushrebase_bookmarks.contains(source_bookmark_name) {
            Some(source_bookmark_name.clone())
        } else if source_bookmark_name.as_str().starts_with(prefix.as_str()) {
            let unprefixed = &source_bookmark_name.as_ascii()[prefix.len()..];
            Some(BookmarkKey::new_ascii(unprefixed.into()))
        } else {
            None
        }
    }))
}

/// Get both forward and reverse bookmark renamer in the `BookmarkRenamers` struct
pub fn get_bookmark_renamers(
    commit_sync_config: &CommonCommitSyncConfig,
    small_repo_id: RepositoryId,
    direction: CommitSyncDirection,
) -> Result<BookmarkRenamers> {
    match direction {
        CommitSyncDirection::LargeToSmall => Ok(BookmarkRenamers {
            bookmark_renamer: get_large_to_small_renamer(commit_sync_config, small_repo_id)?,
            reverse_bookmark_renamer: get_small_to_large_renamer(
                commit_sync_config,
                small_repo_id,
            )?,
        }),
        CommitSyncDirection::SmallToLarge => Ok(BookmarkRenamers {
            bookmark_renamer: get_small_to_large_renamer(commit_sync_config, small_repo_id)?,
            reverse_bookmark_renamer: get_large_to_small_renamer(
                commit_sync_config,
                small_repo_id,
            )?,
        }),
    }
}

#[cfg(test)]
mod test {
    use std::str::FromStr;

    use maplit::hashmap;
    use metaconfig_types::SmallRepoPermanentConfig;

    use super::*;

    fn get_commit_sync_config() -> CommonCommitSyncConfig {
        CommonCommitSyncConfig {
            common_pushrebase_bookmarks: vec![
                BookmarkKey::new("m1").unwrap(),
                BookmarkKey::new("m2").unwrap(),
            ],
            small_repos: hashmap! {
                RepositoryId::new(1) => SmallRepoPermanentConfig {
                    bookmark_prefix: AsciiString::from_str("b1/").unwrap(),
                },
                RepositoryId::new(2) => SmallRepoPermanentConfig {
                    bookmark_prefix: AsciiString::from_str("b2/").unwrap(),
                },
            },
            large_repo_id: RepositoryId::new(0),
        }
    }

    #[test]
    fn test_small_to_large_renamer() {
        let commit_sync_config = get_commit_sync_config();
        let bookmark_renamer_1 =
            get_small_to_large_renamer(&commit_sync_config, RepositoryId::new(1)).unwrap();
        let bookmark_renamer_2 =
            get_small_to_large_renamer(&commit_sync_config, RepositoryId::new(2)).unwrap();

        let hello = BookmarkKey::new("hello").unwrap();
        let b1_hello = BookmarkKey::new("b1/hello").unwrap();
        let b2_hello = BookmarkKey::new("b2/hello").unwrap();
        let m1 = BookmarkKey::new("m1").unwrap();
        let m2 = BookmarkKey::new("m2").unwrap();

        assert_eq!(bookmark_renamer_1(&hello), Some(b1_hello));
        assert_eq!(bookmark_renamer_2(&hello), Some(b2_hello));
        assert_eq!(bookmark_renamer_1(&m1), Some(m1.clone()));
        assert_eq!(bookmark_renamer_2(&m1), Some(m1.clone()));
        assert_eq!(bookmark_renamer_1(&m2), Some(m2.clone()));
        assert_eq!(bookmark_renamer_2(&m2), Some(m2.clone()));
    }

    #[test]
    fn test_large_to_small_renamer() {
        let commit_sync_config = get_commit_sync_config();
        let bookmark_renamer_1 =
            get_large_to_small_renamer(&commit_sync_config, RepositoryId::new(1)).unwrap();
        let bookmark_renamer_2 =
            get_large_to_small_renamer(&commit_sync_config, RepositoryId::new(2)).unwrap();

        let hello = BookmarkKey::new("hello").unwrap();
        let b1_hello = BookmarkKey::new("b1/hello").unwrap();
        let b2_hello = BookmarkKey::new("b2/hello").unwrap();
        let m1 = BookmarkKey::new("m1").unwrap();
        let m2 = BookmarkKey::new("m2").unwrap();

        // Unprefixed and non-common-pushrebase bookmarks are not synced
        assert_eq!(bookmark_renamer_1(&hello), None);
        assert_eq!(bookmark_renamer_2(&hello), None);
        // Common-pushrebase bookmarks are synced as is
        assert_eq!(bookmark_renamer_1(&m1), Some(m1.clone()));
        assert_eq!(bookmark_renamer_2(&m1), Some(m1.clone()));
        assert_eq!(bookmark_renamer_1(&m2), Some(m2.clone()));
        assert_eq!(bookmark_renamer_2(&m2), Some(m2.clone()));
        // Correctly prefixed bookmarks are synced with prefixes removed
        assert_eq!(bookmark_renamer_1(&b1_hello), Some(hello.clone()));
        assert_eq!(bookmark_renamer_2(&b2_hello), Some(hello));
        // Bookmarks, prefixed with prefixes, belonging to other small repos are not synced
        assert_eq!(bookmark_renamer_1(&b2_hello), None);
        assert_eq!(bookmark_renamer_2(&b1_hello), None);
    }
}


/*
 * 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 clap::Parser;
use mononoke_app::args::RepoArgs;

#[derive(clap::Subcommand, Debug)]
pub(crate) enum SubcommandValidator {
    /// Validate a single entry (mainly for integration tests)
    Once {
        /// A commit to validate
        #[clap(long)]
        entry_id: u64,
    },
    /// Validates entries in a loop, tailing bookmarks_update_log
    Tail {
        /// Starting BookmarksUpdateLog entry id to use (ignores the mutable_counters)
        #[clap(long)]
        start_id: Option<u64>,
    },
}

/// Mirroring hg commits
#[derive(Parser, Debug)]
#[clap(about = "Mononoke cross-repo sync validator job ")]
pub(crate) struct MononokeCommitValidatorArgs {
    #[clap(flatten)]
    pub(crate) repo: RepoArgs,
    /// Name of the master bookmark in this repo
    #[clap(long, default_value = "master")]
    pub(crate) master_bookmark: String,
    #[clap(subcommand)]
    pub(crate) subcommand: SubcommandValidator,
}


/*
 * 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(auto_traits)]

//! Mononoke Cross Repo validator job
//!
//! This is a special job used to validate that cross-repo sync,
//! produced correct results

use std::collections::HashSet;

use anyhow::format_err;
use anyhow::Context;
use anyhow::Error;
use anyhow::Result;
use blobrepo::BlobRepo;
use bookmarks::BookmarkKey;
use bookmarks::BookmarkUpdateLogArc;
use bookmarks::BookmarkUpdateLogEntry;
use bookmarks::BookmarkUpdateLogRef;
use bookmarks::Freshness;
use context::CoreContext;
use fbinit::FacebookInit;
use futures::future;
use futures::stream;
use futures::stream::Stream;
use futures::stream::StreamExt;
use futures::stream::TryStreamExt;
use mononoke_api_types::InnerRepo;
use mononoke_app::args::AsRepoArg;
use mononoke_app::fb303::AliveService;
use mononoke_app::fb303::Fb303AppExtension;
use mononoke_app::MononokeApp;
use mononoke_app::MononokeAppBuilder;
use mutable_counters::MutableCountersRef;
use repo_identity::RepoIdentityRef;
use scuba_ext::MononokeScubaSampleBuilder;
use slog::info;

mod cli;
mod reporting;
mod setup;
mod tail;
mod validation;

use crate::cli::MononokeCommitValidatorArgs;
use crate::cli::SubcommandValidator::Once;
use crate::cli::SubcommandValidator::Tail;
use crate::setup::format_counter;
use crate::setup::get_start_id;
use crate::setup::get_validation_helpers;
use crate::tail::tail_entries;
use crate::tail::QueueSize;
use crate::validation::get_entry_with_small_repo_mapings;
use crate::validation::prepare_entry;
use crate::validation::unfold_bookmarks_update_log_entry;
use crate::validation::validate_entry;
use crate::validation::EntryCommitId;
use crate::validation::ValidationHelpers;

const SERVICE_NAME: &str = "mononoke_x_repo_commit_validator";

fn validate_stream<'a>(
    ctx: &'a CoreContext,
    validation_helpers: &'a ValidationHelpers,
    entries: impl Stream<Item = Result<(BookmarkUpdateLogEntry, QueueSize), Error>> + 'a,
) -> impl Stream<Item = Result<EntryCommitId, Error>> + 'a {
    entries
        .then(move |bookmarks_update_log_entry_res| async move {
            unfold_bookmarks_update_log_entry(
                ctx,
                bookmarks_update_log_entry_res?,
                validation_helpers,
            )
            .await
        })
        .try_flatten()
        .then(move |res_entry| async move {
            get_entry_with_small_repo_mapings(ctx, res_entry?, validation_helpers).await
        })
        .filter_map(|maybe_entry_res| future::ready(maybe_entry_res.transpose()))
        .then(move |res_entry| async move {
            prepare_entry(ctx, res_entry?, validation_helpers).await
        })
        .map(|res_of_option| res_of_option.transpose())
        .filter_map(future::ready)
        .then(move |prepared_entry| async move {
            let prepared_entry = prepared_entry?;
            let entry_id = prepared_entry.entry_id.clone();

            validate_entry(ctx, prepared_entry, validation_helpers).await?;

            Ok(entry_id)
        })
}

async fn run_in_tailing_mode(
    ctx: &CoreContext,
    blobrepo: BlobRepo,
    skip_bookmarks: HashSet<BookmarkKey>,
    validation_helpers: ValidationHelpers,
    start_id: u64,
    scuba_sample: MononokeScubaSampleBuilder,
) -> Result<(), Error> {
    info!(
        ctx.logger(),
        "Starting to tail commits from id {}", start_id
    );
    let counter_name = format_counter();
    let stream_of_entries = tail_entries(
        ctx.clone(),
        start_id,
        skip_bookmarks,
        blobrepo.repo_identity().id(),
        blobrepo.bookmark_update_log_arc(),
        scuba_sample,
    );

    validate_stream(ctx, &validation_helpers, stream_of_entries)
        .then(
            |validated_entry_id_res: Result<EntryCommitId, Error>| async {
                let entry_id = validated_entry_id_res?;
                if entry_id.last_commit_for_bookmark_move() {
                    let id = entry_id.bookmarks_update_log_entry_id;
                    blobrepo
                        .mutable_counters()
                        .set_counter(ctx, &counter_name, id, None)
                        .await?;
                }

                Result::<_, Error>::Ok(())
            },
        )
        .try_for_each(|_| future::ready(Ok(())))
        .await
}

async fn run_in_once_mode(
    ctx: &CoreContext,
    blobrepo: BlobRepo,
    validation_helpers: ValidationHelpers,
    entry_id: u64,
) -> Result<(), Error> {
    let bookmark_update_log = blobrepo.bookmark_update_log();
    let entries: Vec<Result<(BookmarkUpdateLogEntry, QueueSize), Error>> = bookmark_update_log
        .read_next_bookmark_log_entries(
            ctx.clone(),
            entry_id - 1,
            1, /* limit */
            Freshness::MaybeStale,
        )
        .map_ok(|entry: BookmarkUpdateLogEntry| (entry, QueueSize(0)))
        .collect()
        .await;

    if entries.is_empty() {
        return Err(format_err!(
            "No entries for {} with id >{}",
            blobrepo.repo_identity().id(),
            entry_id - 1
        ));
    }

    let stream_of_entries = stream::iter(entries);
    validate_stream(ctx, &validation_helpers, stream_of_entries)
        .try_for_each(|_| future::ready(Ok(())))
        .await
}

async fn run<'a>(fb: FacebookInit, ctx: CoreContext, app: MononokeApp) -> Result<(), Error> {
    let env = app.environment();

    let args: MononokeCommitValidatorArgs = app.args::<MononokeCommitValidatorArgs>()?;
    let repo_arg = args.repo.as_repo_arg();
    let (_, repo_config) = app.repo_config(repo_arg)?;

    let repo: InnerRepo = app.open_repo(&args.repo).await?;
    let mysql_options = &env.mysql_options;
    let readonly_storage = env.readonly_storage;
    let scuba_sample = &env.scuba_sample_builder;
    let skip_bookmarks = repo_config
        .cross_repo_commit_validation_config
        .as_ref()
        .map_or_else(HashSet::new, |conf| conf.skip_bookmarks.clone());
    let validation_helpers = get_validation_helpers(
        fb,
        ctx.clone(),
        &app,
        repo.clone(),
        repo_config,
        mysql_options.clone(),
        readonly_storage.clone(),
        scuba_sample.clone(),
    )
    .await
    .context("While instantiating commit syncers")?;

    let blobrepo = repo.blob_repo.clone();

    let subcommand = args.subcommand;

    match subcommand {
        Once { entry_id } => run_in_once_mode(&ctx, blobrepo, validation_helpers, entry_id).await,
        Tail { start_id } => {
            let start_id = get_start_id(&ctx, &repo, start_id)
                .await
                .context("While fetching the start_id")?;

            run_in_tailing_mode(
                &ctx,
                blobrepo,
                skip_bookmarks,
                validation_helpers,
                start_id,
                scuba_sample.clone(),
            )
            .await
        }
    }
}

async fn async_main(app: MononokeApp) -> Result<()> {
    let (fb, logger) = (app.environment().fb, app.logger());
    let ctx = CoreContext::new_with_logger(fb, logger.clone());

    run(fb, ctx, app).await
}

#[fbinit::main]
fn main(fb: FacebookInit) -> Result<()> {
    let app = MononokeAppBuilder::new(fb)
        .with_app_extension(Fb303AppExtension {})
        .build::<MononokeCommitValidatorArgs>()?;

    app.run_with_monitoring_and_logging(async_main, SERVICE_NAME, AliveService)
}


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

use cross_repo_sync::types::Large;
use cross_repo_sync::types::Small;
use mononoke_types::ChangesetId;
use mononoke_types::RepositoryId;
use scuba_ext::MononokeScubaSampleBuilder;

use crate::tail::QueueSize;

const LARGE_REPO: &str = "large_repo";
const SMALL_REPO: &str = "small_repo";
const LARGE_CS_ID: &str = "large_cs_id";
const SMALL_CS_ID: &str = "small_cs_id";
const NOOP_ITERATION: &str = "noop";
const ENTRY: &str = "entry";
const VALIDATION_DURATION_MS: &str = "validation_duration_ms";
const PREPARATION_DURATION_MS: &str = "preparation_duration_ms";
const QUEUE_SIZE: &str = "queue_size";
const ERROR: &str = "error";
const SUCCESS: &str = "success";

pub fn add_common_commit_syncing_fields(
    scuba_sample: &mut MononokeScubaSampleBuilder,
    large_repo_id: Large<RepositoryId>,
    small_repo_id: Small<RepositoryId>,
) {
    scuba_sample
        .add(LARGE_REPO, large_repo_id.0.id())
        .add(SMALL_REPO, small_repo_id.0.id());
}

pub fn log_validation_result_to_scuba(
    mut scuba_sample: MononokeScubaSampleBuilder,
    entry_id: i64,
    large_cs_id: &Large<ChangesetId>,
    small_cs_id: &Small<ChangesetId>,
    error: Option<String>,
    queue_size: QueueSize,
    preparation_duration: Duration,
    validation_duration: Duration,
) {
    scuba_sample
        .add(LARGE_CS_ID, format!("{}", large_cs_id))
        .add(SMALL_CS_ID, format!("{}", small_cs_id))
        .add(NOOP_ITERATION, 0)
        .add(QUEUE_SIZE, queue_size.0)
        .add(
            VALIDATION_DURATION_MS,
            validation_duration.as_millis() as u64,
        )
        .add(
            PREPARATION_DURATION_MS,
            preparation_duration.as_millis() as u64,
        )
        .add(ENTRY, entry_id);
    match error {
        Some(error) => {
            scuba_sample.add(ERROR, error).add(SUCCESS, 0);
        }
        None => {
            scuba_sample.add(SUCCESS, 1);
        }
    };

    scuba_sample.log();
}

pub fn log_noop_iteration_to_scuba(
    mut scuba_sample: MononokeScubaSampleBuilder,
    large_repo_id: RepositoryId,
) {
    scuba_sample
        .add(NOOP_ITERATION, 1)
        .add(LARGE_REPO, large_repo_id.id())
        .add(SUCCESS, 1)
        .add(QUEUE_SIZE, 0);
    scuba_sample.log();
}


/*
 * 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::format_err;
use anyhow::Error;
use anyhow::Result;
use blobstore_factory::ReadOnlyStorage;
use bookmarks::BookmarkKey;
use borrowed::borrowed;
use context::CoreContext;
use cross_repo_sync::types::Large;
use cross_repo_sync::types::Small;
use fbinit::FacebookInit;
use futures::future::try_join_all;
use live_commit_sync_config::CfgrLiveCommitSyncConfig;
use live_commit_sync_config::LiveCommitSyncConfig;
use metaconfig_types::RepoConfig;
use mononoke_api_types::InnerRepo;
use mononoke_app::args::RepoArg;
use mononoke_app::MononokeApp;
use mutable_counters::MutableCountersRef;
use repo_identity::RepoIdentityRef;
use scuba_ext::MononokeScubaSampleBuilder;
use sql_construct::SqlConstructFromMetadataDatabaseConfig;
use sql_ext::facebook::MysqlOptions;
use synced_commit_mapping::SqlSyncedCommitMapping;

use crate::cli::MononokeCommitValidatorArgs;
use crate::reporting::add_common_commit_syncing_fields;
use crate::validation::ValidationHelpers;

pub async fn get_validation_helpers<'a>(
    fb: FacebookInit,
    ctx: CoreContext,
    app: &MononokeApp,
    large_repo: InnerRepo,
    repo_config: RepoConfig,
    mysql_options: MysqlOptions,
    readonly_storage: ReadOnlyStorage,
    scuba_sample: MononokeScubaSampleBuilder,
) -> Result<ValidationHelpers, Error> {
    let repo_id = large_repo.blob_repo.repo_identity().id();

    let config_store = app.config_store();
    let live_commit_sync_config = CfgrLiveCommitSyncConfig::new(ctx.logger(), config_store)?;
    let common_commit_sync_config = live_commit_sync_config.get_common_config(repo_id)?;

    let mapping = SqlSyncedCommitMapping::with_metadata_database_config(
        fb,
        &repo_config.storage_config.metadata,
        &mysql_options,
        readonly_storage.0,
    )?;

    let large_repo_master_bookmark =
        BookmarkKey::new(app.args::<MononokeCommitValidatorArgs>()?.master_bookmark)?;

    let validation_helper_futs =
        common_commit_sync_config
            .small_repos
            .into_keys()
            .map(|small_repo_id| {
                let large_blob_repo = large_repo.blob_repo.clone();
                borrowed!(app, scuba_sample);
                async move {
                    let scuba_sample = {
                        let mut scuba_sample = scuba_sample.clone();
                        add_common_commit_syncing_fields(
                            &mut scuba_sample,
                            Large(large_blob_repo.repo_identity().id()),
                            Small(small_repo_id),
                        );

                        scuba_sample
                    };
                    let small_repo = app.open_repo(&RepoArg::Id(small_repo_id)).await?;
                    Result::<_, Error>::Ok((
                        small_repo_id,
                        (Large(large_blob_repo), Small(small_repo), scuba_sample),
                    ))
                }
            });

    let validation_helpers = try_join_all(validation_helper_futs).await?;

    Ok(ValidationHelpers::new(
        large_repo,
        validation_helpers.into_iter().collect(),
        large_repo_master_bookmark,
        mapping,
        live_commit_sync_config,
    ))
}

pub fn format_counter() -> String {
    "x_repo_commit_validator".to_string()
}

pub async fn get_start_id<'a>(
    ctx: &CoreContext,
    repo: &impl MutableCountersRef,
    start_id: Option<u64>,
) -> Result<u64, Error> {
    match start_id {
        Some(start_id) => Ok(start_id),
        None => {
            let counter = format_counter();
            repo.mutable_counters()
                .get_counter(ctx, &counter)
                .await?
                .ok_or_else(|| format_err!("mutable counter {} is missing", counter))
                .map(|val| val as u64)
        }
    }
}


/*
 * 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::HashSet;
use std::sync::Arc;
use std::time::Duration;

use anyhow::Context;
use anyhow::Error;
use bookmarks::BookmarkKey;
use bookmarks::BookmarkUpdateLog;
use bookmarks::BookmarkUpdateLogEntry;
use bookmarks::Freshness;
use cloned::cloned;
use context::CoreContext;
use futures::future;
use futures::future::FutureExt;
use futures::stream;
use futures::stream::StreamExt;
use futures::TryStreamExt;
use mononoke_types::RepositoryId;
use scuba_ext::MononokeScubaSampleBuilder;
use slog::debug;

use crate::reporting::log_noop_iteration_to_scuba;

const SLEEP_SECS: u64 = 10;
const SINGLE_DB_QUERY_ENTRIES_LIMIT: u64 = 10;

#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct QueueSize(pub usize);

/// Adds remaining number of changesets to sync to each entry
/// The idea is to indicate the number of changesets, left to sync
/// *after* a given changeset has been synced, therefore `n-i-1`
/// For example, `[c1, c2, c3]` will turn into `[(c1, 2), (c2, 1), (c3, 0)]`
fn add_queue_sizes<T>(
    items: Vec<T>,
    initial_queue_size: usize,
) -> impl Iterator<Item = (T, QueueSize)> {
    items
        .into_iter()
        .enumerate()
        .map(move |(i, item)| (item, QueueSize(initial_queue_size - i - 1)))
}

/// Run a queue size query, consume the produced `Result` and turn it
/// into an `Option`, suitable for `unfold`
async fn query_queue_size(
    ctx: CoreContext,
    bookmark_update_log: Arc<dyn BookmarkUpdateLog>,
    current_id: u64,
) -> Result<u64, Error> {
    bookmark_update_log
        .count_further_bookmark_log_entries(ctx.clone(), current_id, None)
        .await
        .map(|queue_size| {
            debug!(ctx.logger(), "queue size query returned: {}", queue_size);
            queue_size
        })
}

/// Produce an infinite stream of `Result` from a fallible item factory
/// Two differences with the normal `unfold`:
/// - this one does not expect the item factory (`f`) to return an `Option`,
///   so there's no way to terminate a stream from within `f`
/// - this one expects `f` to return a `Result`, which is threaded downstream
///   and allows the consumer of the stream to terminate it on `Err`
/// The main motivation for this is to be able to use `?` in the item factory
fn unfold_forever<T, F, Fut, Item>(
    init: T,
    mut f: F,
) -> impl stream::Stream<Item = Result<Item, Error>>
where
    T: Copy,
    F: FnMut(T) -> Fut,
    Fut: future::Future<Output = Result<(Item, T), Error>>,
{
    stream::unfold(init, move |iteration_value| {
        f(iteration_value).then(move |result| match result {
            Ok((item, next_it_val)) => future::ready(Some((Ok(item), next_it_val))),
            Err(e) => future::ready(Some((Err(e), iteration_value))),
        })
    })
}

pub(crate) fn tail_entries(
    ctx: CoreContext,
    start_id: u64,
    skip_bookmarks: HashSet<BookmarkKey>,
    repo_id: RepositoryId,
    bookmark_update_log: Arc<dyn BookmarkUpdateLog>,
    scuba_sample: MononokeScubaSampleBuilder,
) -> impl stream::Stream<Item = Result<(BookmarkUpdateLogEntry, QueueSize), Error>> {
    unfold_forever((0, start_id), move |(iteration, current_id)| {
        cloned!(ctx, bookmark_update_log, skip_bookmarks, scuba_sample);
        async move {
            let entries: Vec<_> = bookmark_update_log
                .read_next_bookmark_log_entries(
                    ctx.clone(),
                    current_id,
                    SINGLE_DB_QUERY_ENTRIES_LIMIT,
                    Freshness::MaybeStale,
                )
                .try_collect()
                .await
                .context("While querying bookmarks_update_log")?;

            let queue_size =
                query_queue_size(ctx.clone(), bookmark_update_log.clone(), current_id).await?;

            match entries.last().map(|last_item_ref| last_item_ref.id) {
                Some(last_entry_id) => {
                    let entries: Vec<_> = entries
                        .into_iter()
                        .filter(|entry| !skip_bookmarks.contains(&entry.bookmark_name))
                        .collect();
                    debug!(
                        ctx.logger(),
                        "tail_entries generating {} new entries, queue size {}, iteration {}",
                        entries.len(),
                        (queue_size as usize) - entries.len(),
                        iteration
                    );

                    let entries_with_queue_size: std::iter::Map<_, _> =
                        add_queue_sizes(entries, queue_size as usize).map(Ok);

                    Ok((
                        stream::iter(entries_with_queue_size).boxed(),
                        (iteration + 1, last_entry_id as u64),
                    ))
                }
                None => {
                    debug!(
                        ctx.logger(),
                        "tail_entries: no more entries during iteration {}. Sleeping.", iteration
                    );
                    log_noop_iteration_to_scuba(scuba_sample, repo_id);
                    tokio::time::sleep(Duration::new(SLEEP_SECS, 0)).await;
                    Ok((stream::empty().boxed(), (iteration + 1, current_id)))
                }
            }
        }
    })
    .try_flatten()
}


/*
 * 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::collections::HashSet;
use std::fmt;
use std::fmt::Debug;
use std::sync::Arc;
use std::time::Duration;

use anyhow::format_err;
use anyhow::Error;
use blobrepo::BlobRepo;
use blobstore::Loadable;
use bookmarks::BookmarkKey;
use bookmarks::BookmarkUpdateLogEntry;
use bookmarks::BookmarksRef;
use changeset_fetcher::ChangesetFetcherArc;
use changeset_fetcher::ChangesetFetcherRef;
use cloned::cloned;
use commit_graph::CommitGraphArc;
use commit_graph::CommitGraphRef;
use context::CoreContext;
use cross_repo_sync::get_commit_sync_outcome;
use cross_repo_sync::types::Large;
use cross_repo_sync::types::Small;
use cross_repo_sync::types::Source;
use cross_repo_sync::types::Target;
use cross_repo_sync::validation::report_different;
use cross_repo_sync::CommitSyncDataProvider;
use cross_repo_sync::CommitSyncOutcome;
use futures::future;
use futures::future::try_join_all;
use futures::stream;
use futures::stream::Stream;
use futures::stream::StreamExt;
use futures::stream::TryStreamExt;
use futures::try_join;
use futures::TryFutureExt;
use futures_stats::TimedFutureExt;
use live_commit_sync_config::CfgrLiveCommitSyncConfig;
use live_commit_sync_config::LiveCommitSyncConfig;
use manifest::Diff;
use manifest::Entry;
use manifest::ManifestOps;
use mercurial_derivation::DeriveHgChangeset;
use mercurial_types::FileType;
use mercurial_types::HgFileNodeId;
use mercurial_types::HgManifestId;
use metaconfig_types::CommitSyncConfigVersion;
use metaconfig_types::CommitSyncDirection;
use mononoke_api_types::InnerRepo;
use mononoke_types::ChangesetId;
use mononoke_types::MPath;
use mononoke_types::RepositoryId;
use movers::get_movers;
use movers::Mover;
use ref_cast::RefCast;
use repo_blobstore::RepoBlobstoreRef;
use repo_identity::RepoIdentityRef;
use scuba_ext::MononokeScubaSampleBuilder;
use slog::debug;
use slog::error;
use slog::info;
use stats::prelude::*;
use synced_commit_mapping::SqlSyncedCommitMapping;

use crate::reporting::log_validation_result_to_scuba;
use crate::tail::QueueSize;

type LargeBookmarkKey = Large<BookmarkKey>;
type PathToFileNodeIdMapping = HashMap<MPath, (FileType, HgFileNodeId)>;

define_stats! {
    prefix = "mononoke.commit_validation";
    bizzare_boookmark_move: timeseries(Rate, Sum),
}

/// A struct, representing a single "validation entry":
/// a single commit in the `BookmarkUpdateLogEntry`
#[derive(Clone, PartialEq, Eq)]
pub struct EntryCommitId {
    /// A `BookmarkUpdateLogEntry` id, which introduced this commit
    pub bookmarks_update_log_entry_id: i64,
    /// An index of this commit in the "parent" `BookmarkUpdateLogEntry`
    commit_in_entry: i64,
    /// Total number of commits, introduced by the `BookmarkUpdateLogEntry`
    /// Needed to provide convenient logging and verify if commit is the
    /// last in the `BookmarkUpdateLogEntry`
    total_commits_in_entry: usize,
}

impl EntryCommitId {
    pub fn last_commit_for_bookmark_move(&self) -> bool {
        self.commit_in_entry == (self.total_commits_in_entry as i64) - 1
    }
}

impl Debug for EntryCommitId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "Entry {} ({}/{})",
            self.bookmarks_update_log_entry_id,
            // 1-indexing to be clear to humans
            self.commit_in_entry + 1,
            self.total_commits_in_entry
        )
    }
}

/// Enum, representing a change to a single `MPath` in
/// a `FullManifestDiff`
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
enum FilenodeDiffPayload {
    Added(FileType, HgFileNodeId),
    Removed,
    ChangedTo(FileType, HgFileNodeId),
}

/// A unit of change in a `FullManifestDiff`
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
struct FilenodeDiff {
    /// An `MPath`, being changed
    mpath: MPath,
    /// A change, happening to the `mpath`
    payload: FilenodeDiffPayload,
}

impl FilenodeDiff {
    fn new(mpath: MPath, payload: FilenodeDiffPayload) -> Self {
        Self { mpath, payload }
    }

    /// Build `FilenodeDiff` from `manifest::Diff`
    fn from_diff(diff: Diff<Entry<HgManifestId, (FileType, HgFileNodeId)>>) -> Option<Self> {
        use Diff::*;
        use Entry::*;
        match diff {
            // We don't care about repo root diffs!
            Added(None, _) | Removed(None, _) | Changed(None, _, _) => None,
            // We don't care about tree diffs!
            Added(_, Tree(_)) | Removed(_, Tree(_)) | Changed(_, _, Tree(_)) => None,
            Added(Some(mpath), Leaf((file_type, hg_filenode_id))) => Some(Self::new(
                mpath,
                FilenodeDiffPayload::Added(file_type, hg_filenode_id),
            )),
            Removed(Some(mpath), Leaf(_)) => Some(Self::new(mpath, FilenodeDiffPayload::Removed)),
            Changed(Some(mpath), _, Leaf((file_type, hg_filenode_id))) => Some(Self::new(
                mpath,
                FilenodeDiffPayload::ChangedTo(file_type, hg_filenode_id),
            )),
        }
    }

    fn from_added_file(added_file: (MPath, (FileType, HgFileNodeId))) -> Self {
        let (mpath, (file_type, hg_filenode_id)) = added_file;
        let payload = FilenodeDiffPayload::Added(file_type, hg_filenode_id);
        Self { mpath, payload }
    }

    pub fn apply_mover(self, mover: &Mover) -> Result<Option<Self>, Error> {
        let Self { mpath, payload } = self;
        mover(&mpath).map(|maybe_moved_mpath| {
            maybe_moved_mpath.map(|moved_mpath| Self::new(moved_mpath, payload))
        })
    }

    pub fn as_tuple(&self) -> (&MPath, &FilenodeDiffPayload) {
        let Self {
            ref mpath,
            ref payload,
        } = self;
        (mpath, payload)
    }
}

/// A set of changes between two commit manifests, computed
/// by running `ManifestOps::Diff`
type FullManifestDiff = HashSet<FilenodeDiff>;

/// An auxillary struct, containing helper methods for sync
/// validation between a large repo and a single small repo
#[derive(Clone)]
struct ValidationHelper {
    large_repo: Large<BlobRepo>,
    small_repo: Small<BlobRepo>,
    scuba_sample: MononokeScubaSampleBuilder,
    live_commit_sync_config: CfgrLiveCommitSyncConfig,
}

impl ValidationHelper {
    fn new(
        large_repo: Large<BlobRepo>,
        small_repo: Small<BlobRepo>,
        scuba_sample: MononokeScubaSampleBuilder,
        live_commit_sync_config: CfgrLiveCommitSyncConfig,
    ) -> Self {
        Self {
            large_repo,
            small_repo,
            scuba_sample,
            live_commit_sync_config,
        }
    }

    async fn get_synced_commit(
        &self,
        ctx: &CoreContext,
        hash: Large<ChangesetId>,
        mapping: &SqlSyncedCommitMapping,
    ) -> Result<Option<(Small<ChangesetId>, CommitSyncConfigVersion)>, Error> {
        let large_repo_id = self.large_repo.0.repo_identity().id();
        let small_repo_id = self.small_repo.0.repo_identity().id();

        let commit_sync_data_provider =
            CommitSyncDataProvider::Live(Arc::new(self.live_commit_sync_config.clone()));
        let maybe_commit_sync_outcome: Option<CommitSyncOutcome> = get_commit_sync_outcome(
            ctx,
            Source(large_repo_id),
            Target(small_repo_id),
            Source(hash.0),
            mapping,
            CommitSyncDirection::LargeToSmall,
            &commit_sync_data_provider,
        )
        .await?;

        use CommitSyncOutcome::*;
        Ok(match maybe_commit_sync_outcome {
            None | Some(NotSyncCandidate(_)) | Some(EquivalentWorkingCopyAncestor(_, _)) => None,
            Some(RewrittenAs(cs_id, version_name)) => Some((Small(cs_id), version_name)),
        })
    }

    fn move_full_manifest_diff_large_to_small(
        &self,
        full_manifest_diff: Large<FullManifestDiff>,
        large_to_small_mover: &Mover,
    ) -> Result<Large<FullManifestDiff>, Error> {
        let moved_fmd: Result<FullManifestDiff, Error> = full_manifest_diff
            .0
            .into_iter()
            .flat_map(|filenode_diff| filenode_diff.apply_mover(large_to_small_mover).transpose())
            .collect();
        Ok(Large(moved_fmd?))
    }

    /// Check if `payload` is a noop change against p1 of the current commit
    ///
    /// Because of differences of how Mononoke and Mercurial generate filenodes
    /// for merge commits, it is possible that one backend reuses an existing
    /// `HgFilenodeId`, while the other backend generates a new one, while using
    /// the existing one as a parent. A practical case when this may happen arises
    /// from the fact that during a push Mononoke preserves a provided filenode,
    /// while during a x-repo sync it generates a new one.
    /// So the following situation is possible:
    ///
    /// A    ->  A'
    /// |\   ->  | \
    /// B C  ->  B' C'
    ///
    /// -> represents a x-repo sync
    /// A and B share the same `HgFilenodeId` for some `MPath`, while
    /// A' and B' have different `HgFilenodeId`s. At the same time, in all
    /// cases the actual `ContentId` and `FileType` is the same!
    async fn is_true_change(
        &self,
        ctx: &CoreContext,
        repo: &BlobRepo,
        p1_root_mf_id: &HgManifestId,
        mpath: &MPath,
        payload: &FilenodeDiffPayload,
    ) -> Result<bool, Error> {
        debug!(
            ctx.logger(),
            "Checking if a change to {:?}'s filenode in {} ({:?}) relects a true change",
            mpath,
            repo.repo_identity().name(),
            payload
        );

        use FilenodeDiffPayload::*;
        match payload {
            Added(_, _) | Removed => Ok(true),
            ChangedTo(new_file_type, new_filenode_id) => {
                let maybe_entry_in_p1 = p1_root_mf_id
                    .find_entry(
                        ctx.clone(),
                        repo.repo_blobstore().clone(),
                        Some(mpath.clone()),
                    )
                    .await?;

                let (p1_file_type, p1_filenode_id) = match maybe_entry_in_p1 {
                    None => {
                        // p1 does not even have an entry at this path, definitely
                        // something fishy (we really should not have gotten ChangedTo
                        // in the first place).
                        return Err(format_err!(
                            "parent's manifest lacks a file at {:?}, but we were told the diff is Changed, not Added",
                            mpath
                        ));
                    }
                    Some(Entry::Tree(_)) => {
                        // p1 has this path as a directory, it's a true change
                        debug!(
                            ctx.logger(),
                            "change at {:?} in {} was from a tree to a file, so a true change",
                            mpath,
                            repo.repo_identity().name()
                        );
                        return Ok(true);
                    }
                    Some(Entry::Leaf((p1_file_type, p1_filenode_id))) => {
                        (p1_file_type, p1_filenode_id)
                    }
                };

                if p1_file_type != *new_file_type {
                    debug!(
                        ctx.logger(),
                        "change at {:?} in {} was to a file type: {:?}->{:?}",
                        mpath,
                        repo.repo_identity().name(),
                        p1_file_type,
                        new_file_type,
                    );
                    return Ok(true);
                }

                let (p1_content_id, new_content_id) = try_join!(
                    async {
                        let e = p1_filenode_id.load(ctx, repo.repo_blobstore()).await?;
                        Result::<_, Error>::Ok(e.content_id())
                    },
                    async {
                        let e = new_filenode_id.load(ctx, repo.repo_blobstore()).await?;
                        Result::<_, Error>::Ok(e.content_id())
                    }
                )?;

                if p1_content_id != new_content_id {
                    debug!(
                        ctx.logger(),
                        "change at {:?} in {} resolved to different ContentIds: {:?}->{:?}",
                        mpath,
                        repo.repo_identity().name(),
                        p1_content_id,
                        new_content_id,
                    );

                    Ok(true)
                } else {
                    debug!(
                        ctx.logger(),
                        "change at {:?} in {} resolved to identical ContentIds",
                        mpath,
                        repo.repo_identity().name(),
                    );

                    Ok(false)
                }
            }
        }
    }

    /// Remove noop `FilenodeDiffPayload` changes from `paths_and_payloads`
    /// See the docstring of `is_true_change` for more info.
    async fn filter_out_noop_filenode_id_changes(
        &self,
        ctx: &CoreContext,
        repo: &BlobRepo,
        cs_id: &ChangesetId,
        paths_and_payloads: Vec<(MPath, &FilenodeDiffPayload)>,
    ) -> Result<Vec<MPath>, Error> {
        let maybe_p1 = repo
            .changeset_fetcher()
            .get_parents(ctx, cs_id.clone())
            .await?
            .first()
            .cloned();

        let p1 = match maybe_p1 {
            Some(p1) => p1,
            None => {
                // `cs_id` is a root commit, such commits cannot have "fake" filenode changes
                // Just return all paths
                return Ok(paths_and_payloads.into_iter().map(|(mp, _)| mp).collect());
            }
        };

        let p1_root_mf_id = fetch_root_mf_id(ctx, repo, p1).await?;

        // Note: this loop will only ever be entered during the cases when
        // two repos have non-empty symmetric difference of their `FullManifestDiff`s
        // In other words: extremely rarely. Therefore keeping it sequential is fine
        // for now. We can improve it if it ever becomes a problem.
        let mut ret = Vec::new();
        for (mpath, payload) in paths_and_payloads {
            if self
                .is_true_change(ctx, repo, &p1_root_mf_id, &mpath, payload)
                .await?
            {
                ret.push(mpath);
            } else {
                info!(
                    ctx.logger(),
                    "Skipping a change to {:?} as the filenode change is deemed false", mpath
                );
            }
        }

        Ok(ret)
    }

    async fn filter_out_large_noop_filenode_id_changes(
        &self,
        ctx: &CoreContext,
        large_cs_id: &Large<ChangesetId>,
        paths_and_payloads: Vec<(Large<MPath>, Large<&FilenodeDiffPayload>)>,
    ) -> Result<Vec<Large<MPath>>, Error> {
        let v: Vec<_> = self
            .filter_out_noop_filenode_id_changes(
                ctx,
                &self.large_repo.0,
                &large_cs_id.0,
                paths_and_payloads
                    .into_iter()
                    .map(|(mp, pld)| (mp.0, pld.0))
                    .collect(),
            )
            .await?
            .into_iter()
            .map(Large)
            .collect();

        Ok(v)
    }

    async fn filter_out_small_noop_filenode_id_changes(
        &self,
        ctx: &CoreContext,
        small_cs_id: &Small<ChangesetId>,
        paths_and_payloads: Vec<(Small<MPath>, Small<&FilenodeDiffPayload>)>,
    ) -> Result<Vec<Small<MPath>>, Error> {
        let v: Vec<_> = self
            .filter_out_noop_filenode_id_changes(
                ctx,
                &self.small_repo.0,
                &small_cs_id.0,
                paths_and_payloads
                    .into_iter()
                    .map(|(mp, pld)| (mp.0, pld.0))
                    .collect(),
            )
            .await?
            .into_iter()
            .map(Small)
            .collect();

        Ok(v)
    }
}

/// An auxillary struct, containing helper methods for sync
/// validation a large repo and potentially multiple small repos
#[derive(Clone)]
pub struct ValidationHelpers {
    large_repo: Large<InnerRepo>,
    helpers: HashMap<Small<RepositoryId>, ValidationHelper>,
    /// The "master" bookmark in the large repo. This is needed when
    /// we are unfolding an entry, which creates a new bookmark. Such entry
    /// does not have `from_changeset_id`, so instead we using the `to_changeset_id % master`
    /// revset to unfold an entry.
    large_repo_master_bookmark: BookmarkKey,
    live_commit_sync_config: CfgrLiveCommitSyncConfig,
    mapping: SqlSyncedCommitMapping,
}

impl ValidationHelpers {
    pub fn new(
        large_repo: InnerRepo,
        helpers: HashMap<
            RepositoryId,
            (Large<BlobRepo>, Small<BlobRepo>, MononokeScubaSampleBuilder),
        >,
        large_repo_master_bookmark: BookmarkKey,
        mapping: SqlSyncedCommitMapping,
        live_commit_sync_config: CfgrLiveCommitSyncConfig,
    ) -> Self {
        Self {
            large_repo: Large(large_repo),
            helpers: helpers
                .into_iter()
                .map(|(repo_id, (large_repo, small_repo, scuba_sample))| {
                    (
                        Small(repo_id),
                        ValidationHelper::new(
                            large_repo,
                            small_repo,
                            scuba_sample,
                            live_commit_sync_config.clone(),
                        ),
                    )
                })
                .collect(),
            large_repo_master_bookmark,
            live_commit_sync_config,
            mapping,
        }
    }

    fn get_helper(&self, repo_id: &Small<RepositoryId>) -> Result<&ValidationHelper, Error> {
        self.helpers
            .get(repo_id)
            .ok_or_else(|| format_err!("Repo {} is not present in ValidationHelpers", repo_id))
    }

    fn get_small_repo(&self, repo_id: &Small<RepositoryId>) -> Result<&Small<BlobRepo>, Error> {
        let helper = self.get_helper(repo_id)?;
        Ok(&helper.small_repo)
    }

    // For a given commit, find all repos into which this commit was rewritten,
    // the resulting `ChangesetId` values, and the renamed bookmark
    async fn get_synced_commits(
        &self,
        ctx: &CoreContext,
        hash: Large<ChangesetId>,
    ) -> Result<HashMap<Small<RepositoryId>, (Small<ChangesetId>, CommitSyncConfigVersion)>, Error>
    {
        let commit_sync_outcomes = try_join_all(self.helpers.iter().map(|(repo_id, helper)| {
            let mapping = &self.mapping;
            async move {
                let maybe_synced_commit =
                    helper.get_synced_commit(ctx, hash.clone(), mapping).await?;

                Result::<_, Error>::Ok(
                    maybe_synced_commit.map(|synced_commit| (repo_id.clone(), synced_commit)),
                )
            }
        }))
        .await?
        .into_iter()
        .filter_map(std::convert::identity)
        .collect();

        debug!(
            ctx.logger(),
            "Commit {} is rewritten as follows: {:?}", hash, commit_sync_outcomes
        );
        Ok(commit_sync_outcomes)
    }

    async fn get_root_full_manifest_diff(
        ctx: &CoreContext,
        repo: &BlobRepo,
        root_mf_id: HgManifestId,
    ) -> Result<FullManifestDiff, Error> {
        let all_files = list_all_filenode_ids(ctx, repo, root_mf_id).await?;
        Ok(all_files
            .into_iter()
            .map(FilenodeDiff::from_added_file)
            .collect())
    }

    /// Produce a full manifest diff between `cs_id` and its first parent
    async fn get_full_manifest_diff(
        ctx: &CoreContext,
        repo: &BlobRepo,
        cs_id: &ChangesetId,
    ) -> Result<FullManifestDiff, Error> {
        let cs_root_mf_id_fut = fetch_root_mf_id(ctx, repo, cs_id.clone());
        let maybe_p1 = repo
            .changeset_fetcher()
            .get_parents(ctx, cs_id.clone())
            .await?
            .first()
            .cloned();

        let p1 = match maybe_p1 {
            Some(p1) => p1,
            None => {
                info!(
                    ctx.logger(),
                    "{} is a root cs. Grabbing its entire manifest", cs_id
                );
                return Self::get_root_full_manifest_diff(ctx, repo, cs_root_mf_id_fut.await?)
                    .await;
            }
        };

        let p1_root_mf_id_fut = fetch_root_mf_id(ctx, repo, p1);
        let (cs_root_mf_id, p1_root_mf_id): (HgManifestId, HgManifestId) =
            try_join!(cs_root_mf_id_fut, p1_root_mf_id_fut)?;

        let r: Vec<Result<_, Error>> = p1_root_mf_id
            .diff(ctx.clone(), repo.repo_blobstore().clone(), cs_root_mf_id)
            .filter_map(|diff| async move { diff.map(FilenodeDiff::from_diff).transpose() })
            .collect()
            .await;

        r.into_iter().collect()
    }

    async fn get_large_repo_full_manifest_diff(
        &self,
        ctx: &CoreContext,
        cs_id: &Large<ChangesetId>,
    ) -> Result<Large<FullManifestDiff>, Error> {
        Self::get_full_manifest_diff(ctx, &self.large_repo.0.blob_repo, &cs_id.0)
            .await
            .map(Large)
    }

    async fn get_small_repos_full_manifest_diffs(
        &self,
        ctx: &CoreContext,
        repo_cs_ids: HashMap<Small<RepositoryId>, (Small<ChangesetId>, CommitSyncConfigVersion)>,
    ) -> Result<
        HashMap<
            Small<RepositoryId>,
            (
                Small<ChangesetId>,
                Small<FullManifestDiff>,
                CommitSyncConfigVersion,
            ),
        >,
        Error,
    > {
        let full_manifest_diff_futures = repo_cs_ids.into_iter().map(
            |(small_repo_id, (small_cs_id, version_name))| async move {
                let small_repo = self.get_small_repo(&small_repo_id)?;
                let full_manifest_diff =
                    Self::get_full_manifest_diff(ctx, &small_repo.0, &small_cs_id.0).await?;
                Result::<_, Error>::Ok((
                    small_repo_id,
                    (small_cs_id, Small(full_manifest_diff), version_name),
                ))
            },
        );

        let full_manifest_diff: Vec<(
            Small<RepositoryId>,
            (
                Small<ChangesetId>,
                Small<FullManifestDiff>,
                CommitSyncConfigVersion,
            ),
        )> = try_join_all(full_manifest_diff_futures).await?;

        Ok(full_manifest_diff.into_iter().collect())
    }

    async fn get_large_repo_master(&self, ctx: CoreContext) -> Result<ChangesetId, Error> {
        let maybe_cs_id = self
            .large_repo
            .0
            .blob_repo
            .bookmarks()
            .get(ctx, &self.large_repo_master_bookmark)
            .await?;
        maybe_cs_id.ok_or_else(|| format_err!("No master in the large repo"))
    }

    // First returned mover is small to large, second is large to small
    async fn create_movers(
        &self,
        small_repo_id: &Small<RepositoryId>,
        version_name: &CommitSyncConfigVersion,
    ) -> Result<(Mover, Mover), Error> {
        let commit_sync_config = self
            .live_commit_sync_config
            .get_commit_sync_config_by_version(
                self.large_repo.0.blob_repo.repo_identity().id(),
                version_name,
            )
            .await?;

        let movers = get_movers(
            &commit_sync_config,
            small_repo_id.0,
            CommitSyncDirection::SmallToLarge,
        )?;
        Ok((movers.mover, movers.reverse_mover))
    }
}

/// This represents a single commit move after `BookmarkUpdateLogEntry` is unfolded
#[derive(Debug)]
pub struct CommitEntry {
    entry_id: EntryCommitId,
    bookmark_name: BookmarkKey,
    cs_id: ChangesetId,
    queue_size: QueueSize,
}

/// Given a `BookmarkUpdateLogEntry`, create a stream of `CommitEntry` objects,
/// one for each commit, introduced by this entry
/// This will return an empty stream for entry, which deletes a bookmark.
/// For entry, which creates a bookmark, an equivalent of a revset `new_book % master`
/// will be returned.
pub async fn unfold_bookmarks_update_log_entry(
    ctx: &CoreContext,
    (entry, queue_size): (BookmarkUpdateLogEntry, QueueSize),
    validation_helpers: &ValidationHelpers,
) -> Result<impl Stream<Item = Result<CommitEntry, Error>>, Error> {
    let bookmarks_update_log_entry_id = entry.id;
    let is_master_entry = entry.bookmark_name == validation_helpers.large_repo_master_bookmark;
    let master_cs_id = validation_helpers
        .get_large_repo_master(ctx.clone())
        .await?;

    info!(
        ctx.logger(),
        "BookmarkUpdateLogEntry {} will be expanded into commits", bookmarks_update_log_entry_id,
    );

    // Normally a single entry would resolve to just a few commits.
    // Even if we imagine a case, where a single entry will produce
    // a stream of a million commits (merge of a new repo or smth like this)
    // we are talking about <200bytes * 1M ~= 200Mb, which it not that big
    // of a deal
    let mut collected = match (
        entry.from_changeset_id.clone(),
        entry.to_changeset_id.clone(),
    ) {
        (_, None) => {
            // Entry deletes a bookmark. Nothing to validate
            vec![]
        }
        (None, Some(to_cs_id)) => {
            // Entry creates a bookmark (or it is a blobimport entry,
            // or it has been created by the mononoke_admin in the
            // original repo).
            // In any case, out best bet is to build up all
            // commits since LCA(to_cs_id, master)

            if is_master_entry {
                // A bizzare case, when a master bookmark is either created
                // or blobiported. I do not think we will ever observe this in
                // practice, but for completeness sake, let's just make sure
                // we verify `to_cs_id` itself. If we decided to just use
                // the revset from below, it would've excluded `to_cs_id`
                // as it is likely to be an ancestor of master.
                STATS::bizzare_boookmark_move.add_value(1);
                vec![Ok(to_cs_id)]
            } else {
                info!(
                    ctx.logger(),
                    "[{}] Creation of a new bookmark at {}, slow path.",
                    bookmarks_update_log_entry_id,
                    to_cs_id
                );
                // This might be slow. If too many bookmakrs are being created, it can be optimised
                // or we could just check to_cs_id as a best effort.
                validation_helpers
                    .large_repo
                    .commit_graph()
                    .ancestors_difference_stream(ctx, vec![to_cs_id], vec![master_cs_id])
                    .await?
                    .collect()
                    .await
            }
        }
        (Some(from_cs_id), Some(to_cs_id)) => {
            if !validation_helpers
                .large_repo
                .commit_graph()
                .is_ancestor(ctx, from_cs_id, to_cs_id)
                .await?
            {
                info!(
                    ctx.logger(),
                    "[{}] {} -> {} not a forward move, slow path.",
                    bookmarks_update_log_entry_id,
                    from_cs_id,
                    to_cs_id
                );
                // This might be slow. If too many bookmakrs are being non-FF moved, it can be optimised
                // or we could just check to_cs_id as a best effort.
                validation_helpers
                    .large_repo
                    .commit_graph()
                    .ancestors_difference_stream(
                        ctx,
                        vec![to_cs_id],
                        if is_master_entry {
                            vec![from_cs_id]
                        } else {
                            // Skip ancestors of master since validating master bookmark moves covers them.
                            vec![from_cs_id, master_cs_id]
                        },
                    )
                    .await?
                    .collect()
                    .await
            } else {
                validation_helpers
                    .large_repo
                    .commit_graph()
                    .range_stream(ctx, from_cs_id, to_cs_id)
                    .await?
                    // Drop from_cs
                    .skip(1)
                    .map(Ok)
                    .collect::<Vec<_>>()
                    .await
                    .into_iter()
                    .rev()
                    .collect()
            }
        }
    };
    // Changesets are listed top to bottom, let's reverse
    collected.reverse();

    let total_commits_in_entry = collected.len();
    info!(
        ctx.logger(),
        "BookmarkUpdateLogEntry {} has been expanded into {} commits",
        bookmarks_update_log_entry_id,
        total_commits_in_entry
    );

    let bookmark_name = entry.bookmark_name;
    Ok(stream::iter(collected).map_ok({
        cloned!(bookmark_name);
        let mut commit_in_entry = 0;
        move |cs_id| {
            commit_in_entry += 1;

            let entry_id = EntryCommitId {
                bookmarks_update_log_entry_id,
                commit_in_entry: commit_in_entry - 1,
                total_commits_in_entry,
            };

            CommitEntry {
                entry_id,
                bookmark_name: bookmark_name.clone(),
                cs_id,
                queue_size,
            }
        }
    }))
}

/// A preprocessed-entry struct, which has enough data for 2 purposes:
/// - to decide if entry `id` needs to be skipped
/// - to create an `EntryPreparedForValidation`
#[derive(Debug)]
pub struct CommitEntryWithSmallReposMapped {
    entry_id: EntryCommitId,
    bookmark_name: LargeBookmarkKey,
    cs_id: Large<ChangesetId>,
    small_repo_cs_ids: HashMap<Small<RepositoryId>, (Small<ChangesetId>, CommitSyncConfigVersion)>,
    queue_size: QueueSize,
}

pub async fn get_entry_with_small_repo_mapings(
    ctx: &CoreContext,
    entry: CommitEntry,
    validation_helpers: &ValidationHelpers,
) -> Result<Option<CommitEntryWithSmallReposMapped>, Error> {
    info!(
        ctx.logger(),
        "Mapping small cs_ids for entry {:?}; book: {}; to_cs_id: {:?}; remaining queue: {}",
        entry.entry_id,
        entry.bookmark_name,
        entry.cs_id,
        entry.queue_size.0,
    );
    let cs_id = Large(entry.cs_id.clone());

    let small_repo_cs_ids: HashMap<
        Small<RepositoryId>,
        (Small<ChangesetId>, CommitSyncConfigVersion),
    > = validation_helpers
        .get_synced_commits(ctx, cs_id.clone())
        .await?;

    Ok(Some(CommitEntryWithSmallReposMapped {
        entry_id: entry.entry_id.clone(),
        bookmark_name: Large(entry.bookmark_name),
        cs_id,
        small_repo_cs_ids,
        queue_size: entry.queue_size,
    }))
}

#[derive(Debug)]
pub struct EntryPreparedForValidation {
    pub entry_id: EntryCommitId,
    cs_id: Large<ChangesetId>,
    large_repo_full_manifest_diff: Large<FullManifestDiff>,
    small_repo_full_manifest_diffs: HashMap<
        Small<RepositoryId>,
        (
            Small<ChangesetId>,
            Small<FullManifestDiff>,
            CommitSyncConfigVersion,
        ),
    >,
    queue_size: QueueSize,
    preparation_duration: Duration,
}

pub async fn prepare_entry(
    ctx: &CoreContext,
    entry_with_small_repo_mappings: CommitEntryWithSmallReposMapped,
    validation_helpers: &ValidationHelpers,
) -> Result<Option<EntryPreparedForValidation>, Error> {
    let CommitEntryWithSmallReposMapped {
        entry_id,
        bookmark_name,
        cs_id,
        small_repo_cs_ids,
        queue_size,
    } = entry_with_small_repo_mappings;
    let cs_id = cs_id.clone();

    let before_preparation = std::time::Instant::now();
    info!(
        ctx.logger(),
        "Preparing entry {:?}; book: {}; cs_id: {:?}; remaining queue: {}",
        entry_id,
        bookmark_name,
        cs_id,
        queue_size.0,
    );

    // Note: executing the following two async operations  concurrently
    // does not really matter as we are already executing multiple
    // entry preparations at the same time
    let (large_repo_full_manifest_diff, small_repo_full_manifest_diffs) = try_join!(
        validation_helpers.get_large_repo_full_manifest_diff(ctx, &cs_id),
        validation_helpers.get_small_repos_full_manifest_diffs(ctx, small_repo_cs_ids)
    )?;

    Ok(Some(EntryPreparedForValidation {
        entry_id,
        cs_id,
        large_repo_full_manifest_diff,
        small_repo_full_manifest_diffs,
        queue_size,
        preparation_duration: before_preparation.elapsed(),
    }))
}

/// Validate that parents of a changeset in a small repo are
/// ancestors of it's equivalent in the large repo
async fn validate_topological_order<
    'a,
    R: ChangesetFetcherArc + RepoIdentityRef + CommitGraphArc,
>(
    ctx: &'a CoreContext,
    large_repo: &'a Large<R>,
    large_cs_id: Large<ChangesetId>,
    small_repo: &'a Small<R>,
    small_cs_id: Small<ChangesetId>,
    mapping: &'a SqlSyncedCommitMapping,
    commit_sync_data_provider: &'a CommitSyncDataProvider,
) -> Result<(), Error> {
    debug!(
        ctx.logger(),
        "validating topological order for {}<->{}", large_cs_id, small_cs_id
    );
    let small_repo_id = small_repo.0.repo_identity().id();
    let large_repo_id = large_repo.0.repo_identity().id();

    let small_parents = small_repo
        .0
        .changeset_fetcher()
        .get_parents(ctx, small_cs_id.0.clone())
        .await?;

    let remapped_small_parents: Vec<(ChangesetId, ChangesetId)> =
        try_join_all(small_parents.into_iter().map(|small_parent| {
            cloned!(ctx, commit_sync_data_provider);
            async move {
                let maybe_commit_sync_outcome = get_commit_sync_outcome(
                    &ctx,
                    Source(small_repo_id),
                    Target(large_repo_id),
                    Source(small_parent),
                    mapping,
                    CommitSyncDirection::SmallToLarge,
                    &commit_sync_data_provider,
                )
                .await?;

                let commit_sync_outcome = maybe_commit_sync_outcome.ok_or_else(|| {
                    format_err!(
                        "Unexpectedly missing CommitSyncOutcome for {} in {}->{}",
                        small_parent,
                        small_repo_id,
                        large_repo_id,
                    )
                })?;

                use CommitSyncOutcome::*;
                let remapping_of_small_parent = match commit_sync_outcome {
                    RewrittenAs(cs_id, _) | EquivalentWorkingCopyAncestor(cs_id, _) => cs_id,
                    NotSyncCandidate(_) => {
                        return Err(format_err!(
                            "Parent of synced {} is NotSyncCandidate in {}->{}",
                            small_cs_id,
                            small_repo_id,
                            large_repo_id,
                        ));
                    }
                };
                Ok((small_parent, remapping_of_small_parent))
            }
        }))
        .await?;

    let large_repo_commit_graph = large_repo.0.commit_graph_arc();
    try_join_all(remapped_small_parents.into_iter().map(
        |(small_parent, remapping_of_small_parent)| {
            cloned!(ctx, large_repo_commit_graph);
            async move {
                if !large_repo_commit_graph
                    .is_ancestor(&ctx, remapping_of_small_parent, large_cs_id.0.clone())
                    .await?
                {
                    Err(format_err!(
                        "{} (remapping of parent {} of {} in {}) is not an ancestor of {} in {}",
                        remapping_of_small_parent,
                        small_parent,
                        small_cs_id,
                        small_repo_id,
                        large_cs_id,
                        large_repo_id,
                    ))
                } else {
                    Ok(())
                }
            }
        },
    ))
    .await?;

    debug!(
        ctx.logger(),
        "done validating topological order for {}<->{}", large_cs_id, small_cs_id
    );
    Ok(())
}

/// Given changes to equivalent `MPath`s in two synced repos,
/// return the differences between the changes
fn compare_diffs_at_mpath(
    mpath: &MPath,
    small_payload: Small<&FilenodeDiffPayload>,
    large_payload: Large<&FilenodeDiffPayload>,
) -> (
    Option<(MPath, Small<HgFileNodeId>, Large<HgFileNodeId>)>,
    Option<(MPath, Small<FileType>, Large<FileType>)>,
    Option<(
        MPath,
        Small<FilenodeDiffPayload>,
        Large<FilenodeDiffPayload>,
    )>,
) {
    use FilenodeDiffPayload::*;
    let mut should_be_equivalent = None;
    let mut different_filetypes = None;
    let mut different_actions = None;

    match (small_payload.0, large_payload.0) {
        (Added(small_file_type, small_file_node), Added(large_file_type, large_file_node))
        | (
            ChangedTo(small_file_type, small_file_node),
            ChangedTo(large_file_type, large_file_node),
        ) => {
            if small_file_node != large_file_node {
                should_be_equivalent = Some((
                    mpath.clone(),
                    Small(small_file_node.clone()),
                    Large(large_file_node.clone()),
                ));
            }

            if small_file_type != large_file_type {
                different_filetypes = Some((
                    mpath.clone(),
                    Small(small_file_type.clone()),
                    Large(large_file_type.clone()),
                ));
            }
        }
        (Removed, Removed) => {} // both are `Removed` so there's nothing to compare
        (small_action, large_action) => {
            different_actions = Some((
                mpath.clone(),
                Small(small_action.clone()),
                Large(large_action.clone()),
            ));
        }
    }

    (should_be_equivalent, different_filetypes, different_actions)
}

/// Given full manifest diffs, check that they represent
/// equivalent sets of changes in two repos
async fn validate_full_manifest_diffs_equivalence<'a>(
    ctx: &'a CoreContext,
    validation_helper: &'a ValidationHelper,
    large_cs_id: &'a Large<ChangesetId>,
    small_cs_id: &'a Small<ChangesetId>,
    large_repo_full_manifest_diff: Large<FullManifestDiff>,
    small_repo_full_manifest_diff: Small<FullManifestDiff>,
    small_to_large_mover: Mover,
    large_to_small_mover: Mover,
) -> Result<(), Error> {
    let moved_large_repo_full_manifest_diff = validation_helper
        .move_full_manifest_diff_large_to_small(
            large_repo_full_manifest_diff,
            &large_to_small_mover,
        )?;

    // Let's remove all `FilenodeDiff` structs, which are strictly equivalent
    // Note that `in_small_but_not_in_large` and `in_large_but_not_in_small`
    // do *not* represent different `MPath`s, but `FilenodeDiff` structs. That is, it
    // is possible that there are two different `FilenodeDiff` structs in the
    // large and small repos, but they correspond to the same `MPath` and may
    // even represent the exact same `ContentId`!
    let in_small_but_not_in_large: HashMap<Small<&MPath>, Small<&FilenodeDiffPayload>> =
        small_repo_full_manifest_diff
            .0
            .difference(&moved_large_repo_full_manifest_diff.0)
            .map(|filenode_diff| {
                let (mpath, payload) = filenode_diff.as_tuple();
                (Small(mpath), Small(payload))
            })
            .collect();

    // Note that this hashmap maps small paths to large payloads. This is
    // intentional and represents the idea that paths are already moved
    let mut in_large_but_not_in_small: HashMap<Small<&MPath>, Large<&FilenodeDiffPayload>> =
        moved_large_repo_full_manifest_diff
            .0
            .difference(&small_repo_full_manifest_diff.0)
            .map(|filenode_diff| {
                let (mpath, payload) = filenode_diff.as_tuple();
                (Small(mpath), Large(payload))
            })
            .collect();

    // This is set of *small* repo changes, which do not have an equivalent
    // in the large repo
    let mut missing_in_large_repo: Vec<(Small<MPath>, Small<&FilenodeDiffPayload>)> = vec![];

    let mut should_be_equivalent: Vec<(MPath, Small<HgFileNodeId>, Large<HgFileNodeId>)> = vec![];
    let mut different_filetypes: Vec<(MPath, Small<FileType>, Large<FileType>)> = vec![];
    let mut different_actions: Vec<(
        MPath,
        Small<FilenodeDiffPayload>,
        Large<FilenodeDiffPayload>,
    )> = vec![];

    for (small_mpath, small_payload) in in_small_but_not_in_large {
        if small_to_large_mover(small_mpath.0)?.is_none() {
            // It is expected to be missing in the large repo
            continue;
        };

        match in_large_but_not_in_small.remove(&small_mpath) {
            None => {
                // `small_mpath` is present in the small repo, `small_to_large_mover`
                // does not rewrite it into `None`. Seems like a problem!
                let small_mpath: Small<MPath> = small_mpath.cloned();
                missing_in_large_repo.push((small_mpath, small_payload));
            }
            Some(large_payload) => {
                let (
                    maybe_should_be_equivalent,
                    maybe_different_filetypes,
                    maybe_different_actions,
                ) = compare_diffs_at_mpath(small_mpath.0, small_payload, large_payload);

                should_be_equivalent.extend(maybe_should_be_equivalent);
                different_filetypes.extend(maybe_different_filetypes);
                different_actions.extend(maybe_different_actions);
            }
        };
    }

    // This is a set of *large* repo changes, which do not have an equivalent
    // in the small repo. We need large paths to filter noop ones from these.
    let missing_in_small_repo: Vec<(Large<MPath>, Large<&FilenodeDiffPayload>)> =
        in_large_but_not_in_small
            .into_iter()
            .map(
                |(small_mpath, payload): (Small<&MPath>, Large<&FilenodeDiffPayload>)| {
                    // `in_large_but_not_in_small` was initially populated by small
                    // paths, so let's convert
                    let large_mpath = small_to_large_mover(small_mpath.0)?.ok_or_else(|| {
                        format_err!(
                            "{:?} unexpectedly produces None when moved small-to-large",
                            small_mpath.0
                        )
                    })?;

                    Result::<_, Error>::Ok((Large(large_mpath), payload))
                },
            )
            .collect::<Result<Vec<_>, Error>>()?;

    let missing_in_small_repo: Vec<Large<MPath>> = validation_helper
        .filter_out_large_noop_filenode_id_changes(ctx, large_cs_id, missing_in_small_repo)
        .await?;

    // Note: we populated `missing_in_small_repo` with large repo paths,
    // and `missing_in_large_repo` with small repo paths. Let's unify this
    // to small repo paths, so that we always report the same kind of path.
    let missing_in_large_repo: Vec<Large<MPath>> = validation_helper
        .filter_out_small_noop_filenode_id_changes(ctx, small_cs_id, missing_in_large_repo)
        .await?
        .into_iter()
        .map(|small_mpath: Small<MPath>| {
            let large_mpath = small_to_large_mover(&small_mpath.0)?.ok_or_else(|| {
                format_err!(
                    "{:?} surprisingly produces None when moved small-to-large",
                    small_mpath
                )
            })?;

            Ok(Large(large_mpath))
        })
        .collect::<Result<Vec<_>, Error>>()?;

    let small_name = validation_helper.small_repo.0.repo_identity().name();
    let large_name = validation_helper.large_repo.0.repo_identity().name();

    report_missing(
        ctx,
        missing_in_large_repo,
        large_cs_id,
        small_name,
        large_name,
    )?;
    report_missing(
        ctx,
        missing_in_small_repo,
        large_cs_id,
        large_name,
        small_name,
    )?;

    // In order to call fns, provided by cross_repo_sync/src/validation,
    // we are switching perspective from Large/Small to Source/Taret
    let source_cs_id = Source(large_cs_id.0);
    let source_repo = Source::ref_cast(&validation_helper.large_repo.0);
    let target_repo = Target::ref_cast(&validation_helper.small_repo.0);
    let source_name = Source(large_name);
    let target_name = Target(small_name);

    let different_filetypes = small_large_to_source_target(different_filetypes);
    report_different(
        ctx,
        different_filetypes,
        &source_cs_id,
        "filetype",
        source_name,
        target_name,
    )?;
    let different_actions = small_large_to_source_target(different_actions);
    report_different(
        ctx,
        different_actions,
        &source_cs_id,
        "action",
        source_name,
        target_name,
    )?;

    let should_be_equivalent = small_large_to_source_target(should_be_equivalent);
    verify_filenodes_have_same_contents(
        ctx,
        target_repo,
        source_repo,
        &source_cs_id,
        should_be_equivalent,
    )
    .await?;

    Ok(())
}

/// Rewrap a vector of things, assuming that `Small` is a `Target`,
/// and `Large` is a `Source`
fn small_large_to_source_target<T, P>(
    v: Vec<(T, Small<P>, Large<P>)>,
) -> Vec<(T, Source<P>, Target<P>)> {
    v.into_iter()
        .map(|(t, small, large)| (t, Source(large.0), Target(small.0)))
        .collect()
}

/// Report file changes, present in acommit in one repo,
/// but missing in a commit in another repo
fn report_missing(
    ctx: &CoreContext,
    missing_things: Vec<Large<MPath>>,
    large_cs_id: &Large<ChangesetId>,
    where_present: &str,
    where_missing: &str,
) -> Result<(), Error> {
    if !missing_things.is_empty() {
        for missing_thing in missing_things.iter().take(10) {
            debug!(
                ctx.logger(),
                "A change to {:?} is present in {}, but missing in {} (large repo cs {})",
                missing_thing,
                where_present,
                where_missing,
                large_cs_id,
            );
        }

        return Err(format_err!(
            "Found {} changes missing in {}, but present in {} (large repo cs {})",
            missing_things.len(),
            where_missing,
            where_present,
            large_cs_id,
        ));
    }

    Ok(())
}

async fn validate_in_a_single_repo(
    ctx: &CoreContext,
    validation_helper: ValidationHelper,
    large_repo: Large<BlobRepo>,
    large_cs_id: Large<ChangesetId>,
    small_cs_id: Small<ChangesetId>,
    large_repo_full_manifest_diff: Large<FullManifestDiff>,
    small_repo_full_manifest_diff: Small<FullManifestDiff>,
    small_to_large_mover: Mover,
    large_to_small_mover: Mover,
    mapping: SqlSyncedCommitMapping,
) -> Result<(), Error> {
    validate_full_manifest_diffs_equivalence(
        ctx,
        &validation_helper,
        &large_cs_id,
        &small_cs_id,
        large_repo_full_manifest_diff,
        small_repo_full_manifest_diff,
        small_to_large_mover,
        large_to_small_mover,
    )
    .await?;

    validate_topological_order(
        ctx,
        &large_repo,
        large_cs_id,
        &validation_helper.small_repo,
        small_cs_id,
        &mapping,
        &CommitSyncDataProvider::Live(Arc::new(validation_helper.live_commit_sync_config.clone())),
    )
    .await
}

pub async fn validate_entry(
    ctx: &CoreContext,
    prepared_entry: EntryPreparedForValidation,
    validation_helpers: &ValidationHelpers,
) -> Result<(), Error> {
    let EntryPreparedForValidation {
        entry_id,
        cs_id: large_cs_id,
        large_repo_full_manifest_diff,
        small_repo_full_manifest_diffs,
        queue_size,
        preparation_duration,
    } = prepared_entry;

    let small_repo_validation_futs = small_repo_full_manifest_diffs.into_iter().map(
        |(repo_id, (small_cs_id, small_repo_full_manifest_diff, version_name))| {
            cloned!(large_repo_full_manifest_diff);

            let entry_id = &entry_id;
            let large_repo = &validation_helpers.large_repo;
            let large_cs_id = &large_cs_id;

            async move {
                let validation_helper = validation_helpers
                    .helpers
                    .get(&repo_id)
                    .ok_or_else(|| format_err!("small repo {} not found", repo_id))?
                    .clone();
                let scuba_sample = validation_helper.scuba_sample.clone();
                let mapping = &validation_helpers.mapping;
                let (small_to_large_mover, large_to_small_mover) = validation_helpers
                    .create_movers(&repo_id, &version_name)
                    .await?;

                let (stats, validation_result): (_, Result<Result<(), _>, tokio::task::JoinError>) =
                    tokio::task::spawn({
                        cloned!(ctx, small_cs_id, large_cs_id, large_repo, mapping);
                        async move {
                            validate_in_a_single_repo(
                                &ctx,
                                validation_helper,
                                Large(large_repo.blob_repo.clone()),
                                large_cs_id,
                                small_cs_id,
                                large_repo_full_manifest_diff,
                                small_repo_full_manifest_diff,
                                small_to_large_mover,
                                large_to_small_mover,
                                mapping,
                            )
                            .await
                        }
                    })
                    .timed()
                    .await;

                // `validation_result` is a `Result`of `spawn`, the `Ok`
                // of which contains the `Result` of `validate_in_a_single_repo`
                let validation_result = validation_result?;
                let validation_duration = stats.completion_time;
                let maybe_error_str = match validation_result.as_ref() {
                    Ok(()) => None,
                    Err(e) => {
                        error!(
                            ctx.logger(),
                            "Error while verifying against {}: {:?}", version_name, e
                        );
                        Some(format!("{}", e))
                    }
                };
                log_validation_result_to_scuba(
                    scuba_sample,
                    entry_id.bookmarks_update_log_entry_id,
                    large_cs_id,
                    &small_cs_id,
                    maybe_error_str,
                    queue_size,
                    preparation_duration,
                    validation_duration,
                );

                validation_result.map_err(Error::from)
            }
        },
    );
    try_join_all(small_repo_validation_futs).await?;
    info!(ctx.logger(), "Validated entry: {:?}", entry_id);
    Ok(())
}

async fn fetch_root_mf_id(
    ctx: &CoreContext,
    repo: &BlobRepo,
    cs_id: ChangesetId,
) -> Result<HgManifestId, Error> {
    let hg_cs_id = repo.derive_hg_changeset(ctx, cs_id).await?;
    let changeset = hg_cs_id.load(ctx, repo.repo_blobstore()).await?;
    Ok(changeset.manifestid())
}

async fn list_all_filenode_ids(
    ctx: &CoreContext,
    repo: &BlobRepo,
    mf_id: HgManifestId,
) -> Result<PathToFileNodeIdMapping, Error> {
    let repoid = repo.repo_identity().id();
    info!(
        ctx.logger(),
        "fetching filenode ids for {:?} in {}", mf_id, repoid,
    );
    let res = mf_id
        .list_all_entries(ctx.clone(), repo.repo_blobstore().clone())
        .try_filter_map(move |(path, entry)| {
            let res = match entry {
                Entry::Leaf(leaf_payload) => path.map(|path| (path, leaf_payload)),
                Entry::Tree(_) => None,
            };
            future::ready(Ok(res))
        })
        .try_collect::<HashMap<_, _>>()
        .await?;

    debug!(
        ctx.logger(),
        "fetched {} filenode ids for {}",
        res.len(),
        repoid,
    );
    Ok(res)
}

async fn verify_filenodes_have_same_contents<
    // item is a tuple: (MPath, large filenode id, small filenode id)
    I: IntoIterator<Item = (MPath, Source<HgFileNodeId>, Target<HgFileNodeId>)>,
>(
    ctx: &CoreContext,
    target_repo: &Target<BlobRepo>,
    source_repo: &Source<BlobRepo>,
    source_hash: &Source<ChangesetId>,
    should_be_equivalent: I,
) -> Result<(), Error> {
    let fetched_content_ids = stream::iter(should_be_equivalent)
        .map({
            move |(path, source_filenode_id, target_filenode_id)| async move {
                debug!(
                    ctx.logger(),
                    "checking content for different filenodes: source {} vs target {}",
                    source_filenode_id,
                    target_filenode_id,
                );
                let f1 = async move {
                    source_filenode_id
                        .0
                        .load(ctx, source_repo.0.repo_blobstore())
                        .await
                }
                .map_ok(|e| Source(e.content_id()));
                let f2 = async move {
                    target_filenode_id
                        .0
                        .load(ctx, target_repo.0.repo_blobstore())
                        .await
                }
                .map_ok(|e| Target(e.content_id()));

                let (c1, c2) = future::try_join(f1, f2).await?;
                Result::<_, Error>::Ok((path, c1, c2))
            }
        })
        .buffered(1000)
        .try_collect::<Vec<_>>()
        .await?;

    let different_contents: Vec<_> = fetched_content_ids
        .into_iter()
        .filter(|(_mpath, c1, c2)| c1.0 != c2.0)
        .collect();

    report_different(
        ctx,
        different_contents,
        source_hash,
        "contents",
        Source(source_repo.0.repo_identity().name()),
        Target(target_repo.0.repo_identity().name()),
    )
}

#[cfg(test)]
mod tests {
    use cross_repo_sync::update_mapping_with_version;
    use cross_repo_sync_test_utils::init_small_large_repo;
    use cross_repo_sync_test_utils::xrepo_mapping_version_with_small_repo;
    use cross_repo_sync_test_utils::TestRepo;
    use fbinit::FacebookInit;
    use maplit::hashmap;
    use tests_utils::CommitIdentifier;
    use tests_utils::CreateCommitContext;
    use tokio::runtime::Runtime;

    use super::*;

    async fn add_commits_to_repo(
        ctx: &CoreContext,
        spec: Vec<HashMap<&str, &str>>,
        repo: &TestRepo,
    ) -> Result<Vec<ChangesetId>, Error> {
        let mut parent: CommitIdentifier = "master".into();
        let mut commits: Vec<ChangesetId> = vec![];
        for file_changes in spec {
            let commit = CreateCommitContext::new(ctx, repo, vec![parent])
                .add_files(file_changes)
                .commit()
                .await?;
            commits.push(commit.clone());
            parent = commit.into();
        }

        Ok(commits)
    }

    /// Initialize small and large repos, create commits according
    /// to provided specifications  and update the SyncedCommitMapping.
    /// `large_repo_commits_spec`, `small_repo_commits_spec` are specs for new commits.
    /// Each spec will trigger a linear chain of commits in its repo with
    /// appropriate file changes.
    /// `commit_index_mapping` is a mapping specification, which says:
    /// "for each pair of indices `i=>j` in this hashmap, please take
    /// i-th produced commit in the large repo and map to j'th produced
    /// commit in the small repo.
    async fn test_topological_order_validation(
        fb: FacebookInit,
        large_repo_commits_spec: Vec<HashMap<&str, &str>>,
        small_repo_commits_spec: Vec<HashMap<&str, &str>>,
        commit_index_mapping: HashMap<usize, usize>,
        large_index_to_test: usize,
        small_index_to_test: usize,
    ) -> Result<(), Error> {
        let ctx = CoreContext::test_mock(fb);
        let (syncers, _, _, _) = init_small_large_repo(&ctx).await?;
        let small_to_large_commit_syncer = syncers.small_to_large;
        let small_repo = small_to_large_commit_syncer.get_small_repo();
        let large_repo = small_to_large_commit_syncer.get_large_repo();

        let large_commits = add_commits_to_repo(&ctx, large_repo_commits_spec, large_repo).await?;
        let small_commits = add_commits_to_repo(&ctx, small_repo_commits_spec, small_repo).await?;
        let commit_mapping: HashMap<ChangesetId, ChangesetId> = commit_index_mapping
            .into_iter()
            .map(|(large_index, small_index)| {
                (
                    small_commits[small_index].clone(),
                    large_commits[large_index].clone(),
                )
            })
            .collect();

        update_mapping_with_version(
            &ctx,
            commit_mapping,
            &small_to_large_commit_syncer,
            &xrepo_mapping_version_with_small_repo(),
        )
        .await?;
        let large_repo = Large(large_repo.clone());
        let small_repo = Small(small_repo.clone());

        validate_topological_order(
            &ctx,
            &large_repo,
            Large(large_commits[large_index_to_test].clone()),
            &small_repo,
            Small(small_commits[small_index_to_test].clone()),
            &small_to_large_commit_syncer.mapping,
            small_to_large_commit_syncer.get_commit_sync_data_provider(),
        )
        .await?;

        Ok(())
    }

    #[fbinit::test]
    fn test_topological_order_validation_ok(fb: FacebookInit) -> Result<(), Error> {
        // Large repo  Mapping  Small repo
        //    2    -------------    1
        //    |                     |
        //    1                     |
        //    |                     |
        //    0    -------------    0
        //   ..                     ..
        let runtime = Runtime::new()?;
        runtime.block_on(async {
            let validation_result = test_topological_order_validation(
                fb,
                vec![
                    hashmap! {"newfile0" => "newcontent"},
                    hashmap! {"newfile1" => "newcontent"},
                    hashmap! {"newfile2" => "newcontent"},
                ],
                vec![
                    hashmap! {"newfile0" => "newcontent"},
                    hashmap! {"newfile2" => "newcontent"},
                ],
                hashmap! {0 => 0, 2 => 1},
                2,
                1,
            )
            .await;
            assert!(validation_result.is_ok());
            Ok(())
        })
    }

    #[fbinit::test]
    fn test_topological_order_validation_bad_direct_parents(fb: FacebookInit) -> Result<(), Error> {
        // Large repo  Mapping  Small repo
        //    1      --\   /--      1
        //    |         \ /         |
        //    |          X          |
        //    |         / \         |
        //    0      --/   \--      0
        //   ..                     ..
        let runtime = Runtime::new()?;
        runtime.block_on(async {
            let validation_result = test_topological_order_validation(
                fb,
                vec![
                    hashmap! {"newfile0" => "newcontent"},
                    hashmap! {"newfile1" => "newcontent"},
                ],
                vec![
                    hashmap! {"newfile0" => "newcontent"},
                    hashmap! {"newfile1" => "newcontent"},
                ],
                hashmap! {0 => 1, 1 => 0},
                0,
                1,
            )
            .await;
            assert!(validation_result.is_err());
            Ok(())
        })
    }

    #[fbinit::test]
    fn test_topological_order_validation_bad_ancestors(fb: FacebookInit) -> Result<(), Error> {
        // Large repo  Mapping  Small repo
        //    2      --\   /--      1
        //    |         \ /         |
        //    1          X          |
        //    |         / \         |
        //    0      --/   \--      0
        //   ..                     ..
        let runtime = Runtime::new()?;
        runtime.block_on(async {
            let validation_result = test_topological_order_validation(
                fb,
                vec![
                    hashmap! {"newfile0" => "newcontent"},
                    hashmap! {"newfile1" => "newcontent"},
                    hashmap! {"newfile2" => "newcontent"},
                ],
                vec![
                    hashmap! {"newfile0" => "newcontent"},
                    hashmap! {"newfile2" => "newcontent"},
                ],
                hashmap! {0 => 1, 2 => 0},
                0,
                1,
            )
            .await;
            assert!(validation_result.is_err());
            Ok(())
        })
    }

    #[fbinit::test]
    fn test_topological_order_validation_bad_unremapped_ancestors(
        fb: FacebookInit,
    ) -> Result<(), Error> {
        // Large repo  Mapping  Small repo
        //    2      ---------      1
        //    |                     |
        //    1                     |
        //    |                     |
        //    0                     0
        //   ..                     ..
        let runtime = Runtime::new()?;
        runtime.block_on(async {
            let validation_result = test_topological_order_validation(
                fb,
                vec![
                    hashmap! {"newfile0" => "newcontent"},
                    hashmap! {"newfile1" => "newcontent"},
                    hashmap! {"newfile2" => "newcontent"},
                ],
                vec![
                    hashmap! {"newfile0" => "newcontent"},
                    hashmap! {"newfile2" => "newcontent"},
                ],
                hashmap! {2 => 1},
                2,
                1,
            )
            .await;
            assert!(validation_result.is_err());
            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::HashSet;
use std::sync::Arc;

use anyhow::anyhow;
use anyhow::Error;
use bookmark_renaming::get_bookmark_renamers;
use bookmark_renaming::BookmarkRenamer;
use bookmark_renaming::BookmarkRenamers;
use bookmarks::BookmarkKey;
use live_commit_sync_config::LiveCommitSyncConfig;
use metaconfig_types::CommitSyncConfig;
use metaconfig_types::CommitSyncConfigVersion;
use metaconfig_types::CommitSyncDirection;
use metaconfig_types::CommonCommitSyncConfig;
use mononoke_types::RepositoryId;
use movers::get_movers;
use movers::Mover;
use movers::Movers;

#[derive(Clone)]
pub enum CommitSyncDataProvider {
    Live(Arc<dyn LiveCommitSyncConfig>),
}

impl CommitSyncDataProvider {
    pub async fn get_mover(
        &self,
        version: &CommitSyncConfigVersion,
        source_repo_id: RepositoryId,
        target_repo_id: RepositoryId,
    ) -> Result<Mover, Error> {
        use CommitSyncDataProvider::*;

        match self {
            Live(live_commit_sync_config) => {
                let commit_sync_config = live_commit_sync_config
                    .get_commit_sync_config_by_version(source_repo_id, version)
                    .await?;
                let common_config = live_commit_sync_config.get_common_config(source_repo_id)?;

                let Movers { mover, .. } = get_movers_from_config(
                    &common_config,
                    &commit_sync_config,
                    source_repo_id,
                    target_repo_id,
                )?;
                Ok(mover)
            }
        }
    }

    pub async fn get_reverse_mover(
        &self,
        version: &CommitSyncConfigVersion,
        source_repo_id: RepositoryId,
        target_repo_id: RepositoryId,
    ) -> Result<Mover, Error> {
        use CommitSyncDataProvider::*;

        match self {
            Live(live_commit_sync_config) => {
                let commit_sync_config = live_commit_sync_config
                    .get_commit_sync_config_by_version(source_repo_id, version)
                    .await?;
                let common_config = live_commit_sync_config.get_common_config(source_repo_id)?;

                let Movers { reverse_mover, .. } = get_movers_from_config(
                    &common_config,
                    &commit_sync_config,
                    source_repo_id,
                    target_repo_id,
                )?;
                Ok(reverse_mover)
            }
        }
    }

    pub async fn get_bookmark_renamer(
        &self,
        source_repo_id: RepositoryId,
        target_repo_id: RepositoryId,
    ) -> Result<BookmarkRenamer, Error> {
        use CommitSyncDataProvider::*;

        match self {
            Live(live_commit_sync_config) => {
                let commit_sync_config =
                    live_commit_sync_config.get_common_config(source_repo_id)?;

                let BookmarkRenamers {
                    bookmark_renamer, ..
                } = get_bookmark_renamers_from_config(
                    &commit_sync_config,
                    source_repo_id,
                    target_repo_id,
                )?;
                Ok(bookmark_renamer)
            }
        }
    }

    pub async fn get_reverse_bookmark_renamer(
        &self,
        source_repo_id: RepositoryId,
        target_repo_id: RepositoryId,
    ) -> Result<BookmarkRenamer, Error> {
        use CommitSyncDataProvider::*;

        match self {
            Live(live_commit_sync_config) => {
                let commit_sync_config =
                    live_commit_sync_config.get_common_config(source_repo_id)?;

                let BookmarkRenamers {
                    reverse_bookmark_renamer,
                    ..
                } = get_bookmark_renamers_from_config(
                    &commit_sync_config,
                    source_repo_id,
                    target_repo_id,
                )?;
                Ok(reverse_bookmark_renamer)
            }
        }
    }

    pub async fn get_small_repos_for_version(
        &self,
        repo_id: RepositoryId,
        version: &CommitSyncConfigVersion,
    ) -> Result<HashSet<RepositoryId>, Error> {
        use CommitSyncDataProvider::*;

        match self {
            Live(live_commit_sync_config) => {
                let commit_sync_config = live_commit_sync_config
                    .get_commit_sync_config_by_version(repo_id, version)
                    .await?;

                Ok(commit_sync_config.small_repos.keys().cloned().collect())
            }
        }
    }

    pub async fn version_exists(
        &self,
        repo_id: RepositoryId,
        version: &CommitSyncConfigVersion,
    ) -> Result<bool, Error> {
        match self {
            Self::Live(live_commit_sync_config) => {
                let maybe_version = live_commit_sync_config
                    .get_commit_sync_config_by_version_if_exists(repo_id, version)
                    .await?;
                Ok(maybe_version.is_some())
            }
        }
    }

    pub async fn get_common_pushrebase_bookmarks(
        &self,
        repo_id: RepositoryId,
    ) -> Result<Vec<BookmarkKey>, Error> {
        use CommitSyncDataProvider::*;

        match self {
            Live(live_commit_sync_config) => {
                let common_sync_config = live_commit_sync_config.get_common_config(repo_id)?;
                Ok(common_sync_config.common_pushrebase_bookmarks)
            }
        }
    }
}

fn get_movers_from_config(
    common_config: &CommonCommitSyncConfig,
    commit_sync_config: &CommitSyncConfig,
    source_repo_id: RepositoryId,
    target_repo_id: RepositoryId,
) -> Result<Movers, Error> {
    let (direction, small_repo_id) =
        get_direction_and_small_repo_id(common_config, source_repo_id, target_repo_id)?;
    get_movers(commit_sync_config, small_repo_id, direction)
}

fn get_bookmark_renamers_from_config(
    common_config: &CommonCommitSyncConfig,
    source_repo_id: RepositoryId,
    target_repo_id: RepositoryId,
) -> Result<BookmarkRenamers, Error> {
    let (direction, small_repo_id) =
        get_direction_and_small_repo_id(common_config, source_repo_id, target_repo_id)?;
    get_bookmark_renamers(common_config, small_repo_id, direction)
}

fn get_direction_and_small_repo_id(
    common_config: &CommonCommitSyncConfig,
    source_repo_id: RepositoryId,
    target_repo_id: RepositoryId,
) -> Result<(CommitSyncDirection, RepositoryId), Error> {
    let small_repo_id = if common_config.large_repo_id == source_repo_id
        && common_config.small_repos.contains_key(&target_repo_id)
    {
        target_repo_id
    } else if common_config.large_repo_id == target_repo_id
        && common_config.small_repos.contains_key(&source_repo_id)
    {
        source_repo_id
    } else {
        return Err(anyhow!(
            "CommitSyncMapping incompatible with source repo {:?} and target repo {:?}",
            source_repo_id,
            target_repo_id,
        ));
    };

    let direction = if source_repo_id == small_repo_id {
        CommitSyncDirection::SmallToLarge
    } else {
        CommitSyncDirection::LargeToSmall
    };

    Ok((direction, small_repo_id))
}