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

mod errors;

use std::collections::HashSet;
use std::fmt;

use abomonation_derive::Abomonation;
use anyhow::Result;
use async_trait::async_trait;
use context::CoreContext;
pub use errors::PhasesError;
use mononoke_types::ChangesetId;

#[derive(Abomonation, Clone, Copy, PartialEq, Eq, Debug)]
pub enum Phase {
    Draft,
    Public,
}

impl fmt::Display for Phase {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Phase::Draft => write!(f, "Draft"),
            Phase::Public => write!(f, "Public"),
        }
    }
}

impl From<Phase> for u32 {
    fn from(phase: Phase) -> u32 {
        match phase {
            Phase::Public => 0,
            Phase::Draft => 1,
        }
    }
}

impl TryFrom<u32> for Phase {
    type Error = PhasesError;

    fn try_from(phase_as_int: u32) -> Result<Phase, Self::Error> {
        match phase_as_int {
            0 => Ok(Phase::Public),
            1 => Ok(Phase::Draft),
            _ => Err(PhasesError::EnumError(phase_as_int)),
        }
    }
}

/// Phases tracks which commits are public, and which commits are draft.
///
/// A commit ordinarily becomes public when it is reachable from any
/// publishing bookmark.  Once public, it never becomes draft again, even
/// if the public bookmark is deleted or moved elsewhere.
#[facet::facet]
#[async_trait]
pub trait Phases: Send + Sync {
    /// Mark all commits reachable from heads as public.  Returns all
    /// the newly public commits.
    async fn add_reachable_as_public(
        &self,
        ctx: &CoreContext,
        heads: Vec<ChangesetId>,
    ) -> Result<Vec<ChangesetId>>;

    /// Add the given commits as public.  The caller is responsible
    /// for ensuring that the ancestors of all of these commits are
    /// already public, and the commits are provided in topological
    /// order.
    async fn add_public_with_known_public_ancestors(
        &self,
        ctx: &CoreContext,
        csids: Vec<ChangesetId>,
    ) -> Result<()>;

    /// Returns the commits that are public.  This method will attempt
    /// to check if any of these commits have recently become public.
    async fn get_public(
        &self,
        ctx: &CoreContext,
        csids: Vec<ChangesetId>,
        ephemeral_derive: bool,
    ) -> Result<HashSet<ChangesetId>>;

    /// Returns the commits that are known to be public in the cache.
    /// Commits that have recently become public might not be included,
    /// however this method is more performant than `get_public`.
    async fn get_cached_public(
        &self,
        ctx: &CoreContext,
        csids: Vec<ChangesetId>,
    ) -> Result<HashSet<ChangesetId>>;

    /// List all public commits.
    async fn list_all_public(&self, ctx: &CoreContext) -> Result<Vec<ChangesetId>>;

    /// Return a copy of this phases object with the set of public
    /// heads frozen.
    fn with_frozen_public_heads(&self, heads: Vec<ChangesetId>) -> ArcPhases;
}

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

    #[test]
    fn test_phase_as_integer() {
        assert_eq!(u32::from(Phase::Public), 0);
        assert_eq!(u32::from(Phase::Draft), 1);
        assert_eq!(Phase::try_from(0u32), Ok(Phase::Public));
        assert_eq!(Phase::try_from(1u32), Ok(Phase::Draft));
        assert!(Phase::try_from(2u32).is_err());
    }
}


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

use anyhow::format_err;
use anyhow::Error;
use anyhow::Result;
use blobrepo::BlobRepo;
use bonsai_hg_mapping::BonsaiHgMappingRef;
use bookmarks::BookmarkKey;
use bookmarks::BookmarkUpdateReason;
use bookmarks::BookmarksMaybeStaleExt;
use bookmarks::BookmarksRef;
use borrowed::borrowed;
use cloned::cloned;
use context::CoreContext;
use fbinit::FacebookInit;
use fixtures::Linear;
use fixtures::TestRepoFixture;
use futures::future;
use futures::TryFutureExt;
use futures::TryStreamExt;
use maplit::hashset;
use mercurial_types::nodehash::HgChangesetId;
use mononoke_types::ChangesetId;
use phases::Phases;
use phases::PhasesRef;

async fn delete_all_publishing_bookmarks(ctx: &CoreContext, repo: &BlobRepo) -> Result<(), Error> {
    let bookmarks = repo
        .bookmarks()
        .get_publishing_bookmarks_maybe_stale(ctx.clone())
        .try_collect::<Vec<_>>()
        .await?;

    let mut txn = repo.bookmarks().create_transaction(ctx.clone());

    for (bookmark, _) in bookmarks {
        txn.force_delete(bookmark.key(), BookmarkUpdateReason::TestMove)
            .unwrap();
    }

    let ok = txn.commit().await?;
    if !ok {
        return Err(Error::msg("Deletion did not commit"));
    }

    Ok(())
}

async fn set_bookmark(
    ctx: &CoreContext,
    repo: &BlobRepo,
    book: &BookmarkKey,
    cs_id: &str,
) -> Result<(), Error> {
    let head = repo
        .bonsai_hg_mapping()
        .get_bonsai_from_hg(ctx, HgChangesetId::from_str(cs_id)?)
        .await?
        .ok_or_else(|| Error::msg("cs does not exit"))?;

    let mut txn = repo.bookmarks().create_transaction(ctx.clone());
    txn.force_set(book, head, BookmarkUpdateReason::TestMove)?;

    let ok = txn.commit().await?;
    if !ok {
        return Err(Error::msg("Deletion did not commit"));
    }

    Ok(())
}

async fn is_public(
    phases: &dyn Phases,
    ctx: &CoreContext,
    csid: ChangesetId,
) -> Result<bool, Error> {
    let public = phases.get_public(ctx, vec![csid], false).await?;
    Ok(public.contains(&csid))
}

#[fbinit::test]
async fn get_phase_hint_test(fb: FacebookInit) -> Result<(), Error> {
    let repo = Linear::getrepo(fb).await;
    //  @  79a13814c5ce7330173ec04d279bf95ab3f652fb
    //  |
    //  o  a5ffa77602a066db7d5cfb9fb5823a0895717c5a
    //  |
    //  o  3c15267ebf11807f3d772eb891272b911ec68759
    //  |
    //  o  a9473beb2eb03ddb1cccc3fbaeb8a4820f9cd157
    //  |
    //  o  0ed509bf086fadcb8a8a5384dc3b550729b0fc17
    //  |
    //  o  eed3a8c0ec67b6a6fe2eb3543334df3f0b4f202b  master
    //  |
    //  o  cb15ca4a43a59acff5388cea9648c162afde8372
    //  |
    //  o  d0a361e9022d226ae52f689667bd7d212a19cfe0
    //  |
    //  o  607314ef579bd2407752361ba1b0c1729d08b281
    //  |
    //  o  3e0e761030db6e479a7fb58b12881883f9f8c63f
    //  |
    //  o  2d7d4ba9ce0a6ffd222de7785b249ead9c51c536

    let ctx = CoreContext::test_mock(fb);

    delete_all_publishing_bookmarks(&ctx, &repo).await?;

    // create a new master bookmark
    set_bookmark(
        &ctx,
        &repo,
        &BookmarkKey::new("master").unwrap(),
        "eed3a8c0ec67b6a6fe2eb3543334df3f0b4f202b",
    )
    .await?;

    let public_commit = repo
        .bonsai_hg_mapping()
        .get_bonsai_from_hg(
            &ctx,
            HgChangesetId::from_str("d0a361e9022d226ae52f689667bd7d212a19cfe0")?,
        )
        .await?
        .ok_or_else(|| Error::msg("Invalid cs: public_commit"))?;

    let other_public_commit = repo
        .bonsai_hg_mapping()
        .get_bonsai_from_hg(
            &ctx,
            HgChangesetId::from_str("2d7d4ba9ce0a6ffd222de7785b249ead9c51c536")?,
        )
        .await?
        .ok_or_else(|| Error::msg("Invalid cs: other_public_commit"))?;

    let draft_commit = repo
        .bonsai_hg_mapping()
        .get_bonsai_from_hg(
            &ctx,
            HgChangesetId::from_str("a9473beb2eb03ddb1cccc3fbaeb8a4820f9cd157")?,
        )
        .await?
        .ok_or_else(|| Error::msg("Invalid cs: draft_commit"))?;

    let other_draft_commit = repo
        .bonsai_hg_mapping()
        .get_bonsai_from_hg(
            &ctx,
            HgChangesetId::from_str("a5ffa77602a066db7d5cfb9fb5823a0895717c5a")?,
        )
        .await?
        .ok_or_else(|| Error::msg("Invalid cs: other_draft_commit"))?;

    let public_bookmark_commit = repo
        .bonsai_hg_mapping()
        .get_bonsai_from_hg(
            &ctx,
            HgChangesetId::from_str("eed3a8c0ec67b6a6fe2eb3543334df3f0b4f202b")?,
        )
        .await?
        .ok_or_else(|| Error::msg("Invalid cs: public_bookmark_commit"))?;

    let phases = repo.phases();

    assert!(
        is_public(phases, &ctx, public_bookmark_commit).await?,
        "slow path: get phase for a Public commit which is also a public bookmark"
    );

    assert!(
        is_public(phases, &ctx, public_commit).await?,
        "slow path: get phase for a Public commit"
    );

    assert!(
        is_public(phases, &ctx, other_public_commit).await?,
        "slow path: get phase for other Public commit"
    );

    assert!(
        !is_public(phases, &ctx, draft_commit).await?,
        "slow path: get phase for a Draft commit"
    );

    assert!(
        !is_public(phases, &ctx, other_draft_commit).await?,
        "slow path: get phase for other Draft commit"
    );

    let public = phases
        .get_public(
            &ctx,
            vec![
                public_commit,
                other_public_commit,
                draft_commit,
                other_draft_commit,
            ],
            false,
        )
        .await?;

    assert_eq!(
        public,
        hashset! {
            public_commit,
            other_public_commit,
        },
        "slow path: get phases for set of commits"
    );

    assert!(
        is_public(phases, &ctx, public_commit).await?,
        "sql: make sure that phase was written to the db for public commit"
    );

    assert!(
        !is_public(phases, &ctx, draft_commit).await?,
        "sql: make sure that phase was not written to the db for draft commit"
    );

    Ok(())
}

#[fbinit::test]
async fn test_mark_reachable_as_public(fb: FacebookInit) -> Result<()> {
    let repo = fixtures::BranchEven::getrepo(fb).await;
    // @  4f7f3fd428bec1a48f9314414b063c706d9c1aed (6)
    // |
    // o  b65231269f651cfe784fd1d97ef02a049a37b8a0 (5)
    // |
    // o  d7542c9db7f4c77dab4b315edd328edf1514952f (4)
    // |
    // | o  16839021e338500b3cf7c9b871c8a07351697d68 (3)
    // | |
    // | o  1d8a907f7b4bf50c6a09c16361e2205047ecc5e5 (2)
    // | |
    // | o  3cda5c78aa35f0f5b09780d971197b51cad4613a (1)
    // |/
    // |
    // o  15c40d0abc36d47fb51c8eaec51ac7aad31f669c (0)
    let hgcss = [
        "15c40d0abc36d47fb51c8eaec51ac7aad31f669c",
        "3cda5c78aa35f0f5b09780d971197b51cad4613a",
        "1d8a907f7b4bf50c6a09c16361e2205047ecc5e5",
        "16839021e338500b3cf7c9b871c8a07351697d68",
        "d7542c9db7f4c77dab4b315edd328edf1514952f",
        "b65231269f651cfe784fd1d97ef02a049a37b8a0",
        "4f7f3fd428bec1a48f9314414b063c706d9c1aed",
    ];
    let ctx = CoreContext::test_mock(fb);

    borrowed!(ctx, repo);

    delete_all_publishing_bookmarks(ctx, repo).await?;

    // resolve bonsai
    let bcss = future::try_join_all(
        hgcss
            .iter()
            .map({
                |hgcs| async move {
                    let bcs = repo
                        .bonsai_hg_mapping()
                        .get_bonsai_from_hg(ctx, HgChangesetId::from_str(hgcs)?)
                        .await?
                        .ok_or_else(|| format_err!("Invalid hgcs: {}", hgcs))?;
                    Result::<_, Error>::Ok(bcs)
                }
            })
            .collect::<Vec<_>>(),
    )
    .await?;

    let phases = repo.phases();
    // get phases mapping for all `bcss` in the same order
    let get_phases_map = || {
        phases.get_public(ctx, bcss.clone(), false).map_ok({
            cloned!(bcss);
            move |public| {
                bcss.iter()
                    .map(|bcs| public.contains(bcs))
                    .collect::<Vec<_>>()
            }
        })
    };

    // all phases are draft
    assert_eq!(get_phases_map().await?, [false; 7]);

    phases.add_reachable_as_public(ctx, vec![bcss[1]]).await?;
    assert_eq!(
        get_phases_map().await?,
        [true, true, false, false, false, false, false],
    );

    phases
        .add_reachable_as_public(ctx, vec![bcss[2], bcss[5]])
        .await?;
    assert_eq!(
        get_phases_map().await?,
        [true, true, true, false, true, true, false],
    );

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

#[cfg(fbcode_build)]
mod facebook;
mod local;

use std::collections::HashMap;
use std::collections::HashSet;

use bookmarks_movement::BookmarkKindRestrictions;
use bookmarks_movement::BookmarkMovementError;
use bookmarks_types::BookmarkKey;
use bytes::Bytes;
#[cfg(fbcode_build)]
pub use facebook::land_service::override_certificate_paths as land_service_override_certificate_paths;
#[cfg(fbcode_build)]
pub use facebook::land_service::LandServicePushrebaseClient;
use hooks::CrossRepoPushSource;
pub use local::LocalPushrebaseClient;
use mononoke_types::BonsaiChangeset;
use pushrebase::PushrebaseOutcome;

#[async_trait::async_trait]
/// This trait provides an abstraction for pushrebase, which can be used to allow
/// pushrebase to happen remotely.
pub trait PushrebaseClient: Sync + Send {
    /// Pushrebase the given changesets to the given bookmark.
    async fn pushrebase(
        &self,
        bookmark: &BookmarkKey,
        // Must be a stack
        changesets: HashSet<BonsaiChangeset>,
        pushvars: Option<&HashMap<String, Bytes>>,
        cross_repo_push_source: CrossRepoPushSource,
        bookmark_restrictions: BookmarkKindRestrictions,
        log_new_public_commits_to_scribe: bool,
    ) -> Result<PushrebaseOutcome, BookmarkMovementError>;
}


/*
 * 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 bookmarks::BookmarkKey;
use bookmarks_movement::BookmarkKindRestrictions;
use bookmarks_movement::BookmarkMovementError;
use bookmarks_movement::PushrebaseOntoBookmarkOp;
use bookmarks_movement::Repo;
use bytes::Bytes;
use context::CoreContext;
use hooks::CrossRepoPushSource;
use hooks::HookManager;
use mononoke_types::BonsaiChangeset;
use pushrebase::PushrebaseOutcome;
use repo_authorization::AuthorizationContext;

use crate::PushrebaseClient;

pub struct LocalPushrebaseClient<'a, R: Repo> {
    pub ctx: &'a CoreContext,
    pub authz: &'a AuthorizationContext,
    pub repo: &'a R,
    pub hook_manager: &'a HookManager,
}

#[async_trait::async_trait]
impl<'a, R: Repo> PushrebaseClient for LocalPushrebaseClient<'a, R> {
    async fn pushrebase(
        &self,
        bookmark: &BookmarkKey,
        changesets: HashSet<BonsaiChangeset>,
        pushvars: Option<&HashMap<String, Bytes>>,
        cross_repo_push_source: CrossRepoPushSource,
        bookmark_restrictions: BookmarkKindRestrictions,
        log_new_public_commits_to_scribe: bool,
    ) -> Result<PushrebaseOutcome, BookmarkMovementError> {
        let mut op = PushrebaseOntoBookmarkOp::new(bookmark, changesets)
            .with_pushvars(pushvars)
            .with_push_source(cross_repo_push_source)
            .with_bookmark_restrictions(bookmark_restrictions);
        if log_new_public_commits_to_scribe {
            op = op.log_new_public_commits_to_scribe();
        }
        op.run(self.ctx, self.authz, self.repo, self.hook_manager)
            .await
    }
}


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

use std::collections::HashMap;

use anyhow::Error;
use async_trait::async_trait;
use bookmarks::BookmarkTransactionError;
use context::CoreContext;
use mononoke_types::BonsaiChangesetMut;
use mononoke_types::ChangesetId;
use mononoke_types::Timestamp;
use sql::Transaction;

pub type RebasedChangesets = HashMap<ChangesetId, (ChangesetId, Timestamp)>;

#[async_trait]
pub trait PushrebaseHook: Send + Sync + 'static {
    /// in_critical_section is called once per pushrebase attempt, right after loading the target
    /// bookmark for pushrebase. It should return a PushrebaseCommitHook for further processing of
    /// the changeset generated by pushrebase.
    /// It can assume no more commits will land to this bookmark until the rest of the pushrebase completes.
    async fn in_critical_section(&self) -> Result<Box<dyn PushrebaseCommitHook>, Error>;
}

#[async_trait]
pub trait PushrebaseCommitHook: Send + Sync + 'static {
    /// post_rebase_changeset is called once per pushrebased changeset, with the ID of the
    /// changeset that was rebased, and a mutable reference to the resulting changeset. This can be
    /// used to modify the pushrebased commit.
    fn post_rebase_changeset(
        &mut self,
        _bcs_old: ChangesetId,
        _bcs_new: &mut BonsaiChangesetMut,
    ) -> Result<(), Error> {
        Ok(())
    }

    /// into_transaction_hook is called after all commits have been pushrebased and we're going to
    /// attempt to move the bookmark to pushrebase onto. This should capture any data necessary and
    /// return a PushrebaseTransactionHook.
    async fn into_transaction_hook(
        self: Box<Self>,
        _ctx: &CoreContext,
        changesets: &RebasedChangesets,
    ) -> Result<Box<dyn PushrebaseTransactionHook>, Error>;
}

#[async_trait]
pub trait PushrebaseTransactionHook: Send + Sync + 'static {
    /// populate_transaction is called once per attempt to update bookmarks in the DB (the
    /// operation might be retried if the write for reasons that are NOT another push having won
    /// the race). This can add extra writes to the transaction if desired.
    async fn populate_transaction(
        &self,
        ctx: &CoreContext,
        txn: Transaction,
    ) -> Result<Transaction, BookmarkTransactionError>;
}