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

use anyhow::format_err;
use anyhow::Error;
use context::CoreContext;
use fbinit::FacebookInit;
use filenodes::FilenodeInfo;
use filenodes::FilenodeRange;
use filenodes::FilenodeResult;
use filenodes::PreparedFilenode;
use maplit::hashmap;
use mercurial_types::HgFileNodeId;
use mercurial_types_mocks::nodehash::ONES_CSID;
use mercurial_types_mocks::nodehash::ONES_FNID;
use mercurial_types_mocks::nodehash::THREES_CSID;
use mercurial_types_mocks::nodehash::THREES_FNID;
use mercurial_types_mocks::nodehash::TWOS_CSID;
use mercurial_types_mocks::nodehash::TWOS_FNID;
use mononoke_types::MPath;
use mononoke_types::RepoPath;
use mononoke_types::RepositoryId;
use mononoke_types_mocks::repo::REPO_ONE;
use mononoke_types_mocks::repo::REPO_ZERO;
use sql::Connection;
use sql_ext::mononoke_queries;
use tunables::with_tunables;
use tunables::MononokeTunables;
use vec1::vec1;
use vec1::Vec1;

use super::util::build_reader_writer;
use super::util::build_shard;
use crate::builder::SQLITE_INSERT_CHUNK_SIZE;
use crate::local_cache::LocalCache;
use crate::reader::FilenodesReader;
use crate::writer::FilenodesWriter;

async fn check_roundtrip(
    ctx: &CoreContext,
    repo_id: RepositoryId,
    reader: Arc<FilenodesReader>,
    writer: &FilenodesWriter,
    payload: PreparedFilenode,
) -> Result<(), Error> {
    assert_eq!(
        async {
            let res = reader
                .clone()
                .get_filenode(ctx, repo_id, &payload.path, payload.info.filenode)
                .await?;
            res.do_not_handle_disabled_filenodes()
        }
        .await?,
        None
    );

    writer
        .insert_filenodes(ctx, repo_id, vec![payload.clone()], false)
        .await?
        .do_not_handle_disabled_filenodes()?;

    assert_eq!(
        async {
            let res = reader
                .get_filenode(ctx, repo_id, &payload.path, payload.info.filenode)
                .await?;
            res.do_not_handle_disabled_filenodes()
        }
        .await?,
        Some(payload.info),
    );

    Ok(())
}

#[fbinit::test]
async fn test_basic(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);

    let shard = build_shard()?;
    let (reader, writer) = build_reader_writer(vec1![shard]);
    let reader = Arc::new(reader);

    let payload = PreparedFilenode {
        path: RepoPath::FilePath(MPath::new(b"test")?),
        info: FilenodeInfo {
            filenode: ONES_FNID,
            p1: Some(TWOS_FNID),
            p2: None,
            copyfrom: None,
            linknode: ONES_CSID,
        },
    };

    check_roundtrip(&ctx, REPO_ZERO, reader, &writer, payload).await?;

    Ok(())
}

#[fbinit::test]
async fn read_copy_info(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);

    let shard = build_shard()?;
    let (reader, writer) = build_reader_writer(vec1![shard]);
    let reader = Arc::new(reader);

    let from = PreparedFilenode {
        path: RepoPath::FilePath(MPath::new(b"from")?),
        info: FilenodeInfo {
            filenode: ONES_FNID,
            p1: None,
            p2: None,
            copyfrom: None,
            linknode: ONES_CSID,
        },
    };

    writer
        .insert_filenodes(&ctx, REPO_ZERO, vec![from.clone()], false)
        .await?
        .do_not_handle_disabled_filenodes()?;

    let payload = PreparedFilenode {
        path: RepoPath::FilePath(MPath::new(b"test")?),
        info: FilenodeInfo {
            filenode: TWOS_FNID,
            p1: None,
            p2: None,
            copyfrom: Some((from.path.clone(), from.info.filenode)),
            linknode: TWOS_CSID,
        },
    };

    check_roundtrip(&ctx, REPO_ZERO, reader, &writer, payload).await?;

    Ok(())
}

#[fbinit::test]
async fn test_repo_ids(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);

    let shard = build_shard()?;
    let (reader, writer) = build_reader_writer(vec1![shard]);
    let reader = Arc::new(reader);

    let payload = root_first_filenode();

    writer
        .insert_filenodes(&ctx, REPO_ZERO, vec![payload.clone()], false)
        .await?
        .do_not_handle_disabled_filenodes()?;

    assert_filenode(
        &ctx,
        reader.clone(),
        &payload.path,
        payload.info.filenode,
        REPO_ZERO,
        payload.info.clone(),
    )
    .await?;

    assert_no_filenode(&ctx, reader, &payload.path, payload.info.filenode, REPO_ONE).await?;

    Ok(())
}

mononoke_queries! {
    write DeleteCopyInfo() {
        none,
        "DELETE FROM fixedcopyinfo"
    }

    write DeletePaths() {
        none,
        "DELETE FROM paths"
    }
}

#[fbinit::test]
async fn test_fallback_on_missing_copy_info(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);

    let master = build_shard()?;
    let replica = build_shard()?;

    // Populate both master and replica with the same filenodes (to simulate replication).
    FilenodesWriter::new(
        SQLITE_INSERT_CHUNK_SIZE,
        vec1![master.clone()],
        vec1![master.clone()],
    )
    .insert_filenodes(
        &ctx,
        REPO_ZERO,
        vec![copied_from_filenode(), copied_filenode()],
        false,
    )
    .await?
    .do_not_handle_disabled_filenodes()?;

    FilenodesWriter::new(
        SQLITE_INSERT_CHUNK_SIZE,
        vec1![replica.clone()],
        vec1![replica.clone()],
    )
    .insert_filenodes(
        &ctx,
        REPO_ZERO,
        vec![copied_from_filenode(), copied_filenode()],
        false,
    )
    .await?
    .do_not_handle_disabled_filenodes()?;

    // Now, delete the copy info from the replica.
    DeleteCopyInfo::query(&replica).await?;

    let reader = Arc::new(FilenodesReader::new(vec1![replica], vec1![master]));
    let prepared = copied_filenode();
    assert_filenode(
        &ctx,
        reader,
        &prepared.path,
        prepared.info.filenode,
        REPO_ZERO,
        prepared.info.clone(),
    )
    .await?;

    Ok(())
}

#[fbinit::test]
async fn test_fallback_on_missing_paths(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);

    let master = build_shard()?;
    let replica = build_shard()?;

    // Populate both master and replica with the same filenodes (to simulate replication).
    FilenodesWriter::new(
        SQLITE_INSERT_CHUNK_SIZE,
        vec1![master.clone()],
        vec1![master.clone()],
    )
    .insert_filenodes(
        &ctx,
        REPO_ZERO,
        vec![copied_from_filenode(), copied_filenode()],
        false,
    )
    .await?
    .do_not_handle_disabled_filenodes()?;

    FilenodesWriter::new(
        SQLITE_INSERT_CHUNK_SIZE,
        vec1![replica.clone()],
        vec1![replica.clone()],
    )
    .insert_filenodes(
        &ctx,
        REPO_ZERO,
        vec![copied_from_filenode(), copied_filenode()],
        false,
    )
    .await?
    .do_not_handle_disabled_filenodes()?;

    // Now, delete the copy info from the replica.
    DeletePaths::query(&replica).await?;

    let reader = Arc::new(FilenodesReader::new(vec1![replica], vec1![master]));
    let prepared = copied_filenode();
    assert_filenode(
        &ctx,
        reader,
        &prepared.path,
        prepared.info.filenode,
        REPO_ZERO,
        prepared.info.clone(),
    )
    .await?;

    Ok(())
}

fn root_first_filenode() -> PreparedFilenode {
    PreparedFilenode {
        path: RepoPath::root(),
        info: FilenodeInfo {
            filenode: ONES_FNID,
            p1: None,
            p2: None,
            copyfrom: None,
            linknode: ONES_CSID,
        },
    }
}

fn root_second_filenode() -> PreparedFilenode {
    PreparedFilenode {
        path: RepoPath::root(),
        info: FilenodeInfo {
            filenode: TWOS_FNID,
            p1: Some(ONES_FNID),
            p2: None,
            copyfrom: None,
            linknode: TWOS_CSID,
        },
    }
}

fn root_merge_filenode() -> PreparedFilenode {
    PreparedFilenode {
        path: RepoPath::root(),
        info: FilenodeInfo {
            filenode: THREES_FNID,
            p1: Some(ONES_FNID),
            p2: Some(TWOS_FNID),
            copyfrom: None,
            linknode: THREES_CSID,
        },
    }
}

fn dir_a_first_filenode() -> PreparedFilenode {
    PreparedFilenode {
        path: RepoPath::dir("a").unwrap(),
        info: FilenodeInfo {
            filenode: ONES_FNID,
            p1: None,
            p2: None,
            copyfrom: None,
            linknode: ONES_CSID,
        },
    }
}

fn file_a_first_filenode() -> PreparedFilenode {
    PreparedFilenode {
        path: RepoPath::file("a").unwrap(),
        info: FilenodeInfo {
            filenode: ONES_FNID,
            p1: None,
            p2: None,
            copyfrom: None,
            linknode: ONES_CSID,
        },
    }
}

fn file_b_first_filenode() -> PreparedFilenode {
    PreparedFilenode {
        path: RepoPath::file("b").unwrap(),
        info: FilenodeInfo {
            filenode: TWOS_FNID,
            p1: None,
            p2: None,
            copyfrom: None,
            linknode: TWOS_CSID,
        },
    }
}

fn copied_from_filenode() -> PreparedFilenode {
    PreparedFilenode {
        path: RepoPath::file("copiedfrom").unwrap(),
        info: FilenodeInfo {
            filenode: ONES_FNID,
            p1: None,
            p2: None,
            copyfrom: None,
            linknode: TWOS_CSID,
        },
    }
}

fn copied_filenode() -> PreparedFilenode {
    PreparedFilenode {
        path: RepoPath::file("copiedto").unwrap(),
        info: FilenodeInfo {
            filenode: TWOS_FNID,
            p1: None,
            p2: None,
            copyfrom: Some((RepoPath::file("copiedfrom").unwrap(), ONES_FNID)),
            linknode: TWOS_CSID,
        },
    }
}

async fn do_add_filenodes(
    ctx: &CoreContext,
    writer: &FilenodesWriter,
    to_insert: Vec<PreparedFilenode>,
    repo_id: RepositoryId,
) -> Result<(), Error> {
    writer
        .insert_filenodes(ctx, repo_id, to_insert, false)
        .await?
        .do_not_handle_disabled_filenodes()?;
    Ok(())
}

async fn do_add_filenode(
    ctx: &CoreContext,
    writer: &FilenodesWriter,
    node: PreparedFilenode,
    repo_id: RepositoryId,
) -> Result<(), Error> {
    do_add_filenodes(ctx, writer, vec![node], repo_id).await?;
    Ok(())
}

async fn assert_no_filenode(
    ctx: &CoreContext,
    reader: Arc<FilenodesReader>,
    path: &RepoPath,
    hash: HgFileNodeId,
    repo_id: RepositoryId,
) -> Result<(), Error> {
    let res = reader.get_filenode(ctx, repo_id, path, hash).await?;
    let res = res.do_not_handle_disabled_filenodes()?;
    assert!(res.is_none());
    Ok(())
}

async fn assert_filenode(
    ctx: &CoreContext,
    reader: Arc<FilenodesReader>,
    path: &RepoPath,
    hash: HgFileNodeId,
    repo_id: RepositoryId,
    expected: FilenodeInfo,
) -> Result<(), Error> {
    let res = reader
        .get_filenode(ctx, repo_id, path, hash)
        .await?
        .do_not_handle_disabled_filenodes()?
        .ok_or_else(|| format_err!("not found: {}", hash))?;
    assert_eq!(res, expected);
    Ok(())
}

async fn assert_all_filenodes(
    ctx: &CoreContext,
    reader: Arc<FilenodesReader>,
    path: &RepoPath,
    repo_id: RepositoryId,
    expected: &Vec<FilenodeInfo>,
    limit: Option<u64>,
) -> Result<(), Error> {
    let res = reader
        .get_all_filenodes_for_path(ctx, repo_id, path, limit)
        .await?;
    match res.do_not_handle_disabled_filenodes()? {
        FilenodeRange::Filenodes(ref filenodes) => assert_eq!(filenodes, expected),
        FilenodeRange::TooBig => panic!("Unexpected FilenodeRange::TooBig"),
    }
    Ok(())
}

macro_rules! filenodes_tests {
    ($test_suite_name:ident, $create_db:ident, $enable_caching:ident) => {
        mod $test_suite_name {
            use fbinit::FacebookInit;

            use super::*;

            #[fbinit::test]
            async fn test_simple_filenode_insert_and_get(fb: FacebookInit) -> Result<(), Error> {
                let ctx = CoreContext::test_mock(fb);
                let (mut reader, writer) = build_reader_writer($create_db()?);
                $enable_caching(&mut reader);
                let reader = Arc::new(reader);

                do_add_filenode(&ctx, &writer, root_first_filenode(), REPO_ZERO).await?;

                assert_filenode(
                    &ctx,
                    reader.clone(),
                    &RepoPath::root(),
                    ONES_FNID,
                    REPO_ZERO,
                    root_first_filenode().info,
                )
                .await?;

                assert_no_filenode(
                    &ctx,
                    reader.clone(),
                    &RepoPath::root(),
                    TWOS_FNID,
                    REPO_ZERO,
                )
                .await?;

                assert_no_filenode(&ctx, reader, &RepoPath::root(), ONES_FNID, REPO_ONE).await?;

                Ok(())
            }

            #[fbinit::test]
            async fn test_insert_identical_in_batch(fb: FacebookInit) -> Result<(), Error> {
                let ctx = CoreContext::test_mock(fb);
                let (_reader, writer) = build_reader_writer($create_db()?);
                do_add_filenodes(
                    &ctx,
                    &writer,
                    vec![root_first_filenode(), root_first_filenode()],
                    REPO_ZERO,
                )
                .await?;
                Ok(())
            }

            #[fbinit::test]
            async fn test_filenode_insert_twice(fb: FacebookInit) -> Result<(), Error> {
                let ctx = CoreContext::test_mock(fb);
                let (_reader, writer) = build_reader_writer($create_db()?);
                do_add_filenode(&ctx, &writer, root_first_filenode(), REPO_ZERO).await?;
                do_add_filenode(&ctx, &writer, root_first_filenode(), REPO_ZERO).await?;
                Ok(())
            }

            #[fbinit::test]
            async fn test_insert_filenode_with_parent(fb: FacebookInit) -> Result<(), Error> {
                let ctx = CoreContext::test_mock(fb);
                let (mut reader, writer) = build_reader_writer($create_db()?);
                $enable_caching(&mut reader);
                let reader = Arc::new(reader);
                do_add_filenode(&ctx, &writer, root_first_filenode(), REPO_ZERO).await?;
                do_add_filenode(&ctx, &writer, root_second_filenode(), REPO_ZERO).await?;
                assert_filenode(
                    &ctx,
                    reader.clone(),
                    &RepoPath::root(),
                    ONES_FNID,
                    REPO_ZERO,
                    root_first_filenode().info,
                )
                .await?;
                assert_filenode(
                    &ctx,
                    reader,
                    &RepoPath::root(),
                    TWOS_FNID,
                    REPO_ZERO,
                    root_second_filenode().info,
                )
                .await?;
                Ok(())
            }

            #[fbinit::test]
            async fn test_insert_root_filenode_with_two_parents(
                fb: FacebookInit,
            ) -> Result<(), Error> {
                let ctx = CoreContext::test_mock(fb);
                let (mut reader, writer) = build_reader_writer($create_db()?);
                $enable_caching(&mut reader);
                let reader = Arc::new(reader);
                do_add_filenode(&ctx, &writer, root_first_filenode(), REPO_ZERO).await?;
                do_add_filenode(&ctx, &writer, root_second_filenode(), REPO_ZERO).await?;
                do_add_filenode(&ctx, &writer, root_merge_filenode(), REPO_ZERO).await?;
                assert_filenode(
                    &ctx,
                    reader,
                    &RepoPath::root(),
                    THREES_FNID,
                    REPO_ZERO,
                    root_merge_filenode().info,
                )
                .await?;
                Ok(())
            }

            #[fbinit::test]
            async fn test_insert_file_filenode(fb: FacebookInit) -> Result<(), Error> {
                let ctx = CoreContext::test_mock(fb);
                let (mut reader, writer) = build_reader_writer($create_db()?);
                $enable_caching(&mut reader);
                let reader = Arc::new(reader);
                do_add_filenode(&ctx, &writer, file_a_first_filenode(), REPO_ZERO).await?;
                do_add_filenode(&ctx, &writer, file_b_first_filenode(), REPO_ZERO).await?;

                assert_no_filenode(
                    &ctx,
                    reader.clone(),
                    &RepoPath::file("non-existent").unwrap(),
                    ONES_FNID,
                    REPO_ZERO,
                )
                .await?;
                assert_filenode(
                    &ctx,
                    reader.clone(),
                    &RepoPath::file("a").unwrap(),
                    ONES_FNID,
                    REPO_ZERO,
                    file_a_first_filenode().info,
                )
                .await?;
                assert_filenode(
                    &ctx,
                    reader,
                    &RepoPath::file("b").unwrap(),
                    TWOS_FNID,
                    REPO_ZERO,
                    file_b_first_filenode().info,
                )
                .await?;
                Ok(())
            }

            #[fbinit::test]
            async fn test_insert_different_repo(fb: FacebookInit) -> Result<(), Error> {
                let ctx = CoreContext::test_mock(fb);
                let (mut reader, writer) = build_reader_writer($create_db()?);
                $enable_caching(&mut reader);
                let reader = Arc::new(reader);
                do_add_filenode(&ctx, &writer, root_first_filenode(), REPO_ZERO).await?;
                do_add_filenode(&ctx, &writer, root_second_filenode(), REPO_ONE).await?;

                assert_filenode(
                    &ctx,
                    reader.clone(),
                    &RepoPath::root(),
                    ONES_FNID,
                    REPO_ZERO,
                    root_first_filenode().info,
                )
                .await?;

                assert_no_filenode(&ctx, reader.clone(), &RepoPath::root(), ONES_FNID, REPO_ONE)
                    .await?;

                assert_filenode(
                    &ctx,
                    reader,
                    &RepoPath::root(),
                    TWOS_FNID,
                    REPO_ONE,
                    root_second_filenode().info,
                )
                .await?;
                Ok(())
            }

            #[fbinit::test]
            async fn test_insert_parent_and_child_in_same_batch(
                fb: FacebookInit,
            ) -> Result<(), Error> {
                let ctx = CoreContext::test_mock(fb);
                let (mut reader, writer) = build_reader_writer($create_db()?);
                $enable_caching(&mut reader);
                let reader = Arc::new(reader);

                do_add_filenodes(
                    &ctx,
                    &writer,
                    vec![root_first_filenode(), root_second_filenode()],
                    REPO_ZERO,
                )
                .await?;

                assert_filenode(
                    &ctx,
                    reader.clone(),
                    &RepoPath::root(),
                    ONES_FNID,
                    REPO_ZERO,
                    root_first_filenode().info,
                )
                .await?;

                assert_filenode(
                    &ctx,
                    reader,
                    &RepoPath::root(),
                    TWOS_FNID,
                    REPO_ZERO,
                    root_second_filenode().info,
                )
                .await?;
                Ok(())
            }

            #[fbinit::test]
            async fn insert_copied_file(fb: FacebookInit) -> Result<(), Error> {
                let ctx = CoreContext::test_mock(fb);
                let (mut reader, writer) = build_reader_writer($create_db()?);
                $enable_caching(&mut reader);
                let reader = Arc::new(reader);

                do_add_filenodes(
                    &ctx,
                    &writer,
                    vec![copied_from_filenode(), copied_filenode()],
                    REPO_ZERO,
                )
                .await?;
                assert_filenode(
                    &ctx,
                    reader,
                    &RepoPath::file("copiedto").unwrap(),
                    TWOS_FNID,
                    REPO_ZERO,
                    copied_filenode().info,
                )
                .await?;
                Ok(())
            }

            #[fbinit::test]
            async fn insert_same_copied_file(fb: FacebookInit) -> Result<(), Error> {
                let ctx = CoreContext::test_mock(fb);
                let (_reader, writer) = build_reader_writer($create_db()?);

                do_add_filenodes(&ctx, &writer, vec![copied_from_filenode()], REPO_ZERO).await?;
                do_add_filenodes(
                    &ctx,
                    &writer,
                    vec![copied_filenode(), copied_filenode()],
                    REPO_ZERO,
                )
                .await?;
                Ok(())
            }

            #[fbinit::test]
            async fn insert_copied_file_to_different_repo(fb: FacebookInit) -> Result<(), Error> {
                let ctx = CoreContext::test_mock(fb);
                let (mut reader, writer) = build_reader_writer($create_db()?);
                $enable_caching(&mut reader);
                let reader = Arc::new(reader);

                let copied = PreparedFilenode {
                    path: RepoPath::file("copiedto").unwrap(),
                    info: FilenodeInfo {
                        filenode: TWOS_FNID,
                        p1: None,
                        p2: None,
                        copyfrom: Some((RepoPath::file("copiedfrom").unwrap(), ONES_FNID)),
                        linknode: TWOS_CSID,
                    },
                };

                let notcopied = PreparedFilenode {
                    path: RepoPath::file("copiedto").unwrap(),
                    info: FilenodeInfo {
                        filenode: TWOS_FNID,
                        p1: None,
                        p2: None,
                        copyfrom: None,
                        linknode: TWOS_CSID,
                    },
                };

                do_add_filenodes(
                    &ctx,
                    &writer,
                    vec![copied_from_filenode(), copied.clone()],
                    REPO_ZERO,
                )
                .await?;

                do_add_filenodes(&ctx, &writer, vec![notcopied.clone()], REPO_ONE).await?;

                assert_filenode(
                    &ctx,
                    reader.clone(),
                    &RepoPath::file("copiedto").unwrap(),
                    TWOS_FNID,
                    REPO_ZERO,
                    copied.info,
                )
                .await?;

                assert_filenode(
                    &ctx,
                    reader,
                    &RepoPath::file("copiedto").unwrap(),
                    TWOS_FNID,
                    REPO_ONE,
                    notcopied.info,
                )
                .await?;
                Ok(())
            }

            #[fbinit::test]
            async fn get_all_filenodes_maybe_stale(fb: FacebookInit) -> Result<(), Error> {
                let ctx = CoreContext::test_mock(fb);
                let (mut reader, writer) = build_reader_writer($create_db()?);
                $enable_caching(&mut reader);
                let reader = Arc::new(reader);
                do_add_filenodes(
                    &ctx,
                    &writer,
                    vec![
                        root_first_filenode(),
                        root_second_filenode(),
                        root_merge_filenode(),
                    ],
                    REPO_ZERO,
                )
                .await?;
                do_add_filenodes(
                    &ctx,
                    &writer,
                    vec![file_a_first_filenode(), file_b_first_filenode()],
                    REPO_ZERO,
                )
                .await?;

                let root_filenodes = vec![
                    root_first_filenode().info,
                    root_second_filenode().info,
                    root_merge_filenode().info,
                ];

                assert_all_filenodes(
                    &ctx,
                    reader.clone(),
                    &RepoPath::RootPath,
                    REPO_ZERO,
                    &root_filenodes,
                    None,
                )
                .await?;

                assert_all_filenodes(
                    &ctx,
                    reader.clone(),
                    &RepoPath::file("a").unwrap(),
                    REPO_ZERO,
                    &vec![file_a_first_filenode().info],
                    None,
                )
                .await?;

                assert_all_filenodes(
                    &ctx,
                    reader,
                    &RepoPath::file("b").unwrap(),
                    REPO_ZERO,
                    &vec![file_b_first_filenode().info],
                    None,
                )
                .await?;
                Ok(())
            }

            #[fbinit::test]
            async fn get_all_filenodes_maybe_stale_limited(fb: FacebookInit) -> Result<(), Error> {
                let ctx = CoreContext::test_mock(fb);
                let (mut reader, writer) = build_reader_writer($create_db()?);
                $enable_caching(&mut reader);
                let reader = Arc::new(reader);
                do_add_filenodes(
                    &ctx,
                    &writer,
                    vec![
                        root_first_filenode(),
                        root_second_filenode(),
                        root_merge_filenode(),
                    ],
                    REPO_ZERO,
                )
                .await?;
                do_add_filenodes(
                    &ctx,
                    &writer,
                    vec![file_a_first_filenode(), file_b_first_filenode()],
                    REPO_ZERO,
                )
                .await?;

                let root_filenodes = vec![
                    root_first_filenode().info,
                    root_second_filenode().info,
                    root_merge_filenode().info,
                ];

                assert_all_filenodes(
                    &ctx,
                    reader.clone(),
                    &RepoPath::RootPath,
                    REPO_ZERO,
                    &root_filenodes,
                    Some(3),
                )
                .await?;

                let res = reader
                    .clone()
                    .get_all_filenodes_for_path(&ctx, REPO_ZERO, &RepoPath::RootPath, Some(1))
                    .await?;
                let res = res.do_not_handle_disabled_filenodes()?;
                assert_eq!(FilenodeRange::TooBig, res);

                let res = reader
                    .get_all_filenodes_for_path(&ctx, REPO_ZERO, &RepoPath::RootPath, Some(2))
                    .await?;
                let res = res.do_not_handle_disabled_filenodes()?;
                assert_eq!(FilenodeRange::TooBig, res);

                Ok(())
            }

            #[fbinit::test]
            async fn test_mixed_path_insert_and_get(fb: FacebookInit) -> Result<(), Error> {
                let ctx = CoreContext::test_mock(fb);
                let (mut reader, writer) = build_reader_writer($create_db()?);
                $enable_caching(&mut reader);
                let reader = Arc::new(reader);

                do_add_filenode(&ctx, &writer, file_a_first_filenode(), REPO_ZERO).await?;
                do_add_filenode(&ctx, &writer, dir_a_first_filenode(), REPO_ZERO).await?;

                assert_filenode(
                    &ctx,
                    reader.clone(),
                    &RepoPath::file("a").unwrap(),
                    ONES_FNID,
                    REPO_ZERO,
                    file_a_first_filenode().info,
                )
                .await?;

                assert_filenode(
                    &ctx,
                    reader.clone(),
                    &RepoPath::dir("a").unwrap(),
                    ONES_FNID,
                    REPO_ZERO,
                    dir_a_first_filenode().info,
                )
                .await?;

                assert_all_filenodes(
                    &ctx,
                    reader.clone(),
                    &RepoPath::file("a").unwrap(),
                    REPO_ZERO,
                    &vec![file_a_first_filenode().info],
                    None,
                )
                .await?;

                assert_all_filenodes(
                    &ctx,
                    reader,
                    &RepoPath::dir("a").unwrap(),
                    REPO_ZERO,
                    &vec![dir_a_first_filenode().info],
                    None,
                )
                .await?;

                Ok(())
            }
        }
    };
}

fn create_unsharded() -> Result<Vec1<Connection>, Error> {
    Ok(vec1![build_shard()?])
}

fn create_sharded() -> Result<Vec1<Connection>, Error> {
    Ok((0..16)
        .map(|_| build_shard())
        .collect::<Result<Vec<_>, Error>>()?
        .try_into()?)
}

fn no_caching(_reader: &mut FilenodesReader) {}

fn with_caching(reader: &mut FilenodesReader) {
    reader.local_cache = LocalCache::new_mock();
}

filenodes_tests!(uncached_unsharded_test, create_unsharded, no_caching);
filenodes_tests!(uncached_sharded_test, create_sharded, no_caching);

filenodes_tests!(cached_unsharded_test, create_unsharded, with_caching);
filenodes_tests!(cached_sharded_test, create_sharded, with_caching);

#[fbinit::test]
fn get_all_filenodes_maybe_stale_with_disabled(fb: FacebookInit) -> Result<(), Error> {
    let runtime = tokio::runtime::Builder::new_current_thread()
        .enable_io()
        .enable_time()
        .build()?;
    let ctx = CoreContext::test_mock(fb);

    let (mut reader, writer) = build_reader_writer(create_sharded()?);
    with_caching(&mut reader);
    let reader = Arc::new(reader);

    runtime.block_on(do_add_filenodes(
        &ctx,
        &writer,
        vec![
            root_first_filenode(),
            root_second_filenode(),
            root_merge_filenode(),
        ],
        REPO_ZERO,
    ))?;

    runtime.block_on(do_add_filenodes(
        &ctx,
        &writer,
        vec![file_a_first_filenode(), file_b_first_filenode()],
        REPO_ZERO,
    ))?;

    let tunables = MononokeTunables::default();
    tunables.update_bools(&hashmap! {"filenodes_disabled".to_string() => true});
    let res = with_tunables(tunables, || {
        runtime.block_on(reader.clone().get_all_filenodes_for_path(
            &ctx,
            REPO_ZERO,
            &RepoPath::RootPath,
            None,
        ))
    })?;

    if let FilenodeResult::Present(_) = res {
        panic!("expected FilenodeResult::Disabled");
    }

    let root_filenodes = vec![
        root_first_filenode().info,
        root_second_filenode().info,
        root_merge_filenode().info,
    ];

    runtime.block_on(assert_all_filenodes(
        &ctx,
        reader.clone(),
        &RepoPath::RootPath,
        REPO_ZERO,
        &root_filenodes,
        None,
    ))?;

    // All filenodes are cached now, even with filenodes_disabled = true
    // all filenodes should be returned
    let tunables = MononokeTunables::default();
    tunables.update_bools(&hashmap! {"filenodes_disabled".to_string() => true});
    with_tunables(tunables, || {
        runtime.block_on(assert_all_filenodes(
            &ctx,
            reader,
            &RepoPath::RootPath,
            REPO_ZERO,
            &root_filenodes,
            None,
        ))
    })?;
    Ok(())
}

#[fbinit::test]
fn test_get_filenode_with_disabled(fb: FacebookInit) -> Result<(), Error> {
    let runtime = tokio::runtime::Builder::new_current_thread()
        .enable_io()
        .enable_time()
        .build()?;
    let ctx = CoreContext::test_mock(fb);

    let (mut reader, writer) = build_reader_writer(create_sharded()?);
    with_caching(&mut reader);
    let reader = Arc::new(reader);

    runtime.block_on(do_add_filenodes(
        &ctx,
        &writer,
        vec![root_first_filenode()],
        REPO_ZERO,
    ))?;

    let payload_info = root_first_filenode().info;

    let tunables = MononokeTunables::default();
    tunables.update_bools(&hashmap! {"filenodes_disabled".to_string() => true});
    let res = with_tunables(tunables, || {
        runtime.block_on(reader.clone().get_filenode(
            &ctx,
            REPO_ZERO,
            &RepoPath::RootPath,
            payload_info.filenode,
        ))
    })?;

    if let FilenodeResult::Present(_) = res {
        panic!("expected FilenodeResult::Disabled");
    }

    runtime.block_on(assert_filenode(
        &ctx,
        reader.clone(),
        &RepoPath::root(),
        ONES_FNID,
        REPO_ZERO,
        root_first_filenode().info,
    ))?;

    // The filenode are cached now, even with filenodes_disabled = true
    // all filenodes should be returned
    let tunables = MononokeTunables::default();
    tunables.update_bools(&hashmap! {"filenodes_disabled".to_string() => true});
    with_tunables(tunables, || {
        runtime.block_on(assert_filenode(
            &ctx,
            reader,
            &RepoPath::root(),
            ONES_FNID,
            REPO_ZERO,
            root_first_filenode().info,
        ))
    })?;
    Ok(())
}

#[fbinit::test]
async fn test_all_filenodes_caching(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);

    let (mut reader, writer) = build_reader_writer(create_unsharded()?);
    with_caching(&mut reader);
    let reader = Arc::new(reader);
    do_add_filenode(&ctx, &writer, file_a_first_filenode(), REPO_ZERO).await?;

    let dir_a_second_filenode = PreparedFilenode {
        path: RepoPath::dir("a").unwrap(),
        info: FilenodeInfo {
            filenode: TWOS_FNID,
            p1: None,
            p2: None,
            copyfrom: None,
            linknode: TWOS_CSID,
        },
    };
    do_add_filenode(&ctx, &writer, dir_a_second_filenode.clone(), REPO_ZERO).await?;

    assert_all_filenodes(
        &ctx,
        reader.clone(),
        &RepoPath::file("a")?,
        REPO_ZERO,
        &vec![file_a_first_filenode().info],
        None,
    )
    .await?;

    assert_all_filenodes(
        &ctx,
        reader,
        &RepoPath::dir("a")?,
        REPO_ZERO,
        &vec![dir_a_second_filenode.info],
        None,
    )
    .await?;

    Ok(())
}

#[fbinit::test]
async fn test_point_filenode_caching(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);

    let (mut reader, writer) = build_reader_writer(create_unsharded()?);
    with_caching(&mut reader);
    let reader = Arc::new(reader);
    do_add_filenode(&ctx, &writer, file_a_first_filenode(), REPO_ZERO).await?;

    let dir_a_second_filenode = PreparedFilenode {
        path: RepoPath::dir("a").unwrap(),
        info: FilenodeInfo {
            filenode: ONES_FNID,
            p1: None,
            p2: None,
            copyfrom: None,
            linknode: TWOS_CSID,
        },
    };
    do_add_filenode(&ctx, &writer, dir_a_second_filenode.clone(), REPO_ZERO).await?;

    assert_filenode(
        &ctx,
        reader.clone(),
        &RepoPath::file("a")?,
        ONES_FNID,
        REPO_ZERO,
        file_a_first_filenode().info,
    )
    .await?;

    assert_filenode(
        &ctx,
        reader,
        &RepoPath::dir("a")?,
        ONES_FNID,
        REPO_ZERO,
        dir_a_second_filenode.info,
    )
    .await?;

    Ok(())
}


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

use anyhow::Error;
use context::CoreContext;
use context::PerfCounterType;
use fbinit::FacebookInit;
use filenodes::FilenodeInfo;
use filenodes::PreparedFilenode;
use mercurial_types_mocks::nodehash::ONES_CSID;
use mercurial_types_mocks::nodehash::ONES_FNID;
use mononoke_types::RepoPath;
use mononoke_types_mocks::repo::REPO_ZERO;
use vec1::vec1;

use super::util::build_reader_writer;
use super::util::build_shard;

#[fbinit::test]
async fn test_batching(fb: FacebookInit) -> Result<(), Error> {
    let (_, writer) = build_reader_writer(vec1![build_shard()?, build_shard()?]);

    let ctx = CoreContext::test_mock(fb);

    let filenodes = vec!["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k"]
        .into_iter()
        .map(|p| PreparedFilenode {
            path: RepoPath::file(p).unwrap(),
            info: FilenodeInfo {
                filenode: ONES_FNID,
                p1: None,
                p2: None,
                copyfrom: None,
                linknode: ONES_CSID,
            },
        })
        .collect();

    writer
        .insert_filenodes(&ctx, REPO_ZERO, filenodes, false)
        .await?
        .do_not_handle_disabled_filenodes()?;

    // 1 for paths, 1 for filenodes, per shard.
    assert_eq!(
        ctx.perf_counters().get_counter(PerfCounterType::SqlWrites),
        4
    );

    Ok(())
}

#[fbinit::test]
async fn test_no_empty_queries(fb: FacebookInit) -> Result<(), Error> {
    let (_, writer) = build_reader_writer(vec1![build_shard()?, build_shard()?]);

    let ctx = CoreContext::test_mock(fb);

    let filenodes = vec![PreparedFilenode {
        path: RepoPath::file("a").unwrap(),
        info: FilenodeInfo {
            filenode: ONES_FNID,
            p1: None,
            p2: None,
            copyfrom: None,
            linknode: ONES_CSID,
        },
    }];

    writer
        .insert_filenodes(&ctx, REPO_ZERO, filenodes, false)
        .await?
        .do_not_handle_disabled_filenodes()?;

    // 1 for paths, 1 for filenodes.
    assert_eq!(
        ctx.perf_counters().get_counter(PerfCounterType::SqlWrites),
        2
    );

    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 anyhow::Error;
use sql::rusqlite::Connection as SqliteConnection;
use sql::Connection;
use sql_construct::SqlConstruct;
use vec1::Vec1;

use crate::builder::NewFilenodesBuilder;
use crate::builder::SQLITE_INSERT_CHUNK_SIZE;
use crate::reader::FilenodesReader;
use crate::writer::FilenodesWriter;

pub fn build_shard() -> Result<Connection, Error> {
    let con = SqliteConnection::open_in_memory()?;
    con.execute_batch(NewFilenodesBuilder::CREATION_QUERY)?;
    Ok(Connection::with_sqlite(con))
}

pub fn build_reader_writer(shards: Vec1<Connection>) -> (FilenodesReader, FilenodesWriter) {
    let reader = FilenodesReader::new(shards.clone(), shards.clone());
    let writer = FilenodesWriter::new(SQLITE_INSERT_CHUNK_SIZE, shards.clone(), shards);
    (reader, writer)
}


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

use anyhow::anyhow;
use anyhow::Error;
use observability_config::types::ObservabilityConfig as CfgrObservabilityConfig;
use observability_config::types::ScubaObservabilityConfig as CfgrScubaObservabilityConfig;
use observability_config::types::ScubaVerbosityLevel as CfgrScubaVerbosityLevel;
use observability_config::types::SlogLoggingLevel as CfgrLoggingLevel;
use observability_config::types::SlogObservabilityConfig as CfgrSlogObservabilityConfig;
use regex::Regex;
use serde::de::Deserializer;
use serde::de::Error as _;
use serde::Deserialize;
use slog::Level;

fn cfgr_to_slog_level(level: CfgrLoggingLevel) -> Result<Level, Error> {
    match level {
        CfgrLoggingLevel::Trace => Ok(Level::Trace),
        CfgrLoggingLevel::Debug => Ok(Level::Debug),
        CfgrLoggingLevel::Info => Ok(Level::Info),
        CfgrLoggingLevel::Warning => Ok(Level::Warning),
        CfgrLoggingLevel::Error => Ok(Level::Error),
        CfgrLoggingLevel::Critical => Ok(Level::Critical),
        other => Err(anyhow!("unexpected SlogLoggingLevel: {:?}", other)),
    }
}

fn cfgr_to_scuba_level(level: &CfgrScubaVerbosityLevel) -> Result<ScubaVerbosityLevel, Error> {
    match *level {
        CfgrScubaVerbosityLevel::Normal => Ok(ScubaVerbosityLevel::Normal),
        CfgrScubaVerbosityLevel::Verbose => Ok(ScubaVerbosityLevel::Verbose),
        other => Err(anyhow!("unexpected ScubaLoggingLevel: {:?}", other)),
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum ScubaVerbosityLevel {
    Normal,
    Verbose,
}

pub struct SlogObservabilityConfig {
    pub level: Level,
}

pub struct ScubaObservabilityConfig {
    pub level: ScubaVerbosityLevel,
    pub verbose_sessions: Vec<String>,
    pub verbose_unixnames: Vec<String>,
    pub verbose_source_hostname_regexes: RwLock<Vec<Regex>>,
}

pub struct ObservabilityConfig {
    pub slog_config: SlogObservabilityConfig,
    pub scuba_config: ScubaObservabilityConfig,
}

impl TryFrom<CfgrSlogObservabilityConfig> for SlogObservabilityConfig {
    type Error = Error;
    fn try_from(value: CfgrSlogObservabilityConfig) -> Result<Self, Error> {
        Ok(Self {
            level: cfgr_to_slog_level(value.level)?,
        })
    }
}

impl TryFrom<CfgrScubaObservabilityConfig> for ScubaObservabilityConfig {
    type Error = Error;

    fn try_from(value: CfgrScubaObservabilityConfig) -> Result<Self, Error> {
        let CfgrScubaObservabilityConfig {
            level,
            verbose_sessions,
            verbose_unixnames,
            verbose_source_hostnames,
            ..
        } = value;
        let regexes = verbose_source_hostnames
            .into_iter()
            .map(|s| Regex::new(&s))
            .collect::<Result<Vec<Regex>, _>>()?;
        Ok(Self {
            level: cfgr_to_scuba_level(&level)?,
            verbose_sessions,
            verbose_unixnames,
            verbose_source_hostname_regexes: RwLock::new(regexes),
        })
    }
}

impl TryFrom<CfgrObservabilityConfig> for ObservabilityConfig {
    type Error = Error;

    fn try_from(value: CfgrObservabilityConfig) -> Result<Self, Error> {
        let CfgrObservabilityConfig {
            slog_config,
            scuba_config,
            ..
        } = value;

        Ok(Self {
            slog_config: slog_config.try_into()?,
            scuba_config: scuba_config.try_into()?,
        })
    }
}

impl<'de> Deserialize<'de> for ObservabilityConfig {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let raw = CfgrObservabilityConfig::deserialize(deserializer)?;
        let config = Self::try_from(raw).map_err(|e| D::Error::custom(format!("{:?}", e)))?;
        Ok(config)
    }
}


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

use anyhow::Error;
use cached_config::ConfigHandle;
use cached_config::ConfigStore;
use slog::Level;

use crate::config::ObservabilityConfig;
use crate::config::ScubaVerbosityLevel;
use crate::scuba::should_log_scuba_sample;
use crate::scuba::ScubaLoggingDecisionFields;

const CONFIGERATOR_OBSERVABILITY_CONFIG: &str = "scm/mononoke/observability/observability_config";

struct CfgrObservabilityContextInner {
    config_handle: ConfigHandle<ObservabilityConfig>,
}

impl CfgrObservabilityContextInner {
    fn new(config_store: &ConfigStore) -> Result<Self, Error> {
        let config_handle = config_store
            .get_config_handle_DEPRECATED(CONFIGERATOR_OBSERVABILITY_CONFIG.to_string())?;

        Ok(Self { config_handle })
    }

    fn get_logging_level(&self) -> Level {
        let config = self.config_handle.get();
        config.slog_config.level
    }

    fn should_log_scuba_sample(
        &self,
        verbosity_level: ScubaVerbosityLevel,
        logging_decision_fields: ScubaLoggingDecisionFields,
    ) -> bool {
        let config = self.config_handle.get();
        let scuba_config = &config.scuba_config;
        should_log_scuba_sample(verbosity_level, scuba_config, logging_decision_fields)
    }
}

/// A modifiable struct to be used in
/// the unit tests
pub struct TestObservabilityContextInner {
    level: Level,
}

impl TestObservabilityContextInner {
    pub fn new(level: Level) -> Arc<Mutex<Self>> {
        Arc::new(Mutex::new(Self { level }))
    }

    pub fn set_logging_level(&mut self, level: Level) {
        self.level = level;
    }

    fn get_logging_level(&self) -> Level {
        self.level
    }

    fn should_log_scuba_sample(
        &self,
        _verbosity_level: ScubaVerbosityLevel,
        _logging_decision_fields: ScubaLoggingDecisionFields,
    ) -> bool {
        true
    }
}

/// A static `ObservabilityContext` to represent
/// traditional behavior with predefined log levels
#[derive(Debug, Clone)]
pub struct StaticObservabilityContextInner {
    level: Level,
}

impl StaticObservabilityContextInner {
    fn new(level: Level) -> Self {
        Self { level }
    }

    fn get_logging_level(&self) -> Level {
        self.level
    }

    fn should_log_scuba_sample(
        &self,
        verbosity_level: ScubaVerbosityLevel,
        _logging_decision_fields: ScubaLoggingDecisionFields,
    ) -> bool {
        verbosity_level == ScubaVerbosityLevel::Normal
    }
}

#[derive(Clone)]
enum ObservabilityContextInner {
    Dynamic(Arc<CfgrObservabilityContextInner>),
    Static(StaticObservabilityContextInner),
    Test(Arc<Mutex<TestObservabilityContextInner>>),
}

impl ObservabilityContextInner {
    fn new(config_store: &ConfigStore) -> Result<Self, Error> {
        Ok(Self::Dynamic(Arc::new(CfgrObservabilityContextInner::new(
            config_store,
        )?)))
    }

    fn new_static(level: Level) -> Self {
        Self::Static(StaticObservabilityContextInner::new(level))
    }

    fn new_test(inner: Arc<Mutex<TestObservabilityContextInner>>) -> Self {
        Self::Test(inner)
    }

    fn get_logging_level(&self) -> Level {
        match self {
            Self::Dynamic(octx) => octx.get_logging_level(),
            Self::Test(octx) => octx.lock().expect("poisoned lock").get_logging_level(),
            Self::Static(octx) => octx.get_logging_level(),
        }
    }

    pub fn should_log_scuba_sample(
        &self,
        verbosity_level: ScubaVerbosityLevel,
        logging_decision_fields: ScubaLoggingDecisionFields,
    ) -> bool {
        match self {
            Self::Dynamic(octx) => {
                octx.should_log_scuba_sample(verbosity_level, logging_decision_fields)
            }
            Self::Static(octx) => {
                octx.should_log_scuba_sample(verbosity_level, logging_decision_fields)
            }
            Self::Test(octx) => octx
                .lock()
                .expect("poiosoned lock")
                .should_log_scuba_sample(verbosity_level, logging_decision_fields),
        }
    }
}

#[derive(Clone)]
pub struct ObservabilityContext {
    inner: ObservabilityContextInner,
}

impl ObservabilityContext {
    pub fn new(config_store: &ConfigStore) -> Result<Self, Error> {
        Ok(Self {
            inner: ObservabilityContextInner::new(config_store)?,
        })
    }

    pub fn new_test(inner: Arc<Mutex<TestObservabilityContextInner>>) -> Self {
        Self {
            inner: ObservabilityContextInner::new_test(inner),
        }
    }

    pub fn new_static(level: Level) -> Self {
        Self {
            inner: ObservabilityContextInner::new_static(level),
        }
    }

    pub fn get_logging_level(&self) -> Level {
        self.inner.get_logging_level()
    }

    pub fn should_log_scuba_sample(
        &self,
        verbosity_level: ScubaVerbosityLevel,
        logging_decision_fields: ScubaLoggingDecisionFields,
    ) -> bool {
        self.inner
            .should_log_scuba_sample(verbosity_level, logging_decision_fields)
    }
}


/*
 * 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 slog::Drain;
use slog::Level;
use slog::Never;
use slog::OwnedKVList;
use slog::Record;

use crate::context::ObservabilityContext;

pub struct DynamicLevelDrain<D> {
    inner: D,
    observability_context: ObservabilityContext,
}

impl<D> DynamicLevelDrain<D> {
    pub fn new(inner: D, observability_context: ObservabilityContext) -> Self {
        Self {
            inner,
            observability_context,
        }
    }

    fn current_level(&self) -> Level {
        self.observability_context.get_logging_level()
    }
}

impl<D: Drain<Ok = (), Err = Never>> Drain for DynamicLevelDrain<D> {
    type Ok = ();
    type Err = Never;

    fn log(&self, record: &Record, values: &OwnedKVList) -> Result<Self::Ok, Self::Err> {
        if record.level().is_at_least(self.current_level()) {
            self.inner.log(record, values)
        } else {
            Ok(())
        }
    }
}


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

mod config;
mod context;
mod drain;
mod scuba;

pub use context::ObservabilityContext;
pub use drain::DynamicLevelDrain;

pub use crate::config::ScubaVerbosityLevel;
pub use crate::scuba::ScubaLoggingDecisionFields;


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

use regex::Regex;
use scuba::ScubaValue;

use crate::config::ScubaObservabilityConfig;
use crate::config::ScubaVerbosityLevel;

#[derive(Clone)]
pub struct ScubaLoggingDecisionFields<'a> {
    pub maybe_session_id: Option<&'a ScubaValue>,
    pub maybe_unix_username: Option<&'a ScubaValue>,
    pub maybe_source_hostname: Option<&'a ScubaValue>,
}

fn is_verbose(maybe_value: Option<&ScubaValue>, verbose_values: &[String]) -> bool {
    // Note: assymptotically speaking having `verbose_values` to be a `HashSet` would
    //       provide better big-O complexity, but I expect the lengths of these lists
    //       to be so small, that O(n) search would provide sufficient perf.
    if let Some(ScubaValue::Normal(this_value)) = maybe_value {
        verbose_values
            .iter()
            .any(|verbose_value| verbose_value == this_value)
    } else {
        false
    }
}

fn is_verbose_by_regexes(maybe_value: Option<&ScubaValue>, regexes: &RwLock<Vec<Regex>>) -> bool {
    if let Some(ScubaValue::Normal(this_value)) = maybe_value {
        regexes
            .read()
            .expect("poisoned rwlock")
            .iter()
            .any(|regex| regex.is_match(this_value))
    } else {
        false
    }
}

pub fn should_log_scuba_sample<'a>(
    sample_verbosity_level: ScubaVerbosityLevel,
    scuba_observability_config: &'a ScubaObservabilityConfig,
    logging_decision_fields: ScubaLoggingDecisionFields<'a>,
) -> bool {
    let current_system_verbosity_level = &scuba_observability_config.level;

    if sample_verbosity_level <= *current_system_verbosity_level {
        // This sample should be logged regardless of its fields
        return true;
    }

    // Check if any of the `logging_decision_fields` convince
    // us to log this sample
    is_verbose(
        logging_decision_fields.maybe_session_id,
        &scuba_observability_config.verbose_sessions,
    ) || is_verbose(
        logging_decision_fields.maybe_unix_username,
        &scuba_observability_config.verbose_unixnames,
    ) || is_verbose_by_regexes(
        logging_decision_fields.maybe_source_hostname,
        &scuba_observability_config.verbose_source_hostname_regexes,
    )
}

#[cfg(test)]
mod tests {
    use std::sync::RwLock;

    use super::*;

    fn normal_scuba_cfg() -> ScubaObservabilityConfig {
        ScubaObservabilityConfig {
            level: ScubaVerbosityLevel::Normal,
            verbose_sessions: Vec::new(),
            verbose_unixnames: Vec::new(),
            verbose_source_hostname_regexes: RwLock::new(Vec::new()),
        }
    }

    #[test]
    fn test_always_log_normal_samples() {
        let fields = ScubaLoggingDecisionFields {
            maybe_session_id: None,
            maybe_unix_username: None,
            maybe_source_hostname: None,
        };

        let observability_config = normal_scuba_cfg();

        assert!(should_log_scuba_sample(
            ScubaVerbosityLevel::Normal,
            &observability_config,
            fields.clone()
        ));

        let observability_config = ScubaObservabilityConfig {
            level: ScubaVerbosityLevel::Verbose,
            ..normal_scuba_cfg()
        };

        assert!(should_log_scuba_sample(
            ScubaVerbosityLevel::Normal,
            &observability_config,
            fields,
        ));
    }

    #[test]
    fn test_log_any_sample_with_verbose_config() {
        let fields = ScubaLoggingDecisionFields {
            maybe_session_id: None,
            maybe_unix_username: None,
            maybe_source_hostname: None,
        };

        let observability_config = ScubaObservabilityConfig {
            level: ScubaVerbosityLevel::Verbose,
            ..normal_scuba_cfg()
        };

        assert!(should_log_scuba_sample(
            ScubaVerbosityLevel::Normal,
            &observability_config,
            fields.clone()
        ));

        assert!(should_log_scuba_sample(
            ScubaVerbosityLevel::Verbose,
            &observability_config,
            fields.clone()
        ));
    }

    #[test]
    fn test_session_filtering() {
        let observability_config = ScubaObservabilityConfig {
            verbose_sessions: vec!["verbose_session".to_string()],
            ..normal_scuba_cfg()
        };

        let verbose_session = ScubaValue::Normal("verbose_session".to_string());
        let fields = ScubaLoggingDecisionFields {
            maybe_session_id: Some(&verbose_session),
            maybe_unix_username: None,
            maybe_source_hostname: None,
        };

        assert!(should_log_scuba_sample(
            ScubaVerbosityLevel::Verbose,
            &observability_config,
            fields
        ));

        let normal_session = ScubaValue::Normal("normal_session".to_string());
        let fields = ScubaLoggingDecisionFields {
            maybe_session_id: Some(&normal_session),
            maybe_unix_username: None,
            maybe_source_hostname: None,
        };

        assert!(!should_log_scuba_sample(
            ScubaVerbosityLevel::Verbose,
            &observability_config,
            fields
        ));

        let fields = ScubaLoggingDecisionFields {
            maybe_session_id: None,
            maybe_unix_username: None,
            maybe_source_hostname: None,
        };

        assert!(!should_log_scuba_sample(
            ScubaVerbosityLevel::Verbose,
            &observability_config,
            fields
        ));
    }

    #[test]
    fn test_unixname_filtering() {
        let observability_config = ScubaObservabilityConfig {
            verbose_unixnames: vec!["verbose_user".to_string()],
            ..normal_scuba_cfg()
        };

        let verbose_user = ScubaValue::Normal("verbose_user".to_string());
        let fields = ScubaLoggingDecisionFields {
            maybe_session_id: None,
            maybe_unix_username: Some(&verbose_user),
            maybe_source_hostname: None,
        };

        assert!(should_log_scuba_sample(
            ScubaVerbosityLevel::Verbose,
            &observability_config,
            fields
        ));

        let normal_user = ScubaValue::Normal("normal_user".to_string());
        let fields = ScubaLoggingDecisionFields {
            maybe_session_id: None,
            maybe_unix_username: Some(&normal_user),
            maybe_source_hostname: None,
        };

        assert!(!should_log_scuba_sample(
            ScubaVerbosityLevel::Verbose,
            &observability_config,
            fields
        ));

        let fields = ScubaLoggingDecisionFields {
            maybe_session_id: None,
            maybe_unix_username: None,
            maybe_source_hostname: None,
        };

        assert!(!should_log_scuba_sample(
            ScubaVerbosityLevel::Verbose,
            &observability_config,
            fields
        ));
    }

    #[test]
    fn test_hostname_filtering() {
        let r = RwLock::new(vec![Regex::new("verbose_host.com").unwrap()]);
        let observability_config = ScubaObservabilityConfig {
            verbose_source_hostname_regexes: r,
            ..normal_scuba_cfg()
        };

        let verbose_host = ScubaValue::Normal("verbose_host.com".to_string());
        let fields = ScubaLoggingDecisionFields {
            maybe_session_id: None,
            maybe_unix_username: None,
            maybe_source_hostname: Some(&verbose_host),
        };

        assert!(should_log_scuba_sample(
            ScubaVerbosityLevel::Verbose,
            &observability_config,
            fields
        ));

        let normal_host = ScubaValue::Normal("normal_host.com".to_string());
        let fields = ScubaLoggingDecisionFields {
            maybe_session_id: None,
            maybe_unix_username: None,
            maybe_source_hostname: Some(&normal_host),
        };

        assert!(!should_log_scuba_sample(
            ScubaVerbosityLevel::Verbose,
            &observability_config,
            fields
        ));

        let fields = ScubaLoggingDecisionFields {
            maybe_session_id: None,
            maybe_unix_username: None,
            maybe_source_hostname: None,
        };

        assert!(!should_log_scuba_sample(
            ScubaVerbosityLevel::Verbose,
            &observability_config,
            fields
        ));
    }
}


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

use async_trait::async_trait;
use futures::stream::FuturesUnordered;
use futures::stream::StreamExt;

use crate::MononokeIdentitySet;

pub type ArcPermissionChecker = Arc<dyn PermissionChecker + Send + Sync + RefUnwindSafe + 'static>;
pub type BoxPermissionChecker = Box<dyn PermissionChecker + Send + Sync + RefUnwindSafe + 'static>;

#[async_trait]
pub trait PermissionChecker {
    async fn check_set(&self, accessors: &MononokeIdentitySet, actions: &[&str]) -> bool;
}

pub struct PermissionCheckerBuilder {
    pub(crate) checkers: Vec<BoxPermissionChecker>,
}

impl PermissionCheckerBuilder {
    pub fn new() -> PermissionCheckerBuilder {
        PermissionCheckerBuilder {
            checkers: Vec::new(),
        }
    }

    pub fn allow(mut self, acl: BoxPermissionChecker) -> PermissionCheckerBuilder {
        self.checkers.push(acl);
        self
    }

    pub fn allow_all(mut self) -> PermissionCheckerBuilder {
        self.checkers.push(Box::new(AlwaysAllow));
        self
    }

    pub fn allow_allowlist(mut self, allowlist: MononokeIdentitySet) -> PermissionCheckerBuilder {
        self.checkers.push(Box::new(AllowlistChecker { allowlist }));
        self
    }

    pub fn build(mut self) -> BoxPermissionChecker {
        if self.checkers.len() <= 1 {
            match self.checkers.pop() {
                None => Box::new(AlwaysReject),
                Some(checker) => checker,
            }
        } else {
            Box::new(UnionPermissionChecker {
                checkers: self.checkers,
            })
        }
    }
}

pub(crate) struct AlwaysAllow;

#[async_trait]
impl PermissionChecker for AlwaysAllow {
    async fn check_set(&self, _accessors: &MononokeIdentitySet, _actions: &[&str]) -> bool {
        true
    }
}

pub(crate) struct AlwaysReject;

#[async_trait]
impl PermissionChecker for AlwaysReject {
    async fn check_set(&self, _accessors: &MononokeIdentitySet, _actions: &[&str]) -> bool {
        false
    }
}

pub(crate) struct AllowlistChecker {
    allowlist: MononokeIdentitySet,
}

#[async_trait]
impl PermissionChecker for AllowlistChecker {
    async fn check_set(&self, accessors: &MononokeIdentitySet, _actions: &[&str]) -> bool {
        !self.allowlist.is_disjoint(accessors)
    }
}

struct UnionPermissionChecker {
    checkers: Vec<BoxPermissionChecker>,
}

#[async_trait]
impl PermissionChecker for UnionPermissionChecker {
    async fn check_set(&self, accessors: &MononokeIdentitySet, actions: &[&str]) -> bool {
        // Check all checkers in parallel.
        let mut checks: FuturesUnordered<_> = self
            .checkers
            .iter()
            .map(|checker| async { checker.check_set(accessors, actions).await })
            .collect();

        while let Some(check_result) = checks.next().await {
            if check_result {
                // Return true as soon as any checker says access is permitted.
                return true;
            }
        }

        return false;
    }
}


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

use std::collections::BTreeSet;
use std::fmt;
use std::str::FromStr;

use anyhow::Context;
use anyhow::Error;
use anyhow::Result;
use itertools::Itertools;
use serde::Deserialize;
use serde::Deserializer;
use serde::Serialize;
use serde::Serializer;

pub type MononokeIdentitySet = BTreeSet<MononokeIdentity>;

#[derive(Clone, Eq, PartialEq, Debug, Hash, Ord, PartialOrd)]
pub struct MononokeIdentity {
    id_type: String,
    id_data: String,
}

impl MononokeIdentity {
    pub fn new(id_type: impl Into<String>, id_data: impl Into<String>) -> Self {
        let id_type = id_type.into();
        let id_data = id_data.into();

        Self { id_type, id_data }
    }

    pub fn id_type(&self) -> &str {
        &self.id_type
    }

    pub fn id_data(&self) -> &str {
        &self.id_data
    }
}

impl fmt::Display for MononokeIdentity {
    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(fmt, "{}:{}", self.id_type(), self.id_data())
    }
}

impl FromStr for MononokeIdentity {
    type Err = Error;

    fn from_str(value: &str) -> Result<Self, Self::Err> {
        let (ty, data) = value.split_once(':').with_context(|| {
            format!(
                "MononokeIdentity parse error, expected TYPE:data, got {:?}",
                value
            )
        })?;
        Ok(Self::new(ty, data))
    }
}

impl Serialize for MononokeIdentity {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.collect_str(self)
    }
}

impl<'de> Deserialize<'de> for MononokeIdentity {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;
        Self::from_str(&s).map_err(serde::de::Error::custom)
    }
}

pub fn pretty_print(identities: &MononokeIdentitySet) -> String {
    identities.iter().map(ToString::to_string).join(",")
}

pub trait MononokeIdentitySetExt {
    fn is_quicksand(&self) -> bool;

    fn is_hg_sync_job(&self) -> bool;

    fn is_proxygen_test_identity(&self) -> bool;

    fn hostprefix(&self) -> Option<&str>;

    fn hostname(&self) -> Option<&str>;

    fn username(&self) -> Option<&str>;
}

#[test]
fn test_ipv6_identity() {
    let id = MononokeIdentity::from_str("MACHINE:2621:10d:c1a8:12c9::1162").unwrap();
    assert_eq!(id.id_data(), "2621:10d:c1a8:12c9::1162");
}


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

use std::collections::HashMap;
use std::fs::File;
use std::io::BufReader;
use std::path::Path;
use std::sync::Arc;

use anyhow::Result;
use async_trait::async_trait;
use serde::Deserialize;
use serde::Serialize;

use crate::AclProvider;
use crate::BoxMembershipChecker;
use crate::BoxPermissionChecker;
use crate::MembershipChecker;
use crate::MononokeIdentitySet;
use crate::PermissionChecker;

pub struct InternalAclProvider {
    acls: Acls,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Acls {
    #[serde(default)]
    pub repos: HashMap<String, Arc<Acl>>,

    #[serde(default)]
    pub repo_regions: HashMap<String, Arc<Acl>>,

    #[serde(default)]
    pub tiers: HashMap<String, Arc<Acl>>,

    #[serde(default)]
    pub groups: HashMap<String, Arc<MononokeIdentitySet>>,
}

#[derive(Debug, Default, Serialize, Deserialize)]
pub struct Acl {
    pub actions: HashMap<String, MononokeIdentitySet>,
}

pub struct AclMembershipChecker {
    group: Arc<MononokeIdentitySet>,
}

#[async_trait]
impl MembershipChecker for AclMembershipChecker {
    async fn is_member(&self, identities: &MononokeIdentitySet) -> bool {
        !self.group.is_disjoint(identities)
    }
}

pub struct AclPermissionChecker {
    acl: Arc<Acl>,
}

#[async_trait]
impl PermissionChecker for AclPermissionChecker {
    async fn check_set(&self, identities: &MononokeIdentitySet, actions: &[&str]) -> bool {
        for action in actions {
            // AclChecker uses the first action that exists
            if let Some(granted) = self.acl.actions.get(*action) {
                return !granted.is_disjoint(identities);
            }
        }
        // If none of the actions were present, the check fails.
        false
    }
}

impl InternalAclProvider {
    pub fn new(acls: Acls) -> Arc<dyn AclProvider> {
        Arc::new(InternalAclProvider { acls })
    }

    pub fn from_file(path: impl AsRef<Path>) -> Result<Arc<dyn AclProvider>> {
        let file = File::open(path.as_ref())?;
        let reader = BufReader::new(file);
        let acls = serde_json::from_reader(reader)?;
        Ok(Self::new(acls))
    }
}

#[async_trait]
impl AclProvider for InternalAclProvider {
    async fn repo_acl(&self, name: &str) -> Result<BoxPermissionChecker> {
        Ok(Box::new(AclPermissionChecker {
            acl: self.acls.repos.get(name).cloned().unwrap_or_default(),
        }))
    }

    async fn repo_region_acl(&self, name: &str) -> Result<BoxPermissionChecker> {
        Ok(Box::new(AclPermissionChecker {
            acl: self
                .acls
                .repo_regions
                .get(name)
                .cloned()
                .unwrap_or_default(),
        }))
    }

    async fn tier_acl(&self, name: &str) -> Result<BoxPermissionChecker> {
        Ok(Box::new(AclPermissionChecker {
            acl: self.acls.tiers.get(name).cloned().unwrap_or_default(),
        }))
    }

    async fn group(&self, name: &str) -> Result<BoxMembershipChecker> {
        Ok(Box::new(AclMembershipChecker {
            group: self.acls.groups.get(name).cloned().unwrap_or_default(),
        }))
    }

    async fn admin_group(&self) -> Result<BoxMembershipChecker> {
        self.group("admin").await
    }

    async fn reviewers_group(&self) -> Result<BoxMembershipChecker> {
        self.group("reviewers").await
    }
}

#[cfg(test)]
mod test {
    use fbinit::FacebookInit;

    use super::*;

    fn ids(ids: &[&str]) -> Result<MononokeIdentitySet> {
        let mut set = MononokeIdentitySet::new();
        for id in ids {
            set.insert(id.parse()?);
        }
        Ok(set)
    }

    #[fbinit::test]
    async fn json_acls(_fb: FacebookInit) -> Result<()> {
        let json = r##"
            {
                "repos": {
                    "repo1": {
                        "actions": {
                            "read": ["USER:user1"]
                        }
                    }
                },
                "groups": {
                    "users": [
                        "USER:user1",
                        "USER:user2"
                    ]
                }
            }
        "##;
        let acls = serde_json::from_str(json)?;
        let prov = InternalAclProvider::new(acls);
        let users_group = prov.group("users").await?;
        assert!(users_group.is_member(&ids(&["USER:user1"])?).await);
        assert!(!users_group.is_member(&ids(&["USER:impostor"])?).await);
        let repo1 = prov.repo_acl("repo1").await?;
        assert!(
            repo1
                .check_set(&ids(&["IP:localhost", "USER:user1"])?, &["access", "read"])
                .await
        );
        assert!(
            !repo1
                .check_set(&ids(&["IP:localhost", "USER:impostor"])?, &["read"])
                .await
        );
        assert!(
            !repo1
                .check_set(&ids(&["IP:localhost", "USER:user1"])?, &["write"])
                .await
        );
        Ok(())
    }
}


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

mod checker;
#[cfg(fbcode_build)]
mod facebook;
mod identity;
mod internal;
mod membership;
#[cfg(not(fbcode_build))]
mod oss;
mod provider;

pub use checker::ArcPermissionChecker;
pub use checker::BoxPermissionChecker;
pub use checker::PermissionChecker;
pub use checker::PermissionCheckerBuilder;
pub use identity::pretty_print;
pub use identity::MononokeIdentity;
pub use identity::MononokeIdentitySet;
pub use identity::MononokeIdentitySetExt;
pub use internal::InternalAclProvider;
pub use membership::AlwaysMember;
pub use membership::ArcMembershipChecker;
pub use membership::BoxMembershipChecker;
pub use membership::MemberAllowlist;
pub use membership::MembershipChecker;
pub use membership::NeverMember;
pub use provider::AclProvider;

#[cfg(fbcode_build)]
pub type DefaultAclProvider = facebook::HipsterAclProvider;

#[cfg(not(fbcode_build))]
pub type DefaultAclProvider = oss::DummyAclProvider;


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

use async_trait::async_trait;

use crate::MononokeIdentitySet;

pub type ArcMembershipChecker = Arc<dyn MembershipChecker + Send + Sync + RefUnwindSafe + 'static>;
pub type BoxMembershipChecker = Box<dyn MembershipChecker + Send + Sync + RefUnwindSafe + 'static>;

#[async_trait]
pub trait MembershipChecker {
    async fn is_member(&self, identities: &MononokeIdentitySet) -> bool;
}

pub struct AlwaysMember;

impl AlwaysMember {
    pub fn new() -> BoxMembershipChecker {
        Box::new(AlwaysMember)
    }
}

#[async_trait]
impl MembershipChecker for AlwaysMember {
    async fn is_member(&self, _identities: &MononokeIdentitySet) -> bool {
        true
    }
}

pub struct NeverMember;

impl NeverMember {
    pub fn new() -> BoxMembershipChecker {
        Box::new(NeverMember)
    }
}

#[async_trait]
impl MembershipChecker for NeverMember {
    async fn is_member(&self, _identities: &MononokeIdentitySet) -> bool {
        false
    }
}

pub struct MemberAllowlist {
    allowlist: MononokeIdentitySet,
}

impl MemberAllowlist {
    pub fn new(allowlist: MononokeIdentitySet) -> BoxMembershipChecker {
        Box::new(MemberAllowlist { allowlist })
    }
}

#[async_trait]
impl MembershipChecker for MemberAllowlist {
    async fn is_member(&self, identities: &MononokeIdentitySet) -> bool {
        !self.allowlist.is_disjoint(identities)
    }
}


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

use anyhow::bail;
use anyhow::Result;
use async_trait::async_trait;
use fbinit::FacebookInit;
use openssl::x509::X509;

use crate::checker::AlwaysAllow;
use crate::checker::BoxPermissionChecker;
use crate::identity::MononokeIdentity;
use crate::identity::MononokeIdentitySet;
use crate::identity::MononokeIdentitySetExt;
use crate::membership::AlwaysMember;
use crate::membership::BoxMembershipChecker;
use crate::membership::NeverMember;
use crate::provider::AclProvider;

impl MononokeIdentity {
    pub fn reviewer_identities(_username: &str) -> MononokeIdentitySet {
        MononokeIdentitySet::new()
    }

    pub fn try_from_ssh_encoded(_encoded: &str) -> Result<MononokeIdentitySet> {
        bail!("Decoding from SSH Principals is not yet implemented for MononokeIdentity")
    }

    pub fn try_from_json_encoded(_: &str) -> Result<MononokeIdentitySet> {
        bail!("Decoding from JSON is not yet implemented for MononokeIdentity")
    }

    pub fn try_from_x509(cert: &X509) -> Result<MononokeIdentitySet> {
        let subject_vec: Result<Vec<_>> = cert
            .subject_name()
            .entries()
            .map(|entry| {
                Ok(format!(
                    "{}={}",
                    entry.object().nid().short_name()?,
                    entry.data().as_utf8()?
                ))
            })
            .collect();
        let subject_name = subject_vec?.as_slice().join(",");

        let mut idents = MononokeIdentitySet::new();
        idents.insert(MononokeIdentity::new("X509_SUBJECT_NAME", subject_name));
        Ok(idents)
    }
}

impl MononokeIdentitySetExt for MononokeIdentitySet {
    fn is_quicksand(&self) -> bool {
        false
    }

    fn is_hg_sync_job(&self) -> bool {
        false
    }

    fn is_proxygen_test_identity(&self) -> bool {
        false
    }

    fn hostprefix(&self) -> Option<&str> {
        None
    }

    fn hostname(&self) -> Option<&str> {
        None
    }

    fn username(&self) -> Option<&str> {
        None
    }
}

pub struct DummyAclProvider;

impl DummyAclProvider {
    pub fn new(_fb: FacebookInit) -> Arc<dyn AclProvider> {
        Arc::new(DummyAclProvider)
    }
}

#[async_trait]
impl AclProvider for DummyAclProvider {
    async fn repo_acl(&self, _name: &str) -> Result<BoxPermissionChecker> {
        Ok(Box::new(AlwaysAllow))
    }

    async fn repo_region_acl(&self, _name: &str) -> Result<BoxPermissionChecker> {
        Ok(Box::new(AlwaysAllow))
    }

    async fn tier_acl(&self, _name: &str) -> Result<BoxPermissionChecker> {
        Ok(Box::new(AlwaysAllow))
    }

    async fn group(&self, _name: &str) -> Result<BoxMembershipChecker> {
        Ok(Box::new(NeverMember))
    }

    async fn admin_group(&self) -> Result<BoxMembershipChecker> {
        Ok(Box::new(NeverMember))
    }

    async fn reviewers_group(&self) -> Result<BoxMembershipChecker> {
        Ok(Box::new(AlwaysMember))
    }
}


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

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

use crate::BoxMembershipChecker;
use crate::BoxPermissionChecker;

/// A provider of access control lists and groups.
///
/// These lists and groups control permissions to access various aspects of
/// Mononoke.
#[async_trait]
pub trait AclProvider: Send + Sync {
    /// Returns a permission checker for the access control list that
    /// controls the named repository.
    async fn repo_acl(&self, name: &str) -> Result<BoxPermissionChecker>;

    /// Returns a permission checker for the access control list that
    /// controls the named repository region.
    async fn repo_region_acl(&self, name: &str) -> Result<BoxPermissionChecker>;

    /// Returns a permission checker for the named non-repo-specific
    /// access control list.
    async fn tier_acl(&self, name: &str) -> Result<BoxPermissionChecker>;

    /// Returns a membership checker for the named group.
    async fn group(&self, name: &str) -> Result<BoxMembershipChecker>;

    /// Returns a membership checker for the group that may administrate
    /// Mononoke.
    async fn admin_group(&self) -> Result<BoxMembershipChecker>;

    /// Returns a membership checker for the group that may review changes.
    async fn reviewers_group(&self) -> Result<BoxMembershipChecker>;
}


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

use caching_ext::CacheHandlerFactory;
use changeset_fetcher::ArcChangesetFetcher;
use memcache::KeyGen;
use mononoke_types::RepositoryId;
use phases::ArcPhases;
use sql::Connection;
use sql_construct::SqlConstruct;
use sql_construct::SqlConstructFromMetadataDatabaseConfig;
use sql_ext::SqlConnections;

use crate::sql_phases::HeadsFetcher;
use crate::sql_phases::SqlPhases;
use crate::sql_store::Caches;
use crate::sql_store::SqlPhasesStore;

// Memcache constants, should be changed when we want to invalidate memcache
// entries
const MC_CODEVER: u32 = 0;
const MC_SITEVER: u32 = 0;

/// Builder that can be used to produce SqlPhasesStore object.  Primarily
/// intended to be used by Repo factories.
#[derive(Clone)]
pub struct SqlPhasesBuilder {
    write_connection: Connection,
    read_connection: Connection,
    read_master_connection: Connection,
    caches: Arc<Caches>,
}

impl SqlPhasesBuilder {
    pub fn enable_caching(&mut self, cache_handler_factory: CacheHandlerFactory) {
        let caches = Caches::new(cache_handler_factory, Self::key_gen());
        self.caches = Arc::new(caches);
    }

    pub fn build(
        self,
        repo_id: RepositoryId,
        changeset_fetcher: ArcChangesetFetcher,
        heads_fetcher: HeadsFetcher,
    ) -> ArcPhases {
        let phases_store = self.phases_store();
        let phases = SqlPhases::new(phases_store, repo_id, changeset_fetcher, heads_fetcher);
        Arc::new(phases)
    }

    fn key_gen() -> KeyGen {
        let key_prefix = "scm.mononoke.phases";
        KeyGen::new(key_prefix, MC_CODEVER, MC_SITEVER)
    }

    fn phases_store(self) -> SqlPhasesStore {
        SqlPhasesStore {
            write_connection: self.write_connection,
            read_connection: self.read_connection,
            read_master_connection: self.read_master_connection,
            caches: self.caches,
        }
    }
}

impl SqlConstruct for SqlPhasesBuilder {
    const LABEL: &'static str = "phases";

    const CREATION_QUERY: &'static str = include_str!("../schemas/sqlite-phases.sql");

    fn from_sql_connections(connections: SqlConnections) -> Self {
        let caches = Arc::new(Caches::new(CacheHandlerFactory::Noop, Self::key_gen()));
        Self {
            write_connection: connections.write_connection,
            read_connection: connections.read_connection,
            read_master_connection: connections.read_master_connection,
            caches,
        }
    }
}

impl SqlConstructFromMetadataDatabaseConfig for SqlPhasesBuilder {}

#[cfg(test)]
mod tests {
    use anyhow::Error;
    use context::CoreContext;
    use fbinit::FacebookInit;
    use maplit::hashset;
    use mononoke_types_mocks::changesetid::*;
    use phases::Phase;

    use super::*;

    #[fbinit::test]
    async fn add_get_phase_sql_test(fb: FacebookInit) -> Result<(), Error> {
        let ctx = CoreContext::test_mock(fb);
        let repo_id = RepositoryId::new(0);
        let phases_builder = SqlPhasesBuilder::with_sqlite_in_memory()?;
        let phases_store = phases_builder.phases_store();

        phases_store
            .add_public_raw(&ctx, repo_id, vec![ONES_CSID])
            .await?;

        assert_eq!(
            phases_store
                .get_single_raw(&ctx, repo_id, ONES_CSID)
                .await?,
            Some(Phase::Public),
            "sql: get phase for the existing changeset"
        );

        assert_eq!(
            phases_store
                .get_single_raw(&ctx, repo_id, TWOS_CSID)
                .await?,
            None,
            "sql: get phase for non existing changeset"
        );

        assert_eq!(
            phases_store
                .get_public_raw(&ctx, repo_id, &[ONES_CSID, TWOS_CSID])
                .await?,
            hashset! {ONES_CSID},
            "sql: get phase for non existing changeset and existing changeset"
        );

        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 thiserror::Error;

#[derive(Debug, Eq, Error, PartialEq)]
pub enum SqlPhasesError {
    #[error("invalid phase value: {0}")]
    ValueError(String),
    #[error("failed to parse phase value from {0} bytes")]
    ParseError(usize),
}


/*
 * 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 builder;
mod errors;
mod sql_phases;
mod sql_store;

pub use builder::SqlPhasesBuilder;
pub use errors::SqlPhasesError;


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

use abomonation_derive::Abomonation;
use anyhow::Error;
use anyhow::Result;
use ascii::AsciiString;
use async_trait::async_trait;
use changeset_fetcher::ArcChangesetFetcher;
use context::CoreContext;
use futures::future::try_join;
use futures::future::BoxFuture;
use futures::future::FutureExt;
use mononoke_types::ChangesetId;
use mononoke_types::RepositoryId;
use phases::ArcPhases;
use phases::Phase;
use phases::Phases;
use sql::mysql;
use sql::mysql_async::prelude::ConvIr;
use sql::mysql_async::prelude::FromValue;
use sql::mysql_async::FromValueError;
use sql::mysql_async::Value;
use stats::define_stats;
use stats::prelude::*;

use crate::errors::SqlPhasesError;
use crate::sql_store::SqlPhasesStore;

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

/// Newtype wrapper for Phase that allows us to derive SQL conversions.
#[derive(Abomonation, Clone, Copy, PartialEq, Eq, Debug)]
#[derive(mysql::OptTryFromRowField)]
#[repr(transparent)]
pub struct SqlPhase(pub Phase);

impl From<SqlPhase> for Phase {
    fn from(phase: SqlPhase) -> Phase {
        phase.0
    }
}

impl From<Phase> for SqlPhase {
    fn from(phase: Phase) -> SqlPhase {
        SqlPhase(phase)
    }
}

impl fmt::Display for SqlPhase {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        fmt::Display::fmt(&self.0, f)
    }
}

impl TryFrom<&[u8]> for SqlPhase {
    type Error = SqlPhasesError;

    fn try_from(buf: &[u8]) -> Result<Self, Self::Error> {
        match std::str::from_utf8(buf) {
            Ok("Draft") => Ok(SqlPhase(Phase::Draft)),
            Ok("Public") => Ok(SqlPhase(Phase::Public)),
            Ok(s) => Err(SqlPhasesError::ValueError(s.to_string())),
            _ => Err(SqlPhasesError::ParseError(buf.len())),
        }
    }
}

impl From<SqlPhase> for Value {
    fn from(phase: SqlPhase) -> Self {
        Value::Bytes(phase.to_string().into())
    }
}

impl FromValue for SqlPhase {
    type Intermediate = SqlPhase;
}

impl ConvIr<SqlPhase> for SqlPhase {
    fn new(v: Value) -> Result<Self, FromValueError> {
        match v {
            Value::Bytes(bytes) => AsciiString::from_ascii(bytes)
                .map_err(|err| FromValueError(Value::Bytes(err.into_source())))
                .and_then(|s| match s.as_str() {
                    "Draft" => Ok(SqlPhase(Phase::Draft)),
                    "Public" => Ok(SqlPhase(Phase::Public)),
                    _ => Err(FromValueError(Value::Bytes(s.into()))),
                }),
            v => Err(FromValueError(v)),
        }
    }

    fn commit(self) -> SqlPhase {
        self
    }

    fn rollback(self) -> Value {
        self.into()
    }
}

pub type HeadsFetcher =
    Arc<dyn Fn(&CoreContext) -> BoxFuture<'static, Result<Vec<ChangesetId>, Error>> + Send + Sync>;

#[derive(Clone)]
pub struct SqlPhases {
    phases_store: SqlPhasesStore,
    changeset_fetcher: ArcChangesetFetcher,
    heads_fetcher: HeadsFetcher,
    repo_id: RepositoryId,
}

impl SqlPhases {
    pub async fn get_single_raw(
        &self,
        ctx: &CoreContext,
        cs_id: ChangesetId,
    ) -> Result<Option<Phase>, Error> {
        self.phases_store
            .get_single_raw(ctx, self.repo_id, cs_id)
            .await
    }

    pub async fn get_public_raw(
        &self,
        ctx: &CoreContext,
        csids: impl IntoIterator<Item = &ChangesetId>,
    ) -> Result<HashSet<ChangesetId>, Error> {
        self.phases_store
            .get_public_raw(ctx, self.repo_id, csids)
            .await
    }

    pub async fn add_public_raw(
        &self,
        ctx: &CoreContext,
        csids: Vec<ChangesetId>,
    ) -> Result<(), Error> {
        self.phases_store
            .add_public_raw(ctx, self.repo_id, csids)
            .await
    }

    pub async fn list_all_public(&self, ctx: &CoreContext) -> Result<Vec<ChangesetId>, Error> {
        self.phases_store.list_all_public(ctx, self.repo_id).await
    }

    pub async fn get_public_derive(
        &self,
        ctx: &CoreContext,
        csids: Vec<ChangesetId>,
        ephemeral_derive: bool,
    ) -> Result<HashSet<ChangesetId>, Error> {
        if csids.is_empty() {
            return Ok(Default::default());
        }
        let public_cold = self.get_public_raw(ctx, &csids).await?;

        let mut unknown: Vec<_> = csids
            .into_iter()
            .filter(|csid| !public_cold.contains(csid))
            .collect();

        if unknown.is_empty() {
            return Ok(public_cold);
        }

        // log fetching public heads
        STATS::public_heads_fetched.add_value(1);
        let heads = (self.heads_fetcher)(ctx).await?;
        let freshly_marked = mark_reachable_as_public(ctx, self, &heads, ephemeral_derive).await?;

        // Still do the get_public_raw incase someone else marked the changes as public
        // and thus mark_reachable_as_public did not return them as freshly_marked
        let public_hot = self.get_public_raw(ctx, &unknown).await?;

        let public_combined = public_cold.into_iter().chain(public_hot);
        let public_combined = if ephemeral_derive {
            unknown.retain(|e| freshly_marked.contains(e));
            public_combined.chain(unknown).collect()
        } else {
            public_combined.collect()
        };

        Ok(public_combined)
    }
}

impl SqlPhases {
    pub fn new(
        phases_store: SqlPhasesStore,
        repo_id: RepositoryId,
        changeset_fetcher: ArcChangesetFetcher,
        heads_fetcher: HeadsFetcher,
    ) -> Self {
        Self {
            phases_store,
            changeset_fetcher,
            heads_fetcher,
            repo_id,
        }
    }
}

#[async_trait]
impl Phases for SqlPhases {
    async fn get_public(
        &self,
        ctx: &CoreContext,
        csids: Vec<ChangesetId>,
        ephemeral_derive: bool,
    ) -> Result<HashSet<ChangesetId>> {
        self.get_public_derive(ctx, csids, ephemeral_derive).await
    }

    async fn get_cached_public(
        &self,
        ctx: &CoreContext,
        csids: Vec<ChangesetId>,
    ) -> Result<HashSet<ChangesetId>> {
        self.get_public_raw(ctx, &csids).await
    }

    async fn list_all_public(&self, ctx: &CoreContext) -> Result<Vec<ChangesetId>> {
        self.list_all_public(ctx).await
    }

    async fn add_reachable_as_public(
        &self,
        ctx: &CoreContext,
        heads: Vec<ChangesetId>,
    ) -> Result<Vec<ChangesetId>> {
        mark_reachable_as_public(ctx, self, &heads, false).await
    }

    async fn add_public_with_known_public_ancestors(
        &self,
        ctx: &CoreContext,
        csids: Vec<ChangesetId>,
    ) -> Result<()> {
        self.add_public_raw(ctx, csids).await
    }

    fn with_frozen_public_heads(&self, heads: Vec<ChangesetId>) -> ArcPhases {
        let heads_fetcher = Arc::new(move |_ctx: &CoreContext| {
            let heads = heads.clone();
            async move { Ok(heads) }.boxed()
        });
        Arc::new(SqlPhases {
            phases_store: self.phases_store.clone(),
            changeset_fetcher: self.changeset_fetcher.clone(),
            heads_fetcher,
            repo_id: self.repo_id,
        })
    }
}

/// Mark all commits reachable from `public_heads` as public
pub async fn mark_reachable_as_public(
    ctx: &CoreContext,
    phases: &SqlPhases,
    all_heads: &[ChangesetId],
    ephemeral_derive: bool,
) -> Result<Vec<ChangesetId>, Error> {
    let changeset_fetcher = &phases.changeset_fetcher;
    let public = phases.get_public_raw(ctx, all_heads).await?;

    let mut input = all_heads
        .iter()
        .filter(|csid| !public.contains(csid))
        .copied()
        .collect::<Vec<_>>();

    let mut unmarked = HashMap::new();
    loop {
        let cs = match input.pop() {
            None => {
                break;
            }
            Some(cs) => cs,
        };

        let phase = phases.get_single_raw(ctx, cs).await?;
        if let Some(Phase::Public) = phase {
            continue;
        }

        let (generation, parents) = try_join(
            changeset_fetcher.get_generation_number(ctx, cs),
            changeset_fetcher.get_parents(ctx, cs),
        )
        .await?;

        unmarked.insert(cs, generation);
        input.extend(parents.into_iter().filter(|p| !unmarked.contains_key(p)));
    }

    // NOTE: We need to write phases in increasing generation number order, this will
    //       ensure that our phases in a valid state (i.e do not have any gaps). Once
    //       first public changeset is found we assume that all ancestors of it have
    //       already been marked as public.
    let mut unmarked: Vec<_> = unmarked.into_iter().collect();
    unmarked.sort_by(|l, r| l.1.cmp(&r.1));

    let mut result = vec![];
    for chunk in unmarked.chunks(100) {
        let chunk = chunk.iter().map(|(cs, _)| *cs).collect::<Vec<_>>();
        if !ephemeral_derive {
            phases.add_public_raw(ctx, chunk.clone()).await?;
        }
        result.extend(chunk)
    }
    Ok(result)
}


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

use anyhow::Context as _;
use anyhow::Error;
use async_trait::async_trait;
use bytes::Bytes;
use caching_ext::fill_cache;
use caching_ext::get_or_fill;
use caching_ext::CacheDisposition;
use caching_ext::CacheHandlerFactory;
use caching_ext::CacheTtl;
use caching_ext::CachelibHandler;
use caching_ext::EntityStore;
use caching_ext::KeyedEntityStore;
use caching_ext::McErrorKind;
use caching_ext::McResult;
use caching_ext::MemcacheEntity;
use caching_ext::MemcacheHandler;
use context::CoreContext;
use context::PerfCounterType;
use maplit::hashset;
use memcache::KeyGen;
use mononoke_types::ChangesetId;
use mononoke_types::RepositoryId;
use phases::Phase;
use sql::Connection;
use sql_ext::mononoke_queries;
use stats::prelude::*;

use crate::sql_phases::SqlPhase;

// 6 hours in sec
pub const TTL_DRAFT_SEC: u64 = 21600;

define_stats! {
    prefix = "mononoke.phases";
    get_single: timeseries(Rate, Sum),
    get_many: timeseries(Rate, Sum),
    add_many: timeseries(Rate, Sum),
    list_all: timeseries(Rate, Sum),
}

pub struct Caches {
    pub memcache: MemcacheHandler, // Memcache Client for temporary caching
    pub cache_pool: CachelibHandler<SqlPhase>,
    pub keygen: KeyGen,
}

impl Caches {
    pub fn new(cache_handler_factory: CacheHandlerFactory, keygen: KeyGen) -> Self {
        Self {
            memcache: cache_handler_factory.memcache(),
            cache_pool: cache_handler_factory.cachelib(),
            keygen,
        }
    }
}

/// Object that reads/writes to phases db
#[derive(Clone)]
pub struct SqlPhasesStore {
    pub(crate) write_connection: Connection,
    pub(crate) read_connection: Connection,
    #[allow(dead_code)]
    pub(crate) read_master_connection: Connection,
    pub(crate) caches: Arc<Caches>,
}

impl SqlPhasesStore {
    pub async fn get_single_raw(
        &self,
        ctx: &CoreContext,
        repo_id: RepositoryId,
        cs_id: ChangesetId,
    ) -> Result<Option<Phase>, Error> {
        STATS::get_single.add_value(1);

        let ctx = (ctx, repo_id, self);

        let res = get_or_fill(&ctx, hashset! { cs_id })
            .await
            .with_context(|| "Error fetching phases via cache")?
            .into_values()
            .map(|val| val.into())
            .next();

        Ok(res)
    }

    pub async fn get_public_raw(
        &self,
        ctx: &CoreContext,
        repo_id: RepositoryId,
        csids: impl IntoIterator<Item = &ChangesetId>,
    ) -> Result<HashSet<ChangesetId>, Error> {
        let ids = csids.into_iter().cloned().collect::<HashSet<ChangesetId>>();
        if ids.is_empty() {
            return Ok(Default::default());
        }
        STATS::get_many.add_value(1);
        let ctx = (ctx, repo_id, self);
        let cs_to_phase = get_or_fill(&ctx, ids)
            .await
            .with_context(|| "Error fetching phases via cache")?;

        Ok(cs_to_phase
            .into_iter()
            .filter_map(|(key, value)| {
                if value == SqlPhase(Phase::Public) {
                    Some(key)
                } else {
                    None
                }
            })
            .collect())
    }

    pub async fn add_public_raw(
        &self,
        ctx: &CoreContext,
        repoid: RepositoryId,
        csids: Vec<ChangesetId>,
    ) -> Result<(), Error> {
        if csids.is_empty() {
            return Ok(());
        }
        STATS::add_many.add_value(1);
        let phases: Vec<_> = csids
            .iter()
            .map(|csid| (&repoid, csid, &SqlPhase(Phase::Public)))
            .collect();

        ctx.perf_counters()
            .increment_counter(PerfCounterType::SqlWrites);
        InsertPhase::query(&self.write_connection, &phases).await?;

        {
            let ctx = (ctx, repoid, self);
            let phases = csids
                .iter()
                .map(|csid| (csid, &SqlPhase(Phase::Public)))
                .collect::<Vec<_>>();
            fill_cache(&ctx, phases).await;
        }

        Ok(())
    }

    pub async fn list_all_public(
        &self,
        ctx: &CoreContext,
        repo_id: RepositoryId,
    ) -> Result<Vec<ChangesetId>, Error> {
        STATS::list_all.add_value(1);
        ctx.perf_counters()
            .increment_counter(PerfCounterType::SqlReadsReplica);
        let ans = SelectAllPublic::query(&self.read_connection, &repo_id).await?;
        Ok(ans.into_iter().map(|x| x.0).collect())
    }
}

impl MemcacheEntity for SqlPhase {
    fn serialize(&self) -> Bytes {
        Bytes::from(self.to_string())
    }

    fn deserialize(bytes: Bytes) -> McResult<Self> {
        bytes
            .as_ref()
            .try_into()
            .map_err(|_| McErrorKind::Deserialization)
    }
}

type CacheRequest<'a> = (&'a CoreContext, RepositoryId, &'a SqlPhasesStore);

impl EntityStore<SqlPhase> for CacheRequest<'_> {
    fn cachelib(&self) -> &CachelibHandler<SqlPhase> {
        let (_, _, phases) = self;
        &phases.caches.cache_pool
    }

    fn keygen(&self) -> &KeyGen {
        let (_, _, phases) = self;
        &phases.caches.keygen
    }

    fn memcache(&self) -> &MemcacheHandler {
        let (_, _, phases) = self;
        &phases.caches.memcache
    }

    fn cache_determinator(&self, phase: &SqlPhase) -> CacheDisposition {
        let ttl = if phase == &SqlPhase(Phase::Public) {
            CacheTtl::NoTtl
        } else {
            CacheTtl::Ttl(Duration::from_secs(TTL_DRAFT_SEC))
        };

        CacheDisposition::Cache(ttl)
    }

    caching_ext::impl_singleton_stats!("phases");
}

#[async_trait]
impl KeyedEntityStore<ChangesetId, SqlPhase> for CacheRequest<'_> {
    fn get_cache_key(&self, cs_id: &ChangesetId) -> String {
        let (_, repo_id, _) = self;
        get_cache_key(*repo_id, cs_id)
    }

    async fn get_from_db(
        &self,
        cs_ids: HashSet<ChangesetId>,
    ) -> Result<HashMap<ChangesetId, SqlPhase>, Error> {
        let (ctx, repo_id, mapping) = self;

        let cs_ids: Vec<_> = cs_ids.into_iter().collect();
        ctx.perf_counters()
            .increment_counter(PerfCounterType::SqlReadsReplica);

        // NOTE: We only track public phases in the DB.
        let public = SelectPhases::query(&mapping.read_connection, repo_id, &cs_ids).await?;

        Result::<_, Error>::Ok(public.into_iter().collect())
    }
}

pub fn get_cache_key(repo_id: RepositoryId, cs_id: &ChangesetId) -> String {
    format!("{}.{}", repo_id.prefix(), cs_id)
}

mononoke_queries! {
    write InsertPhase(values: (repo_id: RepositoryId, cs_id: ChangesetId, phase: SqlPhase)) {
        none,
        mysql("INSERT INTO phases (repo_id, cs_id, phase) VALUES {values} ON DUPLICATE KEY UPDATE phase = VALUES(phase)")
        // sqlite query currently doesn't support changing the value
        // there is not usage for changing the phase at the moment
        // TODO (liubovd): improve sqlite query to make it semantically the same
        sqlite("INSERT OR IGNORE INTO phases (repo_id, cs_id, phase) VALUES {values}")
    }

    read SelectPhases(
        repo_id: RepositoryId,
        >list cs_ids: ChangesetId
    ) -> (ChangesetId, SqlPhase) {
        mysql(
            "SELECT cs_id, phase
            FROM phases FORCE INDEX(PRIMARY)
            WHERE repo_id = {repo_id}
              AND cs_id IN {cs_ids}"
        )
        sqlite(
            "SELECT cs_id, phase
            FROM phases
            WHERE repo_id = {repo_id}
              AND cs_id IN {cs_ids}"
        )
    }

    read SelectAllPublic(repo_id: RepositoryId) -> (ChangesetId, ) {
        "SELECT cs_id
         FROM phases
         WHERE repo_id = {repo_id}
           AND phase = 'Public'"
    }
}


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

#[derive(Debug, Eq, Error, PartialEq)]
pub enum PhasesError {
    #[error("invalid phase enumeration value: {0}")]
    EnumError(u32),
}