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

use anyhow::anyhow;
use anyhow::Context;
use anyhow::Result;
use bookmarks_types::BookmarkKey;
use metaconfig_types::Address;
use metaconfig_types::BlameVersion;
use metaconfig_types::BookmarkOrRegex;
use metaconfig_types::BookmarkParams;
use metaconfig_types::CacheWarmupParams;
use metaconfig_types::CommitGraphConfig;
use metaconfig_types::CommitIdentityScheme;
use metaconfig_types::ComparableRegex;
use metaconfig_types::CrossRepoCommitValidation;
use metaconfig_types::DerivedDataConfig;
use metaconfig_types::DerivedDataTypesConfig;
use metaconfig_types::GlobalrevConfig;
use metaconfig_types::HgSyncConfig;
use metaconfig_types::HookBypass;
use metaconfig_types::HookConfig;
use metaconfig_types::HookManagerParams;
use metaconfig_types::HookParams;
use metaconfig_types::InfinitepushNamespace;
use metaconfig_types::InfinitepushParams;
use metaconfig_types::LfsParams;
use metaconfig_types::LoggingDestination;
use metaconfig_types::PushParams;
use metaconfig_types::PushrebaseFlags;
use metaconfig_types::PushrebaseParams;
use metaconfig_types::PushrebaseRemoteMode;
use metaconfig_types::RepoClientKnobs;
use metaconfig_types::SegmentedChangelogConfig;
use metaconfig_types::SegmentedChangelogHeadConfig;
use metaconfig_types::ServiceWriteRestrictions;
use metaconfig_types::ShardedService;
use metaconfig_types::ShardingModeConfig;
use metaconfig_types::SourceControlServiceMonitoring;
use metaconfig_types::SourceControlServiceParams;
use metaconfig_types::SparseProfilesConfig;
use metaconfig_types::UnodeVersion;
use metaconfig_types::UpdateLoggingConfig;
use metaconfig_types::WalkerConfig;
use metaconfig_types::WalkerJobParams;
use metaconfig_types::WalkerJobType;
use mononoke_types::ChangesetId;
use mononoke_types::MPath;
use mononoke_types::PrefixTrie;
use mononoke_types::RepositoryId;
use regex::Regex;
use repos::RawBookmarkConfig;
use repos::RawCacheWarmupConfig;
use repos::RawCommitGraphConfig;
use repos::RawCommitIdentityScheme;
use repos::RawCrossRepoCommitValidationConfig;
use repos::RawDerivedDataConfig;
use repos::RawDerivedDataTypesConfig;
use repos::RawHgSyncConfig;
use repos::RawHookConfig;
use repos::RawHookManagerParams;
use repos::RawInfinitepushParams;
use repos::RawLfsParams;
use repos::RawLoggingDestination;
use repos::RawLoggingDestinationScribe;
use repos::RawPushParams;
use repos::RawPushrebaseParams;
use repos::RawPushrebaseRemoteMode;
use repos::RawPushrebaseRemoteModeRemote;
use repos::RawRepoClientKnobs;
use repos::RawSegmentedChangelogConfig;
use repos::RawSegmentedChangelogHeadConfig;
use repos::RawServiceWriteRestrictions;
use repos::RawShardedService;
use repos::RawShardingModeConfig;
use repos::RawSourceControlServiceMonitoring;
use repos::RawSourceControlServiceParams;
use repos::RawSparseProfilesConfig;
use repos::RawUpdateLoggingConfig;
use repos::RawWalkerConfig;
use repos::RawWalkerJobParams;
use repos::RawWalkerJobType;

use crate::convert::Convert;
use crate::errors::ConfigurationError;

impl Convert for RawCacheWarmupConfig {
    type Output = CacheWarmupParams;

    fn convert(self) -> Result<Self::Output> {
        Ok(CacheWarmupParams {
            bookmark: BookmarkKey::new(self.bookmark)?,
            commit_limit: self
                .commit_limit
                .map(|v| v.try_into())
                .transpose()?
                .unwrap_or(200000),
            microwave_preload: self.microwave_preload.unwrap_or(false),
        })
    }
}

impl Convert for RawHookManagerParams {
    type Output = HookManagerParams;

    fn convert(self) -> Result<Self::Output> {
        Ok(HookManagerParams {
            disable_acl_checker: self.disable_acl_checker,
            all_hooks_bypassed: self.all_hooks_bypassed,
            bypassed_commits_scuba_table: self.bypassed_commits_scuba_table,
        })
    }
}

impl Convert for RawHookConfig {
    type Output = HookParams;

    fn convert(self) -> Result<Self::Output> {
        let bypass_commit_message = self.bypass_commit_string;

        let bypass_pushvar = self
            .bypass_pushvar
            .map(|s| {
                let parts: Vec<_> = s.split('=').collect();
                match parts.as_slice() {
                    [name, value] => Ok((name.to_string(), value.to_string())),
                    _ => Err(ConfigurationError::InvalidPushvar(s)),
                }
            })
            .transpose()?;

        let bypass = match (bypass_commit_message, bypass_pushvar) {
            (Some(msg), None) => Some(HookBypass::new_with_commit_msg(msg)),
            (None, Some((name, value))) => Some(HookBypass::new_with_pushvar(name, value)),
            (Some(msg), Some((name, value))) => Some(HookBypass::new_with_commit_msg_and_pushvar(
                msg, name, value,
            )),
            (None, None) => None,
        };

        let config = HookConfig {
            bypass,
            strings: self.config_strings.unwrap_or_default(),
            ints: self.config_ints.unwrap_or_default(),
            ints_64: self.config_ints_64.unwrap_or_default(),
            string_lists: self.config_string_lists.unwrap_or_default(),
            int_lists: self.config_int_lists.unwrap_or_default(),
            int_64_lists: self.config_int_64_lists.unwrap_or_default(),
        };

        Ok(HookParams {
            name: self.name,
            config,
        })
    }
}

impl Convert for RawBookmarkConfig {
    type Output = BookmarkParams;

    fn convert(self) -> Result<Self::Output> {
        let bookmark_or_regex = match (self.regex, self.name) {
            (None, Some(name)) => BookmarkOrRegex::Bookmark(BookmarkKey::new(name).unwrap()),
            (Some(regex), None) => match Regex::new(&regex) {
                Ok(regex) => BookmarkOrRegex::Regex(ComparableRegex::new(regex)),
                Err(err) => {
                    return Err(ConfigurationError::InvalidConfig(format!(
                        "invalid bookmark regex: {}",
                        err
                    ))
                    .into());
                }
            },
            _ => {
                return Err(ConfigurationError::InvalidConfig(
                    "bookmark's params need to specify regex xor name".into(),
                )
                .into());
            }
        };

        let hooks = self.hooks.into_iter().map(|rbmh| rbmh.hook_name).collect();
        let only_fast_forward = self.only_fast_forward;
        let allowed_users = self
            .allowed_users
            .map(|re| Regex::new(&re))
            .transpose()?
            .map(ComparableRegex::new);
        let allowed_hipster_group = self.allowed_hipster_group;
        let rewrite_dates = self.rewrite_dates;
        let hooks_skip_ancestors_of = self
            .hooks_skip_ancestors_of
            .unwrap_or_default()
            .into_iter()
            .map(BookmarkKey::new)
            .collect::<Result<Vec<_>, _>>()?;
        let ensure_ancestor_of = self.ensure_ancestor_of.map(BookmarkKey::new).transpose()?;
        let allow_move_to_public_commits_without_hooks = self
            .allow_move_to_public_commits_without_hooks
            .unwrap_or(false);

        Ok(BookmarkParams {
            bookmark: bookmark_or_regex,
            hooks,
            only_fast_forward,
            allowed_users,
            allowed_hipster_group,
            rewrite_dates,
            hooks_skip_ancestors_of,
            ensure_ancestor_of,
            allow_move_to_public_commits_without_hooks,
        })
    }
}

impl Convert for RawPushParams {
    type Output = PushParams;

    fn convert(self) -> Result<Self::Output> {
        let default = PushParams::default();
        Ok(PushParams {
            pure_push_allowed: self.pure_push_allowed.unwrap_or(default.pure_push_allowed),
        })
    }
}

impl Convert for RawCommitIdentityScheme {
    type Output = CommitIdentityScheme;

    fn convert(self) -> Result<Self::Output> {
        let converted = match self {
            RawCommitIdentityScheme::HG => CommitIdentityScheme::HG,
            RawCommitIdentityScheme::GIT => CommitIdentityScheme::GIT,
            RawCommitIdentityScheme::BONSAI => CommitIdentityScheme::BONSAI,
            RawCommitIdentityScheme::UNKNOWN => CommitIdentityScheme::UNKNOWN,
            v => return Err(anyhow!("Invalid value {} for enum CommitIdentityScheme", v)),
        };
        Ok(converted)
    }
}

impl Convert for RawPushrebaseRemoteModeRemote {
    type Output = Address;

    fn convert(self) -> Result<Self::Output> {
        match self {
            Self::tier(t) => Ok(Address::Tier(t)),
            Self::host_port(host) => Ok(Address::HostPort(host)),
            Self::UnknownField(e) => anyhow::bail!("Unknown field: {}", e),
        }
    }
}

impl Convert for RawPushrebaseRemoteMode {
    type Output = PushrebaseRemoteMode;

    fn convert(self) -> Result<Self::Output> {
        match self {
            Self::local(_) => Ok(PushrebaseRemoteMode::Local),
            Self::remote_land_service(addr) => {
                Ok(PushrebaseRemoteMode::RemoteLandService(addr.convert()?))
            }
            Self::remote_land_service_local_fallback(addr) => Ok(
                PushrebaseRemoteMode::RemoteLandServiceWithLocalFallback(addr.convert()?),
            ),
            Self::UnknownField(e) => anyhow::bail!("Unknown field: {}", e),
        }
    }
}

impl Convert for RawPushrebaseParams {
    type Output = PushrebaseParams;

    fn convert(self) -> Result<Self::Output> {
        let default = PushrebaseParams::default();
        Ok(PushrebaseParams {
            flags: PushrebaseFlags {
                rewritedates: self.rewritedates.unwrap_or(default.flags.rewritedates),
                recursion_limit: self
                    .recursion_limit
                    .map(|v| v.try_into())
                    .transpose()?
                    .or(default.flags.recursion_limit),
                forbid_p2_root_rebases: self
                    .forbid_p2_root_rebases
                    .unwrap_or(default.flags.forbid_p2_root_rebases),
                casefolding_check: self
                    .casefolding_check
                    .unwrap_or(default.flags.casefolding_check),
                casefolding_check_excluded_paths: self
                    .casefolding_check_excluded_paths
                    .map(|raw| {
                        raw.into_iter()
                            .map(|path| MPath::new_opt(path.as_bytes()))
                            .collect::<Result<PrefixTrie>>()
                    })
                    .transpose()?
                    .unwrap_or_default(),
                not_generated_filenodes_limit: 500,
                monitoring_bookmark: self.monitoring_bookmark,
            },
            block_merges: self.block_merges.unwrap_or(default.block_merges),
            emit_obsmarkers: self.emit_obsmarkers.unwrap_or(default.emit_obsmarkers),
            globalrev_config: self
                .globalrevs_publishing_bookmark
                .map(|bookmark| {
                    anyhow::Ok(GlobalrevConfig {
                        publishing_bookmark: BookmarkKey::new(bookmark)?,
                        small_repo_id: self.globalrevs_small_repo_id.map(RepositoryId::new),
                    })
                })
                .transpose()?,
            populate_git_mapping: self
                .populate_git_mapping
                .unwrap_or(default.populate_git_mapping),
            allow_change_xrepo_mapping_extra: self
                .allow_change_xrepo_mapping_extra
                .unwrap_or(false),
            remote_mode: self
                .remote_mode
                .map_or(Ok(default.remote_mode), Convert::convert)?,
        })
    }
}

impl Convert for RawLfsParams {
    type Output = LfsParams;

    fn convert(self) -> Result<Self::Output> {
        Ok(LfsParams {
            threshold: self.threshold.map(|v| v.try_into()).transpose()?,
            rollout_percentage: self.rollout_percentage.unwrap_or(0).try_into()?,
            generate_lfs_blob_in_hg_sync_job: self
                .generate_lfs_blob_in_hg_sync_job
                .unwrap_or(false),
        })
    }
}

impl Convert for RawInfinitepushParams {
    type Output = InfinitepushParams;

    fn convert(self) -> Result<Self::Output> {
        Ok(InfinitepushParams {
            allow_writes: self.allow_writes,
            namespace: self
                .namespace_pattern
                .and_then(|ns| Regex::new(&ns).ok().map(InfinitepushNamespace::new)),
            hydrate_getbundle_response: self.hydrate_getbundle_response.unwrap_or(false),
        })
    }
}

impl Convert for RawSourceControlServiceParams {
    type Output = SourceControlServiceParams;

    fn convert(self) -> Result<Self::Output> {
        let service_write_restrictions = self
            .service_write_restrictions
            .unwrap_or_default()
            .into_iter()
            .map(|(name, raw)| Ok((name, raw.convert()?)))
            .collect::<Result<HashMap<_, _>>>()?;

        Ok(SourceControlServiceParams {
            permit_writes: self.permit_writes,
            permit_service_writes: self.permit_service_writes,
            service_write_hipster_acl: self.service_write_hipster_acl,
            permit_commits_without_parents: self.permit_commits_without_parents,
            service_write_restrictions,
        })
    }
}

impl Convert for RawServiceWriteRestrictions {
    type Output = ServiceWriteRestrictions;

    fn convert(self) -> Result<Self::Output> {
        let RawServiceWriteRestrictions {
            permitted_methods,
            permitted_path_prefixes,
            permitted_bookmarks,
            permitted_bookmark_regex,
            ..
        } = self;

        let permitted_methods = permitted_methods.into_iter().collect();

        let permitted_path_prefixes = permitted_path_prefixes
            .map(|raw| {
                raw.into_iter()
                    .map(|path| MPath::new_opt(path.as_bytes()))
                    .collect::<Result<PrefixTrie>>()
            })
            .transpose()?
            .unwrap_or_default();

        let permitted_bookmarks = permitted_bookmarks
            .unwrap_or_default()
            .into_iter()
            .collect();

        let permitted_bookmark_regex = permitted_bookmark_regex
            .as_deref()
            .map(Regex::new)
            .transpose()
            .context("invalid service write permitted bookmark regex")?
            .map(ComparableRegex::new);

        Ok(ServiceWriteRestrictions {
            permitted_methods,
            permitted_path_prefixes,
            permitted_bookmarks,
            permitted_bookmark_regex,
        })
    }
}

impl Convert for RawSourceControlServiceMonitoring {
    type Output = SourceControlServiceMonitoring;

    fn convert(self) -> Result<Self::Output> {
        let bookmarks_to_report_age = self
            .bookmarks_to_report_age
            .into_iter()
            .map(BookmarkKey::new)
            .collect::<Result<Vec<_>>>()?;
        Ok(SourceControlServiceMonitoring {
            bookmarks_to_report_age,
        })
    }
}

impl Convert for RawDerivedDataTypesConfig {
    type Output = DerivedDataTypesConfig;

    fn convert(self) -> Result<Self::Output> {
        let types = self.types.into_iter().collect();
        let mapping_key_prefixes = self.mapping_key_prefixes.into_iter().collect();
        let unode_version = match self.unode_version {
            None => UnodeVersion::default(),
            Some(1) => return Err(anyhow!("unode version 1 has been deprecated")),
            Some(2) => UnodeVersion::V2,
            Some(version) => return Err(anyhow!("unknown unode version {}", version)),
        };
        let blame_filesize_limit = self.blame_filesize_limit.map(|limit| limit as u64);
        let blame_version = match self.blame_version {
            None => BlameVersion::default(),
            Some(1) => return Err(anyhow!("blame version 1 has been deprecated")),
            Some(2) => BlameVersion::V2,
            Some(version) => return Err(anyhow!("unknown blame version {}", version)),
        };
        Ok(DerivedDataTypesConfig {
            types,
            mapping_key_prefixes,
            unode_version,
            blame_filesize_limit,
            hg_set_committer_extra: self.hg_set_committer_extra.unwrap_or(false),
            blame_version,
        })
    }
}

impl Convert for RawDerivedDataConfig {
    type Output = DerivedDataConfig;

    fn convert(self) -> Result<Self::Output> {
        Ok(DerivedDataConfig {
            scuba_table: self.scuba_table,
            enabled_config_name: self.enabled_config_name.unwrap_or_default(),
            available_configs: self
                .available_configs
                .unwrap_or_default()
                .into_iter()
                .map(|(s, raw_config)| Ok((s, raw_config.convert()?)))
                .collect::<Result<_, anyhow::Error>>()?,
        })
    }
}

impl Convert for RawRepoClientKnobs {
    type Output = RepoClientKnobs;

    fn convert(self) -> Result<Self::Output> {
        Ok(RepoClientKnobs {
            allow_short_getpack_history: self.allow_short_getpack_history,
        })
    }
}

impl Convert for RawSegmentedChangelogHeadConfig {
    type Output = SegmentedChangelogHeadConfig;

    fn convert(self) -> Result<Self::Output> {
        let res = match self {
            Self::all_public_bookmarks_except(exceptions) => {
                SegmentedChangelogHeadConfig::AllPublicBookmarksExcept(
                    exceptions
                        .into_iter()
                        .map(BookmarkKey::new)
                        .collect::<Result<Vec<BookmarkKey>>>()?,
                )
            }
            Self::bookmark(bookmark_name) => {
                SegmentedChangelogHeadConfig::Bookmark(BookmarkKey::new(bookmark_name)?)
            }
            Self::bonsai_changeset(changeset_id) => {
                SegmentedChangelogHeadConfig::Changeset(ChangesetId::from_str(&changeset_id)?)
            }
            Self::UnknownField(_) => {
                return Err(anyhow!(
                    "Unknown variant of RawSegmentedChangelogHeadConfig!"
                ));
            }
        };
        Ok(res)
    }
}

impl Convert for RawSegmentedChangelogConfig {
    type Output = SegmentedChangelogConfig;

    fn convert(self) -> Result<Self::Output> {
        fn maybe_secs_to_duration(
            maybe_secs: Option<i64>,
            default: Option<Duration>,
        ) -> Result<Option<Duration>> {
            match maybe_secs {
                Some(secs) if secs == 0 => Ok(None),
                Some(secs) => Ok(Some(Duration::from_secs(secs.try_into()?))),
                None => Ok(default),
            }
        }

        let heads_to_include = self
            .heads_to_include
            .into_iter()
            .map(|raw| raw.convert())
            .collect::<Result<Vec<_>>>()?;

        let extra_heads_to_include_in_background_jobs = self
            .extra_heads_to_include_in_background_jobs
            .into_iter()
            .map(|raw| raw.convert())
            .collect::<Result<Vec<_>>>()?;

        let default = SegmentedChangelogConfig::default();
        Ok(SegmentedChangelogConfig {
            enabled: self.enabled.unwrap_or(default.enabled),
            tailer_update_period: maybe_secs_to_duration(
                self.tailer_update_period_secs,
                default.tailer_update_period,
            )?,
            skip_dag_load_at_startup: self
                .skip_dag_load_at_startup
                .unwrap_or(default.skip_dag_load_at_startup),
            reload_dag_save_period: maybe_secs_to_duration(
                self.reload_dag_save_period_secs,
                default.reload_dag_save_period,
            )?,
            update_to_master_bookmark_period: maybe_secs_to_duration(
                self.update_to_master_bookmark_period_secs,
                default.update_to_master_bookmark_period,
            )?,
            heads_to_include,
            extra_heads_to_include_in_background_jobs,
        })
    }
}

impl Convert for RawWalkerJobType {
    type Output = WalkerJobType;

    fn convert(self) -> Result<Self::Output> {
        let job_type = match self {
            RawWalkerJobType::SCRUB_ALL_CHUNKED => WalkerJobType::ScrubAllChunked,
            RawWalkerJobType::SCRUB_DERIVED_CHUNKED => WalkerJobType::ScrubDerivedChunked,
            RawWalkerJobType::SCRUB_DERIVED_NO_CONTENT_META => {
                WalkerJobType::ScrubDerivedNoContentMeta
            }
            RawWalkerJobType::SCRUB_DERIVED_NO_CONTENT_META_CHUNKED => {
                WalkerJobType::ScrubDerivedNoContentMetaChunked
            }
            RawWalkerJobType::SCRUB_HG_ALL_CHUNKED => WalkerJobType::ScrubHgAllChunked,
            RawWalkerJobType::SCRUB_HG_FILE_CONTENT => WalkerJobType::ScrubHgFileContent,
            RawWalkerJobType::SCRUB_HG_FILE_NODE => WalkerJobType::ScrubHgFileNode,
            RawWalkerJobType::SCRUB_UNODE_ALL_CHUNKED => WalkerJobType::ScrubUnodeAllChunked,
            RawWalkerJobType::SCRUB_UNODE_BLAME => WalkerJobType::ScrubUnodeBlame,
            RawWalkerJobType::SCRUB_UNODE_FASTLOG => WalkerJobType::ScrubUnodeFastlog,
            RawWalkerJobType::SHALLOW_HG_SCRUB => WalkerJobType::ShallowHgScrub,
            RawWalkerJobType::VALIDATE_ALL => WalkerJobType::ValidateAll,
            RawWalkerJobType::UNKNOWN => WalkerJobType::Unknown,
            v => return Err(anyhow!("Invalid value {} for enum WalkerJobType", v)),
        };
        Ok(job_type)
    }
}

impl Convert for RawWalkerJobParams {
    type Output = WalkerJobParams;

    fn convert(self) -> Result<Self::Output> {
        Ok(WalkerJobParams {
            scheduled_max_concurrency: self.scheduled_max_concurrency,
            qps_limit: self.qps_limit,
            exclude_node_type: self.exclude_node_type,
            allow_remaining_deferred: self.allow_remaining_deferred.map_or(false, |v| v),
            error_as_node_data_type: self.error_as_node_data_type,
        })
    }
}

impl Convert for RawWalkerConfig {
    type Output = WalkerConfig;

    fn convert(self) -> Result<Self::Output> {
        Ok(WalkerConfig {
            scrub_enabled: self.scrub_enabled,
            validate_enabled: self.validate_enabled,
            params: self
                .params
                .map(|p| {
                    p.into_iter()
                        .map(|(k, v)| anyhow::Ok((k.convert()?, v.convert()?)))
                        .collect::<Result<_, _>>()
                })
                .transpose()?,
        })
    }
}

impl Convert for RawCrossRepoCommitValidationConfig {
    type Output = CrossRepoCommitValidation;

    fn convert(self) -> Result<Self::Output> {
        let skip_bookmarks = self
            .skip_bookmarks
            .into_iter()
            .map(BookmarkKey::new)
            .collect::<Result<_, _>>()?;
        Ok(CrossRepoCommitValidation { skip_bookmarks })
    }
}

impl Convert for RawSparseProfilesConfig {
    type Output = SparseProfilesConfig;

    fn convert(self) -> Result<Self::Output> {
        Ok(SparseProfilesConfig {
            sparse_profiles_location: self.sparse_profiles_location,
            excluded_paths: self.excluded_paths.unwrap_or_default(),
            monitored_profiles: self.monitored_profiles.unwrap_or_default(),
        })
    }
}

impl Convert for RawHgSyncConfig {
    type Output = HgSyncConfig;

    fn convert(self) -> Result<Self::Output> {
        Ok(HgSyncConfig {
            hg_repo_ssh_path: self.hg_repo_ssh_path,
            batch_size: self.batch_size,
            lock_on_failure: self.lock_on_failure,
            darkstorm_backup_repo_id: self.darkstorm_backup_repo_id,
        })
    }
}

impl Convert for RawLoggingDestination {
    type Output = LoggingDestination;

    fn convert(self) -> Result<Self::Output> {
        let dest = match self {
            Self::logger(_) => LoggingDestination::Logger,
            Self::scribe(RawLoggingDestinationScribe { scribe_category }) => {
                LoggingDestination::Scribe { scribe_category }
            }
            Self::UnknownField(f) => {
                return Err(anyhow!("Unknown variant {} of RawLoggingDestination", f));
            }
        };
        Ok(dest)
    }
}

impl Convert for RawUpdateLoggingConfig {
    type Output = UpdateLoggingConfig;

    fn convert(self) -> Result<Self::Output> {
        Ok(UpdateLoggingConfig {
            bookmark_logging_destination: self.bookmark_logging_destination.convert()?,
            new_commit_logging_destination: self.new_commit_logging_destination.convert()?,
        })
    }
}

impl Convert for RawCommitGraphConfig {
    type Output = CommitGraphConfig;

    fn convert(self) -> Result<Self::Output> {
        Ok(CommitGraphConfig {
            scuba_table: self.scuba_table,
            preloaded_commit_graph_blobstore_key: self.preloaded_commit_graph_blobstore_key,
        })
    }
}

impl Convert for RawShardedService {
    type Output = ShardedService;

    fn convert(self) -> Result<Self::Output> {
        let service = match self {
            RawShardedService::EDEN_API => ShardedService::EdenApi,
            RawShardedService::SOURCE_CONTROL_SERVICE => ShardedService::SourceControlService,
            RawShardedService::DERIVED_DATA_SERVICE => ShardedService::DerivedDataService,
            RawShardedService::LAND_SERVICE => ShardedService::LandService,
            RawShardedService::LARGE_FILES_SERVICE => ShardedService::LargeFilesService,
            RawShardedService::DERIVATION_WORKER => ShardedService::DerivationWorker,
            RawShardedService::ASYNC_REQUESTS_WORKER => ShardedService::AsyncRequestsWorker,
            RawShardedService::WALKER_SCRUB_ALL => ShardedService::WalkerScrubAll,
            RawShardedService::WALKER_VALIDATE_ALL => ShardedService::WalkerValidateAll,
            RawShardedService::HG_SYNC => ShardedService::HgSync,
            RawShardedService::HG_SYNC_BACKUP => ShardedService::HgSyncBackup,
            RawShardedService::DERIVED_DATA_TAILER => ShardedService::DerivedDataTailer,
            RawShardedService::ALIAS_VERIFY => ShardedService::AliasVerify,
            RawShardedService::DRAFT_COMMIT_DELETION => ShardedService::DraftCommitDeletion,
            v => return Err(anyhow!("Invalid value {} for enum ShardedService", v)),
        };
        Ok(service)
    }
}

impl Convert for RawShardingModeConfig {
    type Output = ShardingModeConfig;

    fn convert(self) -> Result<Self::Output> {
        Ok(ShardingModeConfig {
            status: self
                .status
                .into_iter()
                .map(|(k, v)| anyhow::Ok((k.convert()?, v)))
                .collect::<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::num::NonZeroU64;
use std::num::NonZeroUsize;
use std::path::PathBuf;
use std::time::Duration;

use anyhow::anyhow;
use anyhow::bail;
use anyhow::Context;
use anyhow::Result;
use metaconfig_types::BlobConfig;
use metaconfig_types::BlobstoreId;
use metaconfig_types::BubbleDeletionMode;
use metaconfig_types::DatabaseConfig;
use metaconfig_types::EphemeralBlobstoreConfig;
use metaconfig_types::FilestoreParams;
use metaconfig_types::LocalDatabaseConfig;
use metaconfig_types::MetadataDatabaseConfig;
use metaconfig_types::MultiplexId;
use metaconfig_types::MultiplexedStoreType;
use metaconfig_types::PackConfig;
use metaconfig_types::PackFormat;
use metaconfig_types::RemoteDatabaseConfig;
use metaconfig_types::RemoteMetadataDatabaseConfig;
use metaconfig_types::ShardableRemoteDatabaseConfig;
use metaconfig_types::ShardedDatabaseConfig;
use metaconfig_types::ShardedRemoteDatabaseConfig;
use metaconfig_types::StorageConfig;
use nonzero_ext::nonzero;
use repos::RawBlobstoreConfig;
use repos::RawBlobstoreMultiplexedWal;
use repos::RawBlobstorePackConfig;
use repos::RawBlobstorePackFormat;
use repos::RawBubbleDeletionMode;
use repos::RawDbConfig;
use repos::RawDbLocal;
use repos::RawDbRemote;
use repos::RawDbShardableRemote;
use repos::RawDbShardedRemote;
use repos::RawEphemeralBlobstoreConfig;
use repos::RawFilestoreParams;
use repos::RawMetadataConfig;
use repos::RawMultiplexedStoreNormal;
use repos::RawMultiplexedStoreType;
use repos::RawMultiplexedStoreWriteOnly;
use repos::RawShardedDbConfig;
use repos::RawStorageConfig;

use crate::convert::Convert;

impl Convert for RawStorageConfig {
    type Output = StorageConfig;

    fn convert(self) -> Result<Self::Output> {
        Ok(StorageConfig {
            metadata: self.metadata.convert()?,
            blobstore: self.blobstore.convert()?,
            ephemeral_blobstore: self
                .ephemeral_blobstore
                .map(RawEphemeralBlobstoreConfig::convert)
                .transpose()?,
        })
    }
}

impl Convert for RawBubbleDeletionMode {
    type Output = BubbleDeletionMode;

    fn convert(self) -> Result<Self::Output> {
        let deletion_mode = match self {
            RawBubbleDeletionMode::DISABLED => BubbleDeletionMode::Disabled,
            RawBubbleDeletionMode::MARK_ONLY => BubbleDeletionMode::MarkOnly,
            RawBubbleDeletionMode::MARK_AND_DELETE => BubbleDeletionMode::MarkAndDelete,
            v => return Err(anyhow!("Invalid value {} for enum BubbleDeletionMode", v)),
        };
        Ok(deletion_mode)
    }
}

impl Convert for RawEphemeralBlobstoreConfig {
    type Output = EphemeralBlobstoreConfig;

    fn convert(self) -> Result<Self::Output> {
        Ok(EphemeralBlobstoreConfig {
            metadata: self.metadata.convert()?,
            blobstore: self.blobstore.convert()?,
            initial_bubble_lifespan: Duration::from_secs(
                self.initial_bubble_lifespan_secs
                    .try_into()
                    .context("Failed to convert initial_bubble_lifespan")?,
            ),
            bubble_expiration_grace: Duration::from_secs(
                self.bubble_expiration_grace_secs
                    .try_into()
                    .context("Failed to convert bubble_expiration_grace")?,
            ),
            bubble_deletion_mode: self.bubble_deletion_mode.convert()?,
        })
    }
}

impl Convert for RawBlobstoreConfig {
    type Output = BlobConfig;

    fn convert(self) -> Result<Self::Output> {
        let config = match self {
            RawBlobstoreConfig::disabled(_) => BlobConfig::Disabled,
            RawBlobstoreConfig::blob_files(raw) => BlobConfig::Files {
                path: PathBuf::from(raw.path),
            },
            RawBlobstoreConfig::blob_sqlite(raw) => BlobConfig::Sqlite {
                path: PathBuf::from(raw.path),
            },
            RawBlobstoreConfig::manifold(raw) => BlobConfig::Manifold {
                bucket: raw.manifold_bucket,
                prefix: raw.manifold_prefix,
            },
            RawBlobstoreConfig::mysql(raw) => BlobConfig::Mysql {
                remote: raw.remote.convert()?,
            },
            RawBlobstoreConfig::multiplexed_wal(RawBlobstoreMultiplexedWal {
                write_quorum,
                components,
                multiplex_id,
                queue_db,
                inner_blobstores_scuba_table,
                multiplex_scuba_table,
                scuba_sample_rate,
            }) => {
                let write_quorum: usize = write_quorum.try_into()?;
                if write_quorum > components.len() {
                    return Err(anyhow!(
                        "Not enough blobstores for {} write quorum (have {})",
                        write_quorum,
                        components.len()
                    ));
                }

                BlobConfig::MultiplexedWal {
                    multiplex_id: MultiplexId::new(multiplex_id),
                    blobstores: components
                        .into_iter()
                        .map(|comp| {
                            Ok((
                                BlobstoreId::new(comp.blobstore_id.try_into()?),
                                comp.store_type
                                    .convert()?
                                    .unwrap_or(MultiplexedStoreType::Normal),
                                comp.blobstore.convert()?,
                            ))
                        })
                        .collect::<Result<Vec<_>>>()?,
                    write_quorum,
                    queue_db: queue_db.convert()?,
                    inner_blobstores_scuba_table,
                    multiplex_scuba_table,
                    scuba_sample_rate: parse_scuba_sample_rate(scuba_sample_rate)?,
                }
            }

            RawBlobstoreConfig::manifold_with_ttl(raw) => {
                let ttl = Duration::from_secs(raw.ttl_secs.try_into()?);
                BlobConfig::ManifoldWithTtl {
                    bucket: raw.manifold_bucket,
                    prefix: raw.manifold_prefix,
                    ttl,
                }
            }
            RawBlobstoreConfig::logging(raw) => BlobConfig::Logging {
                scuba_table: raw.scuba_table,
                scuba_sample_rate: parse_scuba_sample_rate(raw.scuba_sample_rate)?,
                blobconfig: Box::new(raw.blobstore.convert()?),
            },
            RawBlobstoreConfig::pack(raw) => BlobConfig::Pack {
                blobconfig: Box::new(raw.blobstore.convert()?),
                pack_config: raw.pack_config.map(|c| c.convert()).transpose()?,
            },
            RawBlobstoreConfig::s3(raw) => BlobConfig::S3 {
                bucket: raw.bucket,
                keychain_group: raw.keychain_group,
                region_name: raw.region_name,
                endpoint: raw.endpoint,
                num_concurrent_operations: raw
                    .num_concurrent_operations
                    .map(|x| x.try_into())
                    .transpose()?,
                secret_name: raw.secret_name,
            },
            RawBlobstoreConfig::UnknownField(f) => {
                return Err(anyhow!("unsupported blobstore configuration ({})", f));
            }
        };
        Ok(config)
    }
}

fn parse_scuba_sample_rate(sample_rate: Option<i64>) -> Result<NonZeroU64> {
    let rate = sample_rate
        .map(|rate| {
            NonZeroU64::new(rate.try_into()?)
                .ok_or_else(|| anyhow!("scuba_sample_rate must be an integer larger than zero"))
        })
        .transpose()?
        .unwrap_or(nonzero!(100_u64));

    Ok(rate)
}

impl Convert for RawBlobstorePackFormat {
    type Output = PackFormat;

    fn convert(self) -> Result<Self::Output> {
        let pack_format = match self {
            RawBlobstorePackFormat::Raw(_) => PackFormat::Raw,
            RawBlobstorePackFormat::ZstdIndividual(zstd) => {
                PackFormat::ZstdIndividual(zstd.compression_level)
            }
            RawBlobstorePackFormat::UnknownField(f) => bail!("Unsupported PackFormat {}", f),
        };
        Ok(pack_format)
    }
}

impl Convert for RawBlobstorePackConfig {
    type Output = PackConfig;

    fn convert(self) -> Result<Self::Output> {
        let put_format = self.put_format.convert()?;
        Ok(PackConfig { put_format })
    }
}

impl Convert for RawDbLocal {
    type Output = LocalDatabaseConfig;

    fn convert(self) -> Result<Self::Output> {
        Ok(LocalDatabaseConfig {
            path: PathBuf::from(self.local_db_path),
        })
    }
}

impl Convert for RawDbRemote {
    type Output = RemoteDatabaseConfig;

    fn convert(self) -> Result<Self::Output> {
        Ok(RemoteDatabaseConfig {
            db_address: self.db_address,
        })
    }
}

impl Convert for RawDbShardedRemote {
    type Output = ShardedRemoteDatabaseConfig;

    fn convert(self) -> Result<Self::Output> {
        let shard_num = NonZeroUsize::new(self.shard_num.try_into()?)
            .ok_or_else(|| anyhow!("sharded remote shard_num must be > 0"))?;

        Ok(ShardedRemoteDatabaseConfig {
            shard_map: self.shard_map,
            shard_num,
        })
    }
}

impl Convert for RawDbShardableRemote {
    type Output = ShardableRemoteDatabaseConfig;

    fn convert(self) -> Result<Self::Output> {
        match self {
            RawDbShardableRemote::unsharded(raw) => {
                Ok(ShardableRemoteDatabaseConfig::Unsharded(raw.convert()?))
            }
            RawDbShardableRemote::sharded(raw) => {
                Ok(ShardableRemoteDatabaseConfig::Sharded(raw.convert()?))
            }
            RawDbShardableRemote::UnknownField(f) => {
                Err(anyhow!("unsupported database configuration ({})", f))
            }
        }
    }
}

impl Convert for RawDbConfig {
    type Output = DatabaseConfig;

    fn convert(self) -> Result<Self::Output> {
        match self {
            RawDbConfig::local(raw) => Ok(DatabaseConfig::Local(raw.convert()?)),
            RawDbConfig::remote(raw) => Ok(DatabaseConfig::Remote(raw.convert()?)),
            RawDbConfig::UnknownField(f) => {
                Err(anyhow!("unsupported database configuration ({})", f))
            }
        }
    }
}

impl Convert for RawShardedDbConfig {
    type Output = ShardedDatabaseConfig;

    fn convert(self) -> Result<Self::Output> {
        match self {
            RawShardedDbConfig::local(raw) => Ok(ShardedDatabaseConfig::Local(raw.convert()?)),
            RawShardedDbConfig::remote(raw) => Ok(ShardedDatabaseConfig::Sharded(raw.convert()?)),
            RawShardedDbConfig::unsharded(raw) => {
                Ok(ShardedDatabaseConfig::Unsharded(raw.convert()?))
            }
            RawShardedDbConfig::UnknownField(f) => {
                Err(anyhow!("unsupported database configuration ({})", f))
            }
        }
    }
}

impl Convert for RawMetadataConfig {
    type Output = MetadataDatabaseConfig;

    fn convert(self) -> Result<Self::Output> {
        match self {
            RawMetadataConfig::local(raw) => Ok(MetadataDatabaseConfig::Local(raw.convert()?)),
            RawMetadataConfig::remote(raw) => Ok(MetadataDatabaseConfig::Remote(
                RemoteMetadataDatabaseConfig {
                    primary: raw.primary.convert()?,
                    filenodes: raw.filenodes.convert()?,
                    mutation: raw.mutation.convert()?,
                    sparse_profiles: raw.sparse_profiles.convert()?,
                    bonsai_blob_mapping: raw.bonsai_blob_mapping.convert()?,
                    deletion_log: raw.deletion_log.convert()?,
                },
            )),
            RawMetadataConfig::UnknownField(f) => Err(anyhow!(
                "unsupported metadata database configuration ({})",
                f
            )),
        }
    }
}

impl Convert for RawFilestoreParams {
    type Output = FilestoreParams;

    fn convert(self) -> Result<Self::Output> {
        Ok(FilestoreParams {
            chunk_size: self.chunk_size.try_into()?,
            concurrency: self.concurrency.try_into()?,
        })
    }
}

impl Convert for RawMultiplexedStoreType {
    type Output = MultiplexedStoreType;

    fn convert(self) -> Result<Self::Output> {
        match self {
            RawMultiplexedStoreType::normal(RawMultiplexedStoreNormal {}) => {
                Ok(MultiplexedStoreType::Normal)
            }
            RawMultiplexedStoreType::write_only(RawMultiplexedStoreWriteOnly {}) => {
                Ok(MultiplexedStoreType::WriteOnly)
            }
            RawMultiplexedStoreType::UnknownField(field) => {
                Err(anyhow!("unknown store type {}", field))
            }
        }
    }
}