/*
 * 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::Debug;
use std::fs::create_dir_all;
use std::future::ready;
use std::num::NonZeroU64;
use std::sync::Arc;
use std::time::Duration;

use anyhow::format_err;
use async_trait::async_trait;
use bytes::Bytes as RawBytes;
use edenapi_types::make_hash_lookup_request;
use edenapi_types::AlterSnapshotRequest;
use edenapi_types::AlterSnapshotResponse;
use edenapi_types::AnyFileContentId;
use edenapi_types::AnyId;
use edenapi_types::Batch;
use edenapi_types::BlameRequest;
use edenapi_types::BlameResult;
use edenapi_types::BonsaiChangesetContent;
use edenapi_types::BookmarkEntry;
use edenapi_types::BookmarkRequest;
use edenapi_types::CloneData;
use edenapi_types::CommitGraphEntry;
use edenapi_types::CommitGraphRequest;
use edenapi_types::CommitGraphSegmentsEntry;
use edenapi_types::CommitGraphSegmentsRequest;
use edenapi_types::CommitHashLookupRequest;
use edenapi_types::CommitHashLookupResponse;
use edenapi_types::CommitHashToLocationRequestBatch;
use edenapi_types::CommitHashToLocationResponse;
use edenapi_types::CommitId;
use edenapi_types::CommitIdScheme;
use edenapi_types::CommitKnownResponse;
use edenapi_types::CommitLocationToHashRequest;
use edenapi_types::CommitLocationToHashRequestBatch;
use edenapi_types::CommitLocationToHashResponse;
use edenapi_types::CommitMutationsRequest;
use edenapi_types::CommitMutationsResponse;
use edenapi_types::CommitRevlogData;
use edenapi_types::CommitRevlogDataRequest;
use edenapi_types::CommitTranslateIdRequest;
use edenapi_types::CommitTranslateIdResponse;
use edenapi_types::EdenApiServerError;
use edenapi_types::EphemeralPrepareRequest;
use edenapi_types::EphemeralPrepareResponse;
use edenapi_types::FetchSnapshotRequest;
use edenapi_types::FetchSnapshotResponse;
use edenapi_types::FileRequest;
use edenapi_types::FileResponse;
use edenapi_types::FileSpec;
use edenapi_types::HgFilenodeData;
use edenapi_types::HgMutationEntryContent;
use edenapi_types::HistoryEntry;
use edenapi_types::HistoryRequest;
use edenapi_types::HistoryResponseChunk;
use edenapi_types::IndexableId;
use edenapi_types::LandStackRequest;
use edenapi_types::LandStackResponse;
use edenapi_types::LookupRequest;
use edenapi_types::LookupResponse;
use edenapi_types::LookupResult;
use edenapi_types::PushVar;
use edenapi_types::ServerError;
use edenapi_types::SetBookmarkRequest;
use edenapi_types::ToApi;
use edenapi_types::ToWire;
use edenapi_types::TreeAttributes;
use edenapi_types::TreeEntry;
use edenapi_types::TreeRequest;
use edenapi_types::UploadBonsaiChangesetRequest;
use edenapi_types::UploadHgChangeset;
use edenapi_types::UploadHgChangesetsRequest;
use edenapi_types::UploadHgFilenodeRequest;
use edenapi_types::UploadToken;
use edenapi_types::UploadTokenMetadata;
use edenapi_types::UploadTokensResponse;
use edenapi_types::UploadTreeEntry;
use edenapi_types::UploadTreeRequest;
use edenapi_types::UploadTreeResponse;
use futures::future::BoxFuture;
use futures::prelude::*;
use hg_http::http_client;
use http_client::AsyncResponse;
use http_client::Encoding;
use http_client::HttpClient;
use http_client::Request;
use itertools::Itertools;
use metrics::Counter;
use metrics::EntranceGuard;
use minibytes::Bytes;
use parking_lot::Once;
use progress_model::AggregatingProgressBar;
use progress_model::ProgressBar;
use repo_name::encode_repo_name;
use serde::de::DeserializeOwned;
use serde::Serialize;
use types::HgId;
use types::Key;
use url::Url;

use crate::api::EdenApi;
use crate::builder::Config;
use crate::errors::EdenApiError;
use crate::response::Response;
use crate::response::ResponseMeta;
use crate::retryable::RetryableFileAttrs;
use crate::retryable::RetryableFiles;
use crate::retryable::RetryableStreamRequest;
use crate::retryable::RetryableTrees;
use crate::types::wire::pull::PullFastForwardRequest;
use crate::types::wire::pull::PullLazyRequest;

const MAX_CONCURRENT_LOOKUPS_PER_REQUEST: usize = 10000;
const MAX_CONCURRENT_UPLOAD_FILENODES_PER_REQUEST: usize = 10000;
const MAX_CONCURRENT_UPLOAD_TREES_PER_REQUEST: usize = 1000;
const MAX_CONCURRENT_FILE_UPLOADS: usize = 1000;
const MAX_CONCURRENT_HASH_LOOKUPS_PER_REQUEST: usize = 1000;
const MAX_CONCURRENT_BLAMES_PER_REQUEST: usize = 10;
const MAX_ERROR_MSG_LEN: usize = 500;

static REQUESTS_INFLIGHT: Counter = Counter::new("edenapi.req_inflight");
static FILES_INFLIGHT: Counter = Counter::new("edenapi.files_inflight");
static FILES_ATTRS_INFLIGHT: Counter = Counter::new("edenapi.files_attrs_inflight");

mod paths {
    pub const HEALTH_CHECK: &str = "health_check";
    pub const FILES2: &str = "files2";
    pub const HISTORY: &str = "history";
    pub const TREES: &str = "trees";
    pub const COMMIT_REVLOG_DATA: &str = "commit/revlog_data";
    pub const CLONE_DATA: &str = "clone";
    pub const PULL_FAST_FORWARD: &str = "pull_fast_forward_master";
    pub const PULL_LAZY: &str = "pull_lazy";
    pub const COMMIT_LOCATION_TO_HASH: &str = "commit/location_to_hash";
    pub const COMMIT_HASH_TO_LOCATION: &str = "commit/hash_to_location";
    pub const COMMIT_HASH_LOOKUP: &str = "commit/hash_lookup";
    pub const COMMIT_GRAPH_V2: &str = "commit/graph_v2";
    pub const COMMIT_GRAPH_SEGMENTS: &str = "commit/graph_segments";
    pub const COMMIT_MUTATIONS: &str = "commit/mutations";
    pub const COMMIT_TRANSLATE_ID: &str = "commit/translate_id";
    pub const BOOKMARKS: &str = "bookmarks";
    pub const SET_BOOKMARK: &str = "bookmarks/set";
    pub const LAND_STACK: &str = "land";
    pub const LOOKUP: &str = "lookup";
    pub const UPLOAD: &str = "upload/";
    pub const UPLOAD_FILENODES: &str = "upload/filenodes";
    pub const UPLOAD_TREES: &str = "upload/trees";
    pub const UPLOAD_CHANGESETS: &str = "upload/changesets";
    pub const UPLOAD_BONSAI_CHANGESET: &str = "upload/changeset/bonsai";
    pub const EPHEMERAL_PREPARE: &str = "ephemeral/prepare";
    pub const FETCH_SNAPSHOT: &str = "snapshot";
    pub const ALTER_SNAPSHOT: &str = "snapshot/alter";
    pub const DOWNLOAD_FILE: &str = "download/file";
    pub const BLAME: &str = "blame";
}

#[derive(Clone)]
pub struct Client {
    inner: Arc<ClientInner>,
}

pub struct ClientInner {
    config: Config,
    client: HttpClient,
    tree_progress: Arc<AggregatingProgressBar>,
    file_progress: Arc<AggregatingProgressBar>,
}

static LOG_SERVER_INFO_ONCE: Once = Once::new();

impl Client {
    /// Create an EdenAPI client with the given configuration.
    pub(crate) fn with_config(config: Config) -> Self {
        let client = http_client("edenapi", config.http_config.clone());
        let inner = Arc::new(ClientInner {
            config,
            client,
            tree_progress: AggregatingProgressBar::new("fetching", "trees"),
            file_progress: AggregatingProgressBar::new("fetching", "files"),
        });
        Self { inner }
    }

    pub(crate) fn config(&self) -> &Config {
        &self.inner.config
    }

    fn repo_name(&self) -> &str {
        &self.config().repo_name
    }

    /// Append endpoint path onto the server's base URL.
    fn build_url_repoless(&self, path: &str) -> Result<Url, EdenApiError> {
        let url = &self.config().server_url;
        Ok(url.join(path)?)
    }

    /// Append a repo name and endpoint path onto the server's base URL.
    fn build_url(&self, path: &str) -> Result<Url, EdenApiError> {
        let url = &self.config().server_url;
        // Repo name must be sanitized since it can be set by the user.
        let url = url
            .join(&format!("{}/", encode_repo_name(self.repo_name())))?
            .join(path)?;
        Ok(url)
    }

    /// Add configured values to a request.
    fn configure_request(&self, mut req: Request) -> Result<Request, EdenApiError> {
        // This method should probably not exist. Request
        // configuration should flow through a shared config (i.e.
        // http_client::Config) that is applied by the HttpClient.
        // This way, every use of HttpClient does not its own http
        // config and glue code to apply the config to the request.

        let config = self.config();

        for (k, v) in &config.headers {
            req.set_header(k, v);
        }

        if let Some(ref correlator) = config.correlator {
            req.set_header("X-Client-Correlator", correlator);
        }

        if let Some(timeout) = config.timeout {
            req.set_timeout(timeout);
        }

        if let Some(http_version) = config.http_version {
            req.set_http_version(http_version);
        }

        if let Some(encoding) = &config.encoding {
            req.set_accept_encoding([encoding.clone()]);
        }

        if let Some(mts) = &config.min_transfer_speed {
            req.set_min_transfer_speed(*mts);
        }

        Ok(req)
    }

    /// Prepare a collection of POST requests for the given keys.
    /// The keys will be grouped into batches of the specified size and
    /// passed to the `make_req` callback, which should insert them into
    /// a struct that will be CBOR-encoded and used as the request body.
    fn prepare_requests<T, K, F, R>(
        &self,
        url: &Url,
        keys: K,
        batch_size: Option<usize>,
        mut make_req: F,
    ) -> Result<Vec<Request>, EdenApiError>
    where
        K: IntoIterator<Item = T>,
        F: FnMut(Vec<T>) -> R,
        R: ToWire,
    {
        split_into_batches(keys, batch_size)
            .into_iter()
            .map(|keys| {
                let req = make_req(keys).to_wire();
                self.configure_request(self.inner.client.post(url.clone()))?
                    .cbor(&req)
                    .map_err(EdenApiError::RequestSerializationFailed)
            })
            .collect()
    }

    /// Fetch data from the server without Wire to Api conversion.
    ///
    /// Concurrently performs all of the given HTTP requests, each of
    /// which must result in streaming response of CBOR-encoded values
    /// of type `T`. The metadata of each response will be returned in
    /// the order the responses arrive. The response streams will be
    /// combined into a single stream, in which the returned entries
    /// from different HTTP responses may be arbitrarily interleaved.
    fn fetch_raw<T: DeserializeOwned + Send + 'static>(
        &self,
        requests: Vec<Request>,
    ) -> Result<Response<T>, EdenApiError> {
        let (responses, stats) = self.inner.client.send_async(requests)?;

        // Transform each response `Future` (which resolves when all of the HTTP
        // headers for that response have been received) into a `Stream` that
        // waits until all headers have been received and then starts yielding
        // entries. This allows multiplexing the streams using `select_all`.
        let streams = responses.into_iter().map(|fut| {
            stream::once(async move {
                let res = raise_for_status(fut.await?).await?;
                tracing::debug!("{:?}", ResponseMeta::from(&res));

                LOG_SERVER_INFO_ONCE.call_once(|| {
                    let res_meta = ResponseMeta::from(&res);
                    tracing::debug!(target: "mononoke_info", mononoke_host=res_meta.mononoke_host.unwrap_or_default());
                });

                Ok::<_, EdenApiError>(res.into_body().cbor::<T>().err_into())

            })
            .try_flatten()
            .boxed()
        });

        let entries = stream::select_all(streams).boxed();
        let stats = stats.err_into().boxed();

        Ok(Response { entries, stats })
    }

    /// Fetch data from the server.
    ///
    /// Concurrently performs all of the given HTTP requests, each of
    /// which must result in streaming response of CBOR-encoded values
    /// of type `T`. The metadata of each response will be returned in
    /// the order the responses arrive. The response streams will be
    /// combined into a single stream, in which the returned entries
    /// from different HTTP responses may be arbitrarily interleaved.
    fn fetch<T>(&self, requests: Vec<Request>) -> Result<Response<T>, EdenApiError>
    where
        <T as ToWire>::Wire: Send + DeserializeOwned + 'static,
        T: ToWire + Send + 'static,
    {
        self.fetch_guard::<T>(requests, vec![])
    }

    fn fetch_guard<T>(
        &self,
        requests: Vec<Request>,
        mut guards: Vec<EntranceGuard>,
    ) -> Result<Response<T>, EdenApiError>
    where
        <T as ToWire>::Wire: Send + DeserializeOwned + 'static,
        T: ToWire + Send + 'static,
    {
        guards.push(REQUESTS_INFLIGHT.entrance_guard(requests.len()));
        let Response { entries, stats } = self.fetch_raw::<<T as ToWire>::Wire>(requests)?;

        let stats = metrics::wrap_future_keep_guards(stats, guards).boxed();
        let entries = entries
            .and_then(|v| future::ready(v.to_api().map_err(|e| EdenApiError::from(e.into()))))
            .boxed();

        Ok(Response { entries, stats })
    }

    /// Similar to `fetch`. But returns a `Vec` directly.
    async fn fetch_vec<T>(&self, requests: Vec<Request>) -> Result<Vec<T>, EdenApiError>
    where
        <T as ToWire>::Wire: Send + DeserializeOwned + 'static,
        T: ToWire + Send + 'static,
    {
        self.fetch::<T>(requests)?.flatten().await
    }

    /// Similar to `fetch_vec`. But with retries.
    async fn fetch_vec_with_retry<T>(&self, requests: Vec<Request>) -> Result<Vec<T>, EdenApiError>
    where
        <T as ToWire>::Wire: Send + DeserializeOwned + 'static,
        T: ToWire + Send + 'static,
    {
        self.with_retry(|this| this.fetch_vec::<T>(requests.clone()).boxed())
            .await
    }

    /// Similar to `fetch_vec`. But with retries and a custom progress bar (position drops on a retry).
    async fn fetch_vec_with_retry_and_prog<T>(
        &self,
        requests: Vec<Request>,
        prog: Arc<ProgressBar>,
    ) -> Result<Vec<T>, EdenApiError>
    where
        <T as ToWire>::Wire: Send + DeserializeOwned + 'static,
        T: ToWire + Send + 'static,
    {
        self.with_retry(|this| {
            Box::pin(async {
                let prog = prog.clone();
                prog.set_position(0);
                let resp = this
                    .fetch_guard::<T>(requests.clone(), vec![])?
                    .then(move |v| {
                        prog.increase_position(1);
                        future::ready(v)
                    });
                resp.flatten().await
            })
        })
        .await
    }

    /// Similar to `fetch`, but returns the response type directly, instead of Response<_>.
    async fn fetch_single<T>(&self, request: Request) -> Result<T, EdenApiError>
    where
        <T as ToWire>::Wire: Send + DeserializeOwned + 'static,
        T: ToWire + Send + 'static,
    {
        self.fetch::<T>(vec![request])?.single().await
    }

    /// Log the request to the configured log directory as JSON.
    fn log_request<R: Serialize + Debug>(&self, req: &R, label: &str) {
        tracing::trace!("Sending request: {:?}", req);

        let log_dir = match &self.config().log_dir {
            Some(path) => path.clone(),
            None => return,
        };

        let value: serde_cbor::Value = match serde_cbor::value::to_value(req) {
            Ok(v) => v,
            Err(_e) => return,
        };
        let timestamp = chrono::Local::now().format("%y%m%d_%H%M%S_%f");
        let name = format!("{}_{}.log", &timestamp, label);
        let path = log_dir.join(&name);

        let _ = async_runtime::spawn_blocking(move || {
            if let Err(e) = || -> std::io::Result<()> {
                create_dir_all(&log_dir)?;
                let data = pprint::pformat_value(&value);
                std::fs::write(&path, data)
            }() {
                tracing::warn!("Failed to log request: {:?}", &e);
            }
        });
    }

    pub(crate) async fn fetch_files(
        &self,
        keys: Vec<Key>,
    ) -> Result<Response<FileResponse>, EdenApiError> {
        tracing::info!("Requesting content for {} file(s)", keys.len());

        if keys.is_empty() {
            return Ok(Response::empty());
        }

        let guards = vec![FILES_INFLIGHT.entrance_guard(keys.len())];

        let mut url = self.build_url(paths::FILES2)?;

        if self.config().try_route_consistently && keys.len() == 1 {
            url.set_query(Some(&format!("routing_file={}", keys.first().unwrap())));
            tracing::debug!("Requesting file with a routing key: {}", url);
        }

        let requests = self.prepare_requests(&url, keys, self.config().max_files, |keys| {
            let req = FileRequest { keys, reqs: vec![] };
            self.log_request(&req, "files");
            req
        })?;

        Ok(self.fetch_guard::<FileResponse>(requests, guards)?)
    }

    pub(crate) async fn fetch_trees(
        &self,
        keys: Vec<Key>,
        attributes: Option<TreeAttributes>,
    ) -> Result<Response<Result<TreeEntry, EdenApiServerError>>, EdenApiError> {
        tracing::info!("Requesting {} tree(s)", keys.len());

        if keys.is_empty() {
            return Ok(Response::empty());
        }

        let mut url = self.build_url(paths::TREES)?;

        if self.config().try_route_consistently && keys.len() == 1 {
            url.set_query(Some(&format!("routing_tree={}", keys.first().unwrap())));
            tracing::debug!("Requesting tree with a routing key: {}", url);
        }

        let requests = self.prepare_requests(&url, keys, self.config().max_trees, |keys| {
            let req = TreeRequest {
                keys,
                attributes: attributes.clone().unwrap_or_default(),
            };
            self.log_request(&req, "trees");
            req
        })?;

        Ok(self.fetch::<Result<TreeEntry, EdenApiServerError>>(requests)?)
    }

    pub(crate) async fn fetch_files_attrs(
        &self,
        reqs: Vec<FileSpec>,
    ) -> Result<Response<FileResponse>, EdenApiError> {
        tracing::info!("Requesting attributes for {} file(s)", reqs.len());

        if reqs.is_empty() {
            return Ok(Response::empty());
        }

        let guards = vec![FILES_ATTRS_INFLIGHT.entrance_guard(reqs.len())];

        let mut url = self.build_url(paths::FILES2)?;
        if self.config().try_route_consistently && reqs.len() == 1 {
            url.set_query(Some(&format!("routing_file={}", reqs.first().unwrap().key)));
            tracing::debug!("Requesting file with a routing key: {}", url);
        }

        let requests = self.prepare_requests(&url, reqs, self.config().max_files, |reqs| {
            let req = FileRequest { reqs, keys: vec![] };
            self.log_request(&req, "files");
            req
        })?;

        Ok(self.fetch_guard::<FileResponse>(requests, guards)?)
    }

    /// Upload a single file
    async fn process_single_file_upload(
        &self,
        item: AnyFileContentId,
        raw_content: Bytes,
        bubble_id: Option<NonZeroU64>,
    ) -> Result<Response<UploadToken>, EdenApiError> {
        let mut url = self.build_url(paths::UPLOAD)?;
        url = url.join("file/")?;
        match item {
            AnyFileContentId::ContentId(id) => {
                url = url.join("content_id/")?.join(&format!("{}", id))?;
            }
            AnyFileContentId::Sha1(id) => {
                url = url.join("sha1/")?.join(&format!("{}", id))?;
            }
            AnyFileContentId::Sha256(id) => {
                url = url.join("sha256/")?.join(&format!("{}", id))?;
            }
            AnyFileContentId::SeededBlake3(id) => {
                url = url.join("seeded_blake3/")?.join(&format!("{}", id))?;
            }
        }

        {
            let mut query = url.query_pairs_mut();
            query.append_pair("content_size", &raw_content.len().to_string());
            if let Some(bubble_id) = bubble_id {
                query.append_pair("bubble_id", &bubble_id.to_string());
            }
        }

        let msg = format!("Requesting upload for {}", url);
        tracing::info!("{}", &msg);

        Ok(self.fetch::<UploadToken>(vec![{
            self.configure_request(self.inner.client.put(url.clone()))?
                .body(raw_content.to_vec())
        }])?)
    }

    async fn clone_data_attempt(&self) -> Result<CloneData<HgId>, EdenApiError> {
        let url = self.build_url(paths::CLONE_DATA)?;
        let req = self.configure_request(self.inner.client.post(url))?;
        let mut fetch = self.fetch::<CloneData<HgId>>(vec![req])?;
        fetch.entries.next().await.ok_or_else(|| {
            EdenApiError::Other(format_err!("clone data missing from reponse body"))
        })?
    }

    async fn pull_lazy_attempt(
        &self,
        req: PullLazyRequest,
    ) -> Result<CloneData<HgId>, EdenApiError> {
        let url = self.build_url(paths::PULL_LAZY)?;
        let req = self
            .configure_request(self.inner.client.post(url))?
            .cbor(&req.to_wire())
            .map_err(EdenApiError::RequestSerializationFailed)?;
        let mut fetch = self.fetch::<CloneData<HgId>>(vec![req])?;
        fetch.entries.next().await.ok_or_else(|| {
            EdenApiError::Other(format_err!("clone data missing from reponse body"))
        })?
    }

    async fn fast_forward_pull_attempt(
        &self,
        req: PullFastForwardRequest,
    ) -> Result<CloneData<HgId>, EdenApiError> {
        let url = self.build_url(paths::PULL_FAST_FORWARD)?;
        let req = self
            .configure_request(self.inner.client.post(url))?
            .cbor(&req.to_wire())
            .map_err(EdenApiError::RequestSerializationFailed)?;
        let mut fetch = self.fetch::<CloneData<HgId>>(vec![req])?;
        fetch.entries.next().await.ok_or_else(|| {
            EdenApiError::Other(format_err!("clone data missing from reponse body"))
        })?
    }

    async fn with_retry<'t, T>(
        &'t self,
        func: impl Fn(&'t Self) -> BoxFuture<'t, Result<T, EdenApiError>>,
    ) -> Result<T, EdenApiError> {
        let retry_count = self.inner.config.max_retry_per_request;
        with_retry(retry_count, || func(self)).await
    }
}

#[async_trait]
impl EdenApi for Client {
    fn url(&self) -> Option<String> {
        Some(self.config().server_url.to_string())
    }

    async fn health(&self) -> Result<ResponseMeta, EdenApiError> {
        let url = self.build_url_repoless(paths::HEALTH_CHECK)?;

        tracing::info!("Sending health check request: {}", &url);

        let req = self.configure_request(self.inner.client.get(url))?;
        let res = raise_for_status(req.send_async().await?).await?;

        Ok(ResponseMeta::from(&res))
    }

    async fn capabilities(&self) -> Result<Vec<String>, EdenApiError> {
        tracing::info!("Requesting capabilities for repo {}", &self.repo_name());
        let url = self.build_url("capabilities")?;
        let req = self.configure_request(self.inner.client.get(url))?;
        let res = raise_for_status(req.send_async().await?).await?;
        let body: Vec<u8> = res.into_body().decoded().try_concat().await?;
        let caps = serde_json::from_slice(&body)
            .map_err(|e| EdenApiError::ParseResponse(e.to_string()))?;
        Ok(caps)
    }

    async fn files(&self, keys: Vec<Key>) -> Result<Response<FileResponse>, EdenApiError> {
        tracing::info!("Requesting content for {} file(s)", keys.len());

        let prog = self.inner.file_progress.create_or_extend(keys.len() as u64);

        RetryableFiles::new(keys)
            .perform_with_retries(self.clone())
            .and_then(|r| async {
                Ok(r.then(move |r| {
                    prog.increase_position(1);
                    ready(r)
                }))
            })
            .await
    }

    async fn files_attrs(
        &self,
        reqs: Vec<FileSpec>,
    ) -> Result<Response<FileResponse>, EdenApiError> {
        tracing::info!("Requesting attributes for {} file(s)", reqs.len());

        let prog = self.inner.file_progress.create_or_extend(reqs.len() as u64);

        RetryableFileAttrs::new(reqs)
            .perform_with_retries(self.clone())
            .and_then(|r| async {
                Ok(r.then(move |r| {
                    prog.increase_position(1);
                    ready(r)
                }))
            })
            .await
    }

    async fn history(
        &self,
        keys: Vec<Key>,
        length: Option<u32>,
    ) -> Result<Response<HistoryEntry>, EdenApiError> {
        tracing::info!("Requesting history for {} file(s)", keys.len());

        if keys.is_empty() {
            return Ok(Response::empty());
        }

        let url = self.build_url(paths::HISTORY)?;
        let requests = self.prepare_requests(&url, keys, self.config().max_history, |keys| {
            let req = HistoryRequest { keys, length };
            self.log_request(&req, "history");
            req
        })?;

        let Response { entries, stats } = self.fetch::<HistoryResponseChunk>(requests)?;

        // Convert received `HistoryResponseChunk`s into `HistoryEntry`s.
        let entries = entries
            .map_ok(|entries| stream::iter(entries.into_iter().map(Ok)))
            .try_flatten()
            .boxed();

        Ok(Response { entries, stats })
    }

    async fn trees(
        &self,
        keys: Vec<Key>,
        attributes: Option<TreeAttributes>,
    ) -> Result<Response<Result<TreeEntry, EdenApiServerError>>, EdenApiError> {
        tracing::info!("Requesting {} tree(s)", keys.len());

        let prog = self.inner.tree_progress.create_or_extend(keys.len() as u64);

        RetryableTrees::new(keys, attributes)
            .perform_with_retries(self.clone())
            .and_then(|r| async {
                Ok(r.then(move |r| {
                    prog.increase_position(1);
                    ready(r)
                }))
            })
            .await
    }

    async fn commit_revlog_data(
        &self,
        hgids: Vec<HgId>,
    ) -> Result<Response<CommitRevlogData>, EdenApiError> {
        tracing::info!("Requesting revlog data for {} commit(s)", hgids.len());

        let url = self.build_url(paths::COMMIT_REVLOG_DATA)?;
        let commit_revlog_data_req = CommitRevlogDataRequest { hgids };

        self.log_request(&commit_revlog_data_req, "commit_revlog_data");

        let req = self
            .configure_request(self.inner.client.post(url))?
            .cbor(&commit_revlog_data_req)
            .map_err(EdenApiError::RequestSerializationFailed)?;

        self.fetch_raw::<CommitRevlogData>(vec![req])
    }

    async fn hash_prefixes_lookup(
        &self,
        prefixes: Vec<String>,
    ) -> Result<Vec<CommitHashLookupResponse>, EdenApiError> {
        tracing::info!("Requesting full hashes for {} prefix(es)", prefixes.len());
        let url = self.build_url(paths::COMMIT_HASH_LOOKUP)?;
        let prefixes: Vec<CommitHashLookupRequest> = prefixes
            .into_iter()
            .map(|prefix| make_hash_lookup_request(prefix))
            .collect::<Result<Vec<CommitHashLookupRequest>, _>>()?;
        let requests = self.prepare_requests(
            &url,
            prefixes,
            Some(MAX_CONCURRENT_HASH_LOOKUPS_PER_REQUEST),
            |prefixes| Batch::<_> { batch: prefixes },
        )?;
        self.fetch_vec_with_retry::<CommitHashLookupResponse>(requests)
            .await
    }

    async fn bookmarks(&self, bookmarks: Vec<String>) -> Result<Vec<BookmarkEntry>, EdenApiError> {
        tracing::info!("Requesting '{}' bookmarks", bookmarks.len());
        let url = self.build_url(paths::BOOKMARKS)?;
        let bookmark_req = BookmarkRequest { bookmarks };
        self.log_request(&bookmark_req, "bookmarks");
        let req = self
            .configure_request(self.inner.client.post(url))?
            .cbor(&bookmark_req.to_wire())
            .map_err(EdenApiError::RequestSerializationFailed)?;

        self.fetch_vec_with_retry::<BookmarkEntry>(vec![req]).await
    }

    async fn set_bookmark(
        &self,
        bookmark: String,
        to: Option<HgId>,
        from: Option<HgId>,
        pushvars: HashMap<String, String>,
    ) -> Result<(), EdenApiError> {
        tracing::info!("Set bookmark '{}' from {:?} to {:?}", &bookmark, from, to);
        let url = self.build_url(paths::SET_BOOKMARK)?;
        let set_bookmark_req = SetBookmarkRequest {
            bookmark,
            to,
            from,
            pushvars: pushvars
                .into_iter()
                .map(|(k, v)| PushVar { key: k, value: v })
                .collect(),
        };
        self.log_request(&set_bookmark_req, "set_bookmark");
        let req = self
            .configure_request(self.inner.client.post(url))?
            .cbor(&set_bookmark_req.to_wire())
            .map_err(EdenApiError::RequestSerializationFailed)?;

        self.fetch_single::<()>(req).await
    }

    /// Land a stack of commits, rebasing them onto the specified bookmark
    /// and updating the bookmark to the top of the rebased stack
    async fn land_stack(
        &self,
        bookmark: String,
        head: HgId,
        base: HgId,
        pushvars: HashMap<String, String>,
    ) -> Result<LandStackResponse, EdenApiError> {
        tracing::info!(
            "Landing stack between head {} and base {} to bookmark '{}'",
            head,
            base,
            &bookmark
        );
        let url = self.build_url(paths::LAND_STACK)?;

        let land_stack_req = LandStackRequest {
            bookmark,
            head,
            base,
            pushvars: pushvars
                .into_iter()
                .map(|(k, v)| PushVar { key: k, value: v })
                .collect(),
        };
        self.log_request(&land_stack_req, "land");
        let req = self
            .configure_request(self.inner.client.post(url))?
            .cbor(&land_stack_req.to_wire())
            .map_err(EdenApiError::RequestSerializationFailed)?;

        self.fetch_single::<LandStackResponse>(req).await
    }

    async fn clone_data(&self) -> Result<CloneData<HgId>, EdenApiError> {
        tracing::info!(
            "Requesting clone data for the '{}' repository",
            self.repo_name(),
        );
        self.with_retry(|this| this.clone_data_attempt().boxed())
            .await
    }

    async fn pull_fast_forward_master(
        &self,
        old_master: HgId,
        new_master: HgId,
    ) -> Result<CloneData<HgId>, EdenApiError> {
        tracing::info!(
            "Requesting pull fast forward data for the '{}' repository",
            self.repo_name()
        );

        self.with_retry(|this| {
            let req = PullFastForwardRequest {
                old_master,
                new_master,
            };
            this.fast_forward_pull_attempt(req).boxed()
        })
        .await
    }

    async fn pull_lazy(
        &self,
        common: Vec<HgId>,
        missing: Vec<HgId>,
    ) -> Result<CloneData<HgId>, EdenApiError> {
        tracing::info!(
            "Requesting pull lazy data for the '{}' repository",
            self.repo_name()
        );

        self.with_retry(|this| {
            let req = PullLazyRequest {
                common: common.clone(),
                missing: missing.clone(),
            };
            this.pull_lazy_attempt(req).boxed()
        })
        .await
    }

    async fn commit_location_to_hash(
        &self,
        requests: Vec<CommitLocationToHashRequest>,
    ) -> Result<Vec<CommitLocationToHashResponse>, EdenApiError> {
        tracing::info!(
            "Requesting commit location to hash (batch size = {})",
            requests.len()
        );
        if requests.is_empty() {
            return Ok(Vec::new());
        }

        let url = self.build_url(paths::COMMIT_LOCATION_TO_HASH)?;

        let formatted = self.prepare_requests(
            &url,
            requests,
            self.config().max_location_to_hash,
            |requests| {
                let batch = CommitLocationToHashRequestBatch { requests };
                self.log_request(&batch, "commit_location_to_hash");
                batch
            },
        )?;

        self.fetch_vec_with_retry::<CommitLocationToHashResponse>(formatted)
            .await
    }

    async fn commit_hash_to_location(
        &self,
        master_heads: Vec<HgId>,
        hgids: Vec<HgId>,
    ) -> Result<Vec<CommitHashToLocationResponse>, EdenApiError> {
        tracing::info!(
            "Requesting commit hash to location (batch size = {})",
            hgids.len()
        );

        if hgids.is_empty() {
            return Ok(Vec::new());
        }

        let url = self.build_url(paths::COMMIT_HASH_TO_LOCATION)?;

        let formatted =
            self.prepare_requests(&url, hgids, self.config().max_location_to_hash, |hgids| {
                let batch = CommitHashToLocationRequestBatch {
                    master_heads: master_heads.clone(),
                    hgids,
                    unfiltered: Some(true),
                };
                self.log_request(&batch, "commit_hash_to_location");
                batch
            })?;

        self.fetch_vec_with_retry::<CommitHashToLocationResponse>(formatted)
            .await
    }

    async fn commit_known(
        &self,
        hgids: Vec<HgId>,
    ) -> Result<Vec<CommitKnownResponse>, EdenApiError> {
        let anyids: Vec<_> = hgids
            .iter()
            .cloned()
            .map(|hgid| AnyId::HgChangesetId(hgid))
            .collect();
        let entries = self.lookup_batch(anyids.clone(), None, None).await?;

        let into_hgid = |id: IndexableId| match id.id {
            AnyId::HgChangesetId(hgid) => Ok(hgid),
            _ => Err(EdenApiError::Other(format_err!("Invalid id returned"))),
        };

        let id_to_token: HashMap<HgId, Option<UploadToken>> = entries
            .into_iter()
            .map(|response| match response.result {
                LookupResult::NotPresent(id) => Ok((into_hgid(id)?, None)),
                LookupResult::Present(token) => Ok((into_hgid(token.indexable_id())?, Some(token))),
            })
            .collect::<Result<_, EdenApiError>>()?;

        Ok(hgids
            .into_iter()
            .map(|hgid| match id_to_token.get(&hgid) {
                Some(value) => CommitKnownResponse {
                    hgid,
                    known: Ok(value.is_some()),
                },
                None => CommitKnownResponse {
                    hgid,
                    known: Err(ServerError::generic(
                        "the server cannot check `HgChangesetId`",
                    )),
                },
            })
            .collect::<Vec<CommitKnownResponse>>())
    }

    async fn commit_graph(
        &self,
        heads: Vec<HgId>,
        common: Vec<HgId>,
    ) -> Result<Vec<CommitGraphEntry>, EdenApiError> {
        tracing::info!(
            "Requesting commit graph with {} heads and {} common",
            heads.len(),
            common.len(),
        );
        let url = self.build_url(paths::COMMIT_GRAPH_V2)?;
        let graph_req = CommitGraphRequest { heads, common };
        self.log_request(&graph_req, "commit_graph");
        let wire_graph_req = graph_req.to_wire();

        // In the current implementation, server may send all CommitGraph nodes
        // at once on completion, or may send graph nodes gradually (streaming).
        // Since, it depends on request, min speed transfer check must be disabled.
        // Since we have a special progress bar and response is small, let's disable compression of
        // response's body.
        let req = self
            .configure_request(self.inner.client.post(url))?
            .accept_encoding([Encoding::Identity])
            .min_transfer_speed(None)
            .cbor(&wire_graph_req)
            .map_err(EdenApiError::RequestSerializationFailed)?;

        let prog = ProgressBar::register_new("commit graph", 0, "commits fetched");
        self.fetch_vec_with_retry_and_prog::<CommitGraphEntry>(vec![req], prog)
            .await
    }

    async fn commit_graph_segments(
        &self,
        heads: Vec<HgId>,
        common: Vec<HgId>,
    ) -> Result<Vec<CommitGraphSegmentsEntry>, EdenApiError> {
        tracing::info!(
            "Requesting commit graph segments with {} heads and {} common",
            heads.len(),
            common.len(),
        );
        let url = self.build_url(paths::COMMIT_GRAPH_SEGMENTS)?;
        let graph_req = CommitGraphSegmentsRequest { heads, common };
        self.log_request(&graph_req, "commit_graph_segments");
        let wire_graph_req = graph_req.to_wire();

        let req = self
            .configure_request(self.inner.client.post(url))?
            .min_transfer_speed(None)
            .cbor(&wire_graph_req)
            .map_err(EdenApiError::RequestSerializationFailed)?;

        self.fetch_vec_with_retry::<CommitGraphSegmentsEntry>(vec![req])
            .await
    }

    async fn lookup_batch(
        &self,
        items: Vec<AnyId>,
        bubble_id: Option<NonZeroU64>,
        copy_from_bubble_id: Option<NonZeroU64>,
    ) -> Result<Vec<LookupResponse>, EdenApiError> {
        tracing::info!("Requesting lookup for {} item(s)", items.len());

        if items.is_empty() {
            return Ok(Vec::new());
        }

        let url = self.build_url(paths::LOOKUP)?;
        let requests = self.prepare_requests(
            &url,
            items,
            Some(MAX_CONCURRENT_LOOKUPS_PER_REQUEST),
            |ids| Batch::<LookupRequest> {
                batch: ids
                    .into_iter()
                    .map(|id| LookupRequest {
                        id,
                        bubble_id,
                        copy_from_bubble_id,
                    })
                    .collect(),
            },
        )?;

        self.fetch_vec_with_retry::<LookupResponse>(requests).await
    }

    async fn process_files_upload(
        &self,
        data: Vec<(AnyFileContentId, Bytes)>,
        bubble_id: Option<NonZeroU64>,
        copy_from_bubble_id: Option<NonZeroU64>,
    ) -> Result<Response<UploadToken>, EdenApiError> {
        if data.is_empty() {
            return Ok(Response::empty());
        }
        // Filter already uploaded file contents first

        let mut uploaded_ids = HashSet::new();
        let mut uploaded_tokens: Vec<UploadToken> = vec![];

        let anyids: Vec<_> = data
            .iter()
            .map(|(id, _data)| AnyId::AnyFileContentId(id.clone()))
            .collect();

        let entries = self
            .lookup_batch(anyids.clone(), bubble_id, copy_from_bubble_id)
            .await?;
        for entry in entries {
            if let LookupResult::Present(token) = entry.result {
                uploaded_ids.insert(token.indexable_id());
                uploaded_tokens.push(token);
            }
        }

        let msg = format!(
            "Received {} token(s) from the lookup_batch request",
            uploaded_tokens.len()
        );
        tracing::info!("{}", &msg);

        // Upload the rest of the contents in parallel
        let new_tokens = stream::iter(
            data.into_iter()
                .filter(|(id, _content)| {
                    !uploaded_ids.contains(&IndexableId {
                        id: AnyId::AnyFileContentId(id.clone()),
                        bubble_id,
                    })
                })
                .map(|(id, content)| async move {
                    self.process_single_file_upload(id, content, bubble_id)
                        .await?
                        .entries
                        .next()
                        .await
                        .ok_or_else(|| {
                            EdenApiError::Other(format_err!(
                                "token data is missing from the reponse body for {}",
                                id
                            ))
                        })?
                }),
        )
        .buffer_unordered(MAX_CONCURRENT_FILE_UPLOADS)
        .collect::<Vec<_>>()
        .await;

        let msg = format!(
            "Received {} new token(s) from upload requests",
            new_tokens.iter().filter(|x| x.is_ok()).count()
        );
        tracing::info!("{}", &msg);

        // Merge all the tokens together
        let all_tokens = new_tokens
            .into_iter()
            .chain(uploaded_tokens.into_iter().map(|token| Ok(token)))
            .collect::<Vec<Result<_, _>>>();

        Ok(Response {
            stats: Box::pin(async { Ok(Default::default()) }),
            entries: Box::pin(futures::stream::iter(all_tokens)),
        })
    }

    async fn upload_filenodes_batch(
        &self,
        items: Vec<HgFilenodeData>,
    ) -> Result<Response<UploadTokensResponse>, EdenApiError> {
        tracing::info!("Requesting hg filenodes upload for {} item(s)", items.len());

        if items.is_empty() {
            return Ok(Response::empty());
        }

        let url = self.build_url(paths::UPLOAD_FILENODES)?;
        let requests = self.prepare_requests(
            &url,
            items,
            Some(MAX_CONCURRENT_UPLOAD_FILENODES_PER_REQUEST),
            |ids| Batch::<_> {
                batch: ids
                    .into_iter()
                    .map(|item| UploadHgFilenodeRequest { data: item })
                    .collect(),
            },
        )?;

        Ok(self.fetch::<UploadTokensResponse>(requests)?)
    }

    async fn upload_trees_batch(
        &self,
        items: Vec<UploadTreeEntry>,
    ) -> Result<Response<UploadTreeResponse>, EdenApiError> {
        tracing::info!("Requesting trees upload for {} item(s)", items.len());

        if items.is_empty() {
            return Ok(Response::empty());
        }

        let url = self.build_url(paths::UPLOAD_TREES)?;
        let requests = self.prepare_requests(
            &url,
            items,
            Some(MAX_CONCURRENT_UPLOAD_TREES_PER_REQUEST),
            |ids| Batch::<_> {
                batch: ids
                    .into_iter()
                    .map(|item| UploadTreeRequest { entry: item })
                    .collect(),
            },
        )?;

        Ok(self.fetch::<UploadTreeResponse>(requests)?)
    }

    // the request isn't batched, batching should be done outside if needed
    async fn upload_changesets(
        &self,
        changesets: Vec<UploadHgChangeset>,
        mutations: Vec<HgMutationEntryContent>,
    ) -> Result<Response<UploadTokensResponse>, EdenApiError> {
        tracing::info!(
            "Requesting changesets upload for {} item(s)",
            changesets.len(),
        );

        if changesets.is_empty() {
            return Ok(Response::empty());
        }

        let url = self.build_url(paths::UPLOAD_CHANGESETS)?;
        let req = UploadHgChangesetsRequest {
            changesets,
            mutations,
        }
        .to_wire();

        let request = self
            .configure_request(self.inner.client.post(url))?
            .cbor(&req)
            .map_err(EdenApiError::RequestSerializationFailed)?;

        Ok(self.fetch::<UploadTokensResponse>(vec![request])?)
    }

    async fn upload_bonsai_changeset(
        &self,
        changeset: BonsaiChangesetContent,
        bubble_id: Option<std::num::NonZeroU64>,
    ) -> Result<Response<UploadTokensResponse>, EdenApiError> {
        tracing::info!("Requesting changeset upload");

        let mut url = self.build_url(paths::UPLOAD_BONSAI_CHANGESET)?;
        if let Some(bubble_id) = bubble_id {
            url.query_pairs_mut()
                .append_pair("bubble_id", &bubble_id.to_string());
        }
        let req = UploadBonsaiChangesetRequest { changeset }.to_wire();

        let request = self
            .configure_request(self.inner.client.post(url.clone()))?
            .cbor(&req)
            .map_err(EdenApiError::RequestSerializationFailed)?;

        Ok(self.fetch::<UploadTokensResponse>(vec![request])?)
    }

    async fn ephemeral_prepare(
        &self,
        custom_duration: Option<Duration>,
        labels: Option<Vec<String>>,
    ) -> Result<Response<EphemeralPrepareResponse>, EdenApiError> {
        tracing::info!("Preparing ephemeral bubble");
        let url = self.build_url(paths::EPHEMERAL_PREPARE)?;
        let req = EphemeralPrepareRequest {
            custom_duration_secs: custom_duration.map(|d| d.as_secs()),
            labels,
        }
        .to_wire();
        let request = self
            .configure_request(self.inner.client.post(url))?
            .cbor(&req)
            .map_err(EdenApiError::RequestSerializationFailed)?;

        let mut fetch = self.fetch::<EphemeralPrepareResponse>(vec![request])?;
        fetch.entries = fetch
            .entries
            .inspect_ok(|r| tracing::info!("Created bubble {}", r.bubble_id))
            .boxed();

        Ok(fetch)
    }

    async fn fetch_snapshot(
        &self,
        request: FetchSnapshotRequest,
    ) -> Result<Response<FetchSnapshotResponse>, EdenApiError> {
        tracing::info!("Fetching snapshot {}", request.cs_id,);
        let url = self.build_url(paths::FETCH_SNAPSHOT)?;
        let req = request.to_wire();
        let request = self
            .configure_request(self.inner.client.post(url))?
            .cbor(&req)
            .map_err(EdenApiError::RequestSerializationFailed)?;

        Ok(self.fetch::<FetchSnapshotResponse>(vec![request])?)
    }

    /// Alter the properties of an existing snapshot
    async fn alter_snapshot(
        &self,
        request: AlterSnapshotRequest,
    ) -> Result<Response<AlterSnapshotResponse>, EdenApiError> {
        tracing::info!("Altering snapshot {}", request.cs_id,);
        let url = self.build_url(paths::ALTER_SNAPSHOT)?;
        let req = request.to_wire();
        let request = self
            .configure_request(self.inner.client.post(url))?
            .cbor(&req)
            .map_err(EdenApiError::RequestSerializationFailed)?;

        Ok(self.fetch::<AlterSnapshotResponse>(vec![request])?)
    }

    async fn download_file(&self, token: UploadToken) -> Result<Bytes, EdenApiError> {
        tracing::info!("Downloading file");
        let url = self.build_url(paths::DOWNLOAD_FILE)?;
        let metadata = token.data.metadata.clone();
        let req = token.to_wire();
        let request = self
            .configure_request(self.inner.client.post(url.clone()))?
            .cbor(&req)
            .map_err(EdenApiError::RequestSerializationFailed)?;

        use bytes::BytesMut;
        let buf = if let Some(UploadTokenMetadata::FileContentTokenMetadata(m)) = metadata {
            BytesMut::with_capacity(m.content_size.try_into().unwrap_or_default())
        } else {
            BytesMut::new()
        };

        Ok(self
            .fetch::<RawBytes>(vec![request])?
            .entries
            .try_fold(buf, |mut buf, chunk| async move {
                buf.extend_from_slice(&chunk);
                Ok(buf)
            })
            .await?
            .freeze()
            .into())
    }

    async fn commit_mutations(
        &self,
        commits: Vec<HgId>,
    ) -> Result<Vec<CommitMutationsResponse>, EdenApiError> {
        tracing::info!("Requesting mutation info for {} commit(s)", commits.len());
        let url = self.build_url(paths::COMMIT_MUTATIONS)?;
        let requests = self.prepare_requests(
            &url,
            commits,
            self.config().max_commit_mutations,
            |commits| {
                let req = CommitMutationsRequest { commits };
                self.log_request(&req, "commit_mutations");
                req
            },
        )?;

        self.fetch_vec_with_retry::<CommitMutationsResponse>(requests)
            .await
    }

    async fn commit_translate_id(
        &self,
        commits: Vec<CommitId>,
        scheme: CommitIdScheme,
        from_repo: Option<String>,
        to_repo: Option<String>,
    ) -> Result<Response<CommitTranslateIdResponse>, EdenApiError> {
        tracing::info!(
            "Requesting commit id translation for {} commits into {:?}",
            commits.len(),
            scheme
        );
        let url = self.build_url(paths::COMMIT_TRANSLATE_ID)?;
        let requests = self.prepare_requests(
            &url,
            commits,
            self.config().max_commit_translate_id,
            |commits| {
                let req = CommitTranslateIdRequest {
                    commits,
                    scheme,
                    from_repo: from_repo.clone(),
                    to_repo: to_repo.clone(),
                };
                self.log_request(&req, "commit_translate_id");
                req
            },
        )?;
        Ok(self.fetch::<CommitTranslateIdResponse>(requests)?)
    }

    async fn blame(&self, files: Vec<Key>) -> Result<Response<BlameResult>, EdenApiError> {
        tracing::info!("Blaming {} file(s)", files.len());

        if files.is_empty() {
            return Ok(Response::empty());
        }

        let url = self.build_url(paths::BLAME)?;
        let requests = self.prepare_requests(
            &url,
            files,
            Some(MAX_CONCURRENT_BLAMES_PER_REQUEST),
            |files| {
                let req = BlameRequest { files };
                self.log_request(&req, "blame");
                req
            },
        )?;

        Ok(self.fetch::<BlameResult>(requests)?)
    }
}

/// Split up a collection of keys into batches of at most `batch_size`.
fn split_into_batches<T>(
    keys: impl IntoIterator<Item = T>,
    batch_size: Option<usize>,
) -> Vec<Vec<T>> {
    match batch_size {
        Some(n) => keys
            .into_iter()
            .chunks(n)
            .into_iter()
            .map(Vec::from_iter)
            .collect(),
        None => vec![keys.into_iter().collect()],
    }
}

async fn raise_for_status(res: AsyncResponse) -> Result<AsyncResponse, EdenApiError> {
    let status = res.status();
    if status.as_u16() < 400 {
        return Ok(res);
    }

    let url = res.url().to_string();
    let (head, body) = res.into_parts();
    let body = body.decoded().try_concat().await?;
    let mut message = String::from_utf8_lossy(&body).into_owned();

    if message.len() >= 9 && &*message[..9].to_lowercase() == "<!doctype" {
        message = "HTML content omitted (this error may have come from a proxy server)".into();
    } else if message.len() > MAX_ERROR_MSG_LEN {
        message.truncate(MAX_ERROR_MSG_LEN);
        message.push_str("... (truncated)")
    }

    let headers = head.headers().clone();
    Err(EdenApiError::HttpError {
        status,
        message,
        headers,
        url,
    })
}

async fn with_retry<'t, T>(
    max_retry_count: usize,
    func: impl Fn() -> BoxFuture<'t, Result<T, EdenApiError>>,
) -> Result<T, EdenApiError> {
    let mut attempt = 0usize;
    loop {
        let result = func().await;
        if attempt >= max_retry_count {
            return result;
        }
        match result {
            Ok(result) => return Ok(result),
            Err(ref error) => {
                if !error.is_retryable() {
                    return result;
                }
                tracing::warn!("Retrying http error {:?}", error);
                tokio::time::sleep(Duration::from_secs(1)).await;
            }
        }
        attempt += 1;
    }
}

#[cfg(test)]
mod tests {
    use anyhow::Result;

    use crate::builder::HttpClientBuilder;

    #[test]
    fn test_url_escaping() -> Result<()> {
        let base_url = "https://example.com".parse()?;
        let repo_name = "repo_-. !@#$% foo \u{1f4a9} bar";

        let client = HttpClientBuilder::new()
            .repo_name(repo_name)
            .server_url(base_url)
            .build()?;

        let path = "path";
        let url: String = client.build_url(path)?.into();
        let expected =
            "https://example.com/repo_-.%20%21%40%23%24%25%20foo%20%F0%9F%92%A9%20bar/path";
        assert_eq!(&url, &expected);

        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 builder;
mod client;
mod response;
mod retryable;

// Re-export for convenience.
pub use configmodel;
pub use edenapi_trait::api;
pub use edenapi_trait::errors;
pub use edenapi_trait::Entries;
pub use edenapi_trait::Response;
pub use edenapi_trait::ResponseMeta;
pub use edenapi_types as types;
pub use http_client::Stats;

pub use crate::api::EdenApi;
pub use crate::builder::Builder;
pub use crate::builder::HttpClientBuilder;
pub use crate::builder::DEFAULT_CORRELATOR;
pub use crate::client::Client;
pub use crate::errors::ConfigError;
pub use crate::errors::EdenApiError;
pub use crate::response::BlockingResponse;

pub type Result<T> = std::result::Result<T, EdenApiError>;


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

use anyhow::Context;
use async_runtime::block_unless_interrupted;
pub use edenapi_trait::Response;
pub use edenapi_trait::ResponseMeta;
use futures::prelude::*;
use http_client::Stats;

use crate::errors::EdenApiError;

/// Non-async version of `Response`.
pub struct BlockingResponse<T> {
    pub entries: Vec<T>,
    pub stats: Stats,
}

impl<T> BlockingResponse<T> {
    pub fn from_async<F>(fetch: F) -> Result<Self, EdenApiError>
    where
        F: Future<Output = Result<Response<T>, EdenApiError>>,
    {
        let Response { entries, stats } =
            block_unless_interrupted(fetch).context("transfer interrupted by user")??;
        let entries = block_unless_interrupted(entries.try_collect())
            .context("transfer interrupted by user")??;
        let stats = block_unless_interrupted(stats).context("transfer interrupted by user")??;
        Ok(Self { entries, stats })
    }
}


/*
 * 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 async_trait::async_trait;
use types::Key;

use super::RetryableStreamRequest;
use crate::client::Client;
use crate::errors::EdenApiError;
use crate::response::Response;
use crate::types::FileResponse;
use crate::types::FileSpec;

pub(crate) struct RetryableFiles {
    keys: HashSet<Key>,
}

impl RetryableFiles {
    pub(crate) fn new(keys: impl IntoIterator<Item = Key>) -> Self {
        let keys = keys.into_iter().collect();
        Self { keys }
    }
}

#[async_trait]
impl RetryableStreamRequest for RetryableFiles {
    type Item = FileResponse;

    async fn perform(&self, client: Client) -> Result<Response<Self::Item>, EdenApiError> {
        let keys = self.keys.iter().cloned().collect();
        client.fetch_files(keys).await
    }

    fn received_item(&mut self, item: &Self::Item) {
        self.keys.remove(&item.key);
    }
}

pub(crate) struct RetryableFileAttrs {
    reqs: HashMap<Key, FileSpec>,
}

impl RetryableFileAttrs {
    pub(crate) fn new(reqs: impl IntoIterator<Item = FileSpec>) -> Self {
        let reqs = reqs.into_iter().map(|req| (req.key.clone(), req)).collect();
        Self { reqs }
    }
}

#[async_trait]
impl RetryableStreamRequest for RetryableFileAttrs {
    type Item = FileResponse;

    async fn perform(&self, client: Client) -> Result<Response<Self::Item>, EdenApiError> {
        let reqs = self.reqs.values().cloned().collect();
        client.fetch_files_attrs(reqs).await
    }

    fn received_item(&mut self, item: &Self::Item) {
        self.reqs.remove(&item.key);
    }
}


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

use std::time::Duration;

use async_trait::async_trait;
use edenapi_trait::Entries;
use edenapi_trait::Response;
use futures::channel::mpsc;
use futures::prelude::*;
use http_client::Stats;

use crate::client::Client;
use crate::errors::EdenApiError;

mod files;
mod trees;

pub(crate) use files::RetryableFileAttrs;
pub(crate) use files::RetryableFiles;
pub(crate) use trees::RetryableTrees;

#[async_trait]
pub(crate) trait RetryableStreamRequest: Sized + Sync + Send + 'static {
    type Item: Send + 'static;

    async fn perform(&self, client: Client) -> Result<Response<Self::Item>, EdenApiError>;

    fn received_item(&mut self, _item: &Self::Item) {}

    fn retry_after(
        &mut self,
        error: &EdenApiError,
        attempt: usize,
        max: usize,
    ) -> Option<Duration> {
        if error.is_retryable() && attempt < max {
            Some(Duration::from_secs(attempt as u64 + 1))
        } else {
            None
        }
    }

    async fn perform_with_retries(
        self,
        client: Client,
    ) -> Result<Response<Self::Item>, EdenApiError> {
        struct RetryState<R, T> {
            request: R,
            entries: Option<Entries<T>>,
            attempt: usize,
        }

        let state = RetryState {
            request: self,
            entries: None,
            attempt: 0,
        };

        let (stats_tx, stats_rx) = mpsc::channel(0);
        let entries = stream::unfold(state, move |mut state| {
            let mut stats_tx = stats_tx.clone();
            let client = client.clone();

            async move {
                let max_attempts = client.config().max_retry_per_request;
                loop {
                    // Ideally we'd return None when we hit the final error, but we need to use
                    // that time to return the error, so instead we return None on this next
                    // iteration.
                    if state.attempt > max_attempts {
                        return None;
                    }

                    let res = if let Some(ref mut entries) = state.entries {
                        tracing::trace!("Polling response stream");
                        if let Some(res) = entries.next().await {
                            tracing::trace!("Item received");
                            res
                        } else {
                            tracing::trace!("Transfer complete");
                            return None;
                        }
                    } else {
                        tracing::trace!("No active response stream; sending new request");
                        let res = state.request.perform(client.clone()).await;
                        match res {
                            Ok(Response { entries, stats, .. }) => {
                                state.entries = Some(entries);
                                let _ = stats_tx.try_send(stats);
                                continue;
                            }
                            Err(e) => Err(e),
                        }
                    };

                    let error = match res {
                        Ok(item) => {
                            state.request.received_item(&item);
                            return Some((Ok(item), state));
                        }
                        Err(e) => e,
                    };

                    let retry_after =
                        match state
                            .request
                            .retry_after(&error, state.attempt, max_attempts)
                        {
                            Some(d) => d,
                            None => {
                                state.attempt = max_attempts + 1;
                                return Some((Err(error), state));
                            }
                        };
                    state.attempt += 1;
                    state.entries = None;

                    tracing::error!(
                        "Retrying after {:#?} due to error (attempt {}): {}",
                        &retry_after,
                        state.attempt,
                        &error
                    );

                    tokio::time::sleep(retry_after).await;
                }
            }
        })
        .boxed();

        let stats = stats_rx.into_future().then(|(next, _tail)| match next {
            Some(fut) => fut,
            None => Box::pin(future::ok(Stats::default())),
        });

        Ok(Response {
            entries,
            stats: Box::pin(stats),
        })
    }
}

#[cfg(test)]
mod tests {
    use std::collections::HashSet;
    use std::sync::Arc;

    use anyhow::anyhow;
    use anyhow::Result;
    use async_runtime::block_on;
    use async_runtime::stream_to_iter;
    use async_trait::async_trait;
    use http_client::HttpClientError;
    use parking_lot::Mutex;
    use types::HgId;
    use types::Key;
    use types::RepoPathBuf;

    use super::*;
    use crate::builder::HttpClientBuilder;
    use crate::EdenApiError;
    use crate::Response;

    pub(crate) struct RetryableTest {
        pub keys: HashSet<Key>,
        fails: u32,
        pub attempts: u32,
    }

    impl RetryableTest {
        pub(crate) fn new(keys: Vec<Key>, fails: u32) -> Self {
            Self {
                keys: HashSet::from_iter(keys.into_iter()),
                fails,
                attempts: 0,
            }
        }
    }

    #[async_trait]
    impl RetryableStreamRequest for Arc<Mutex<RetryableTest>> {
        type Item = Key;

        async fn perform(&self, _client: Client) -> Result<Response<Self::Item>, EdenApiError> {
            let mut this = self.lock();
            let mut response = Response::empty();
            let mut entries: Vec<Result<Key, EdenApiError>> =
                this.keys.iter().map(|k| Ok(k.clone())).collect();

            this.attempts += 1;
            if this.attempts <= this.fails {
                entries[1] = Err(EdenApiError::Http(HttpClientError::BadResponse(anyhow!(
                    "fake error"
                ))));
            }
            response.entries = Box::pin(Box::new(stream::iter(entries.into_iter())));
            Ok(response)
        }

        fn received_item(&mut self, item: &Self::Item) {
            self.lock().keys.remove(item);
        }
    }

    #[test]
    fn test_retryable_trait() -> Result<()> {
        let base_url = "https://example.com".parse()?;
        let repo_name = "test_repo";

        let client = HttpClientBuilder::new()
            .repo_name(repo_name)
            .server_url(base_url)
            .max_retry_per_request(10)
            .build()?;

        let keys: Vec<Key> = vec![
            Key::new(
                RepoPathBuf::from_string("1".to_string()).unwrap(),
                HgId::null_id().clone(),
            ),
            Key::new(
                RepoPathBuf::from_string("2".to_string()).unwrap(),
                HgId::null_id().clone(),
            ),
            Key::new(
                RepoPathBuf::from_string("3".to_string()).unwrap(),
                HgId::null_id().clone(),
            ),
        ];
        let fails = 2;
        let retryable = Arc::new(Mutex::new(RetryableTest::new(keys, fails)));
        let retryable_move = retryable.clone();
        let response = block_on(retryable_move.perform_with_retries(client))?;

        let results: Vec<_> = stream_to_iter(response.entries).into_iter().collect();

        assert_eq!(retryable.lock().attempts, fails + 1);
        assert_eq!(retryable.lock().keys.len(), 0);
        let results: HashSet<String> =
            HashSet::from_iter(results.into_iter().map(|k| k.unwrap().path.into_string()));
        assert!(results.contains(&"1".to_string()));
        assert!(results.contains(&"2".to_string()));
        assert!(results.contains(&"3".to_string()));

        Ok(())
    }
}


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

use std::collections::HashSet;

use async_trait::async_trait;
use types::Key;

use super::RetryableStreamRequest;
use crate::client::Client;
use crate::errors::EdenApiError;
use crate::response::Response;
use crate::types::EdenApiServerError;
use crate::types::TreeAttributes;
use crate::types::TreeEntry;

pub(crate) struct RetryableTrees {
    keys: HashSet<Key>,
    attributes: Option<TreeAttributes>,
}

impl RetryableTrees {
    pub(crate) fn new(
        keys: impl IntoIterator<Item = Key>,
        attributes: Option<TreeAttributes>,
    ) -> Self {
        let keys = keys.into_iter().collect();
        Self { keys, attributes }
    }
}

#[async_trait]
impl RetryableStreamRequest for RetryableTrees {
    type Item = Result<TreeEntry, EdenApiServerError>;

    async fn perform(&self, client: Client) -> Result<Response<Self::Item>, EdenApiError> {
        let keys: Vec<Key> = self.keys.iter().cloned().collect();
        client.fetch_trees(keys, self.attributes.clone()).await
    }

    fn received_item(&mut self, item: &Self::Item) {
        let key = match item {
            Ok(entry) => Some(entry.key()),
            Err(e) => e.key.as_ref(),
        };
        if let Some(key) = key {
            self.keys.remove(key);
        }
    }
}


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

use async_trait::async_trait;
use edenapi_types::AlterSnapshotRequest;
use edenapi_types::AlterSnapshotResponse;
use edenapi_types::AnyFileContentId;
use edenapi_types::AnyId;
use edenapi_types::BlameResult;
use edenapi_types::BonsaiChangesetContent;
use edenapi_types::BookmarkEntry;
use edenapi_types::CloneData;
use edenapi_types::CommitGraphEntry;
use edenapi_types::CommitGraphSegmentsEntry;
use edenapi_types::CommitHashLookupResponse;
use edenapi_types::CommitHashToLocationResponse;
use edenapi_types::CommitId;
use edenapi_types::CommitIdScheme;
use edenapi_types::CommitKnownResponse;
use edenapi_types::CommitLocationToHashRequest;
use edenapi_types::CommitLocationToHashResponse;
use edenapi_types::CommitMutationsResponse;
use edenapi_types::CommitRevlogData;
use edenapi_types::CommitTranslateIdResponse;
use edenapi_types::EdenApiServerError;
use edenapi_types::EphemeralPrepareResponse;
use edenapi_types::FetchSnapshotRequest;
use edenapi_types::FetchSnapshotResponse;
use edenapi_types::FileResponse;
use edenapi_types::FileSpec;
use edenapi_types::HgFilenodeData;
use edenapi_types::HgMutationEntryContent;
use edenapi_types::HistoryEntry;
use edenapi_types::LandStackResponse;
use edenapi_types::LookupResponse;
use edenapi_types::TreeAttributes;
use edenapi_types::TreeEntry;
use edenapi_types::UploadHgChangeset;
use edenapi_types::UploadToken;
use edenapi_types::UploadTokensResponse;
use edenapi_types::UploadTreeEntry;
use edenapi_types::UploadTreeResponse;
use minibytes::Bytes;
use types::HgId;
use types::Key;

use crate::errors::EdenApiError;
use crate::response::Response;
use crate::response::ResponseMeta;

#[async_trait]
pub trait EdenApi: Send + Sync + 'static {
    /// Returns the URL to describe the EdenApi. The URL is intended
    /// to match configs like `paths.default`.
    fn url(&self) -> Option<String> {
        None
    }

    async fn health(&self) -> Result<ResponseMeta, EdenApiError> {
        Err(EdenApiError::NotSupported)
    }

    async fn capabilities(&self) -> Result<Vec<String>, EdenApiError> {
        Err(EdenApiError::NotSupported)
    }

    async fn files(&self, keys: Vec<Key>) -> Result<Response<FileResponse>, EdenApiError> {
        let _ = keys;
        Err(EdenApiError::NotSupported)
    }

    async fn files_attrs(
        &self,
        reqs: Vec<FileSpec>,
    ) -> Result<Response<FileResponse>, EdenApiError> {
        let _ = reqs;
        Err(EdenApiError::NotSupported)
    }

    async fn history(
        &self,
        keys: Vec<Key>,
        length: Option<u32>,
    ) -> Result<Response<HistoryEntry>, EdenApiError> {
        let _ = (keys, length);
        Err(EdenApiError::NotSupported)
    }

    async fn trees(
        &self,
        keys: Vec<Key>,
        attributes: Option<TreeAttributes>,
    ) -> Result<Response<Result<TreeEntry, EdenApiServerError>>, EdenApiError> {
        let _ = (keys, attributes);
        Err(EdenApiError::NotSupported)
    }

    async fn commit_revlog_data(
        &self,
        hgids: Vec<HgId>,
    ) -> Result<Response<CommitRevlogData>, EdenApiError> {
        let _ = hgids;
        Err(EdenApiError::NotSupported)
    }

    async fn clone_data(&self) -> Result<CloneData<HgId>, EdenApiError> {
        Err(EdenApiError::NotSupported)
    }

    /// Provide data to complete a lazy pull. `common` defines known heads,
    /// `missing` defines unknown heads.
    async fn pull_lazy(
        &self,
        common: Vec<HgId>,
        missing: Vec<HgId>,
    ) -> Result<CloneData<HgId>, EdenApiError> {
        let _ = (common, missing);
        Err(EdenApiError::NotSupported)
    }

    async fn pull_fast_forward_master(
        &self,
        old_master: HgId,
        new_master: HgId,
    ) -> Result<CloneData<HgId>, EdenApiError> {
        self.pull_lazy(vec![old_master], vec![new_master]).await
    }

    async fn commit_location_to_hash(
        &self,
        requests: Vec<CommitLocationToHashRequest>,
    ) -> Result<Vec<CommitLocationToHashResponse>, EdenApiError> {
        let _ = requests;
        Err(EdenApiError::NotSupported)
    }

    async fn commit_hash_to_location(
        &self,
        master_heads: Vec<HgId>,
        hgids: Vec<HgId>,
    ) -> Result<Vec<CommitHashToLocationResponse>, EdenApiError> {
        let _ = (master_heads, hgids);
        Err(EdenApiError::NotSupported)
    }

    /// Return a subset of commits that are known by the server.
    /// This is similar to the "known" command in HG wireproto.
    async fn commit_known(
        &self,
        hgids: Vec<HgId>,
    ) -> Result<Vec<CommitKnownResponse>, EdenApiError> {
        let _ = hgids;
        Err(EdenApiError::NotSupported)
    }

    /// Return part of the commit graph that are ancestors of `heads`,
    /// excluding ancestors of `common`.
    ///
    /// This is `heads % common` (or `only(heads, common)`) expressed using HG
    /// revset, or `changelog.findmissing` in HG Python code base.
    ///
    /// If any of the nodes in `heads` and `common` are unknown by the server,
    /// it should be an error.
    async fn commit_graph(
        &self,
        heads: Vec<HgId>,
        common: Vec<HgId>,
    ) -> Result<Vec<CommitGraphEntry>, EdenApiError> {
        let _ = (heads, common);
        Err(EdenApiError::NotSupported)
    }

    /// Returns a segmented representation of the part of the commit graph
    /// consisting of ancestors of `heads`, excluding ancestors of `common`.
    ///
    /// If any of the nodes in `heads` and `common` are unknown by the server,
    /// it should be an error.
    async fn commit_graph_segments(
        &self,
        heads: Vec<HgId>,
        common: Vec<HgId>,
    ) -> Result<Vec<CommitGraphSegmentsEntry>, EdenApiError> {
        let _ = (heads, common);
        Err(EdenApiError::NotSupported)
    }

    /// Return matching full hashes of hex hash prefix
    async fn hash_prefixes_lookup(
        &self,
        prefixes: Vec<String>,
    ) -> Result<Vec<CommitHashLookupResponse>, EdenApiError> {
        let _ = prefixes;
        Err(EdenApiError::NotSupported)
    }

    async fn bookmarks(&self, bookmarks: Vec<String>) -> Result<Vec<BookmarkEntry>, EdenApiError> {
        let _ = bookmarks;
        Err(EdenApiError::NotSupported)
    }

    /// Create, delete, or move a bookmark
    async fn set_bookmark(
        &self,
        bookmark: String,
        to: Option<HgId>,
        from: Option<HgId>,
        pushvars: HashMap<String, String>,
    ) -> Result<(), EdenApiError> {
        let _ = (bookmark, to, from, pushvars);
        Err(EdenApiError::NotSupported)
    }

    /// Land a stack of commits, rebasing them onto the specified bookmark
    /// and updating the bookmark to the top of the rebased stack.
    async fn land_stack(
        &self,
        bookmark: String,
        head: HgId,
        base: HgId,
        pushvars: HashMap<String, String>,
    ) -> Result<LandStackResponse, EdenApiError> {
        let _ = (bookmark, head, base, pushvars);
        Err(EdenApiError::NotSupported)
    }

    /// Lookup items and return signed upload tokens if an item has been uploaded
    /// Supports: file content, hg filenode, hg tree, hg changeset
    async fn lookup_batch(
        &self,
        items: Vec<AnyId>,
        bubble_id: Option<NonZeroU64>,
        copy_from_bubble_id: Option<NonZeroU64>,
    ) -> Result<Vec<LookupResponse>, EdenApiError> {
        let _ = (items, bubble_id, copy_from_bubble_id);
        Err(EdenApiError::NotSupported)
    }

    /// Upload files content
    async fn process_files_upload(
        &self,
        data: Vec<(AnyFileContentId, Bytes)>,
        bubble_id: Option<NonZeroU64>,
        copy_from_bubble_id: Option<NonZeroU64>,
    ) -> Result<Response<UploadToken>, EdenApiError> {
        let _ = (data, bubble_id, copy_from_bubble_id);
        Err(EdenApiError::NotSupported)
    }

    /// Upload list of hg filenodes
    async fn upload_filenodes_batch(
        &self,
        items: Vec<HgFilenodeData>,
    ) -> Result<Response<UploadTokensResponse>, EdenApiError> {
        let _ = items;
        Err(EdenApiError::NotSupported)
    }

    /// Upload list of trees
    async fn upload_trees_batch(
        &self,
        items: Vec<UploadTreeEntry>,
    ) -> Result<Response<UploadTreeResponse>, EdenApiError> {
        let _ = items;
        Err(EdenApiError::NotSupported)
    }

    /// Upload list of changesets
    async fn upload_changesets(
        &self,
        changesets: Vec<UploadHgChangeset>,
        mutations: Vec<HgMutationEntryContent>,
    ) -> Result<Response<UploadTokensResponse>, EdenApiError> {
        let _ = (changesets, mutations);
        Err(EdenApiError::NotSupported)
    }

    async fn upload_bonsai_changeset(
        &self,
        changeset: BonsaiChangesetContent,
        bubble_id: Option<NonZeroU64>,
    ) -> Result<Response<UploadTokensResponse>, EdenApiError> {
        let _ = (changeset, bubble_id);
        Err(EdenApiError::NotSupported)
    }

    async fn ephemeral_prepare(
        &self,
        custom_duration: Option<Duration>,
        labels: Option<Vec<String>>,
    ) -> Result<Response<EphemeralPrepareResponse>, EdenApiError> {
        let _ = (custom_duration, labels);
        Err(EdenApiError::NotSupported)
    }

    /// Fetch information about the requested snapshot
    async fn fetch_snapshot(
        &self,
        request: FetchSnapshotRequest,
    ) -> Result<Response<FetchSnapshotResponse>, EdenApiError> {
        let _ = request;
        Err(EdenApiError::NotSupported)
    }

    /// Alter the properties of an existing snapshot
    async fn alter_snapshot(
        &self,
        request: AlterSnapshotRequest,
    ) -> Result<Response<AlterSnapshotResponse>, EdenApiError> {
        let _ = request;
        Err(EdenApiError::NotSupported)
    }

    /// Download single file from upload token
    async fn download_file(&self, token: UploadToken) -> Result<Bytes, EdenApiError> {
        let _ = token;
        Err(EdenApiError::NotSupported)
    }

    /// Download mutation info related to given commits
    async fn commit_mutations(
        &self,
        commits: Vec<HgId>,
    ) -> Result<Vec<CommitMutationsResponse>, EdenApiError> {
        let _ = commits;
        Err(EdenApiError::NotSupported)
    }

    /// Translate commit IDs to a different commit ID scheme
    async fn commit_translate_id(
        &self,
        commits: Vec<CommitId>,
        scheme: CommitIdScheme,
        from_repo: Option<String>,
        to_repo: Option<String>,
    ) -> Result<Response<CommitTranslateIdResponse>, EdenApiError> {
        let _ = (commits, scheme, from_repo, to_repo);
        Err(EdenApiError::NotSupported)
    }

    /// Fetch metadata describing the last commit to modify each line in given file(s)
    async fn blame(&self, files: Vec<Key>) -> Result<Response<BlameResult>, EdenApiError> {
        let _ = files;
        Err(EdenApiError::NotSupported)
    }
}


/*
 * 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 edenapi_types::wire::WireToApiConversionError;
use edenapi_types::EdenApiServerError;
use http::header::HeaderMap;
use http::status::StatusCode;
use http_client::HttpClientError;
use http_client::TlsError;
use http_client::TlsErrorKind;
use thiserror::Error;
use types::errors::NetworkError;

#[derive(Debug, Error)]
pub enum EdenApiError {
    #[error("Failed to serialize request: {0}")]
    RequestSerializationFailed(#[source] serde_cbor::Error),
    #[error("Failed to parse response: {0}")]
    ParseResponse(String),
    #[error(transparent)]
    BadConfig(#[from] ConfigError),
    #[error(transparent)]
    Http(#[from] HttpClientError),
    #[error("Server responded {status} for {url}: {message}. Headers: {headers:#?}")]
    HttpError {
        status: StatusCode,
        message: String,
        headers: HeaderMap,
        url: String,
    },
    #[error(transparent)]
    InvalidUrl(#[from] url::ParseError),
    #[error(transparent)]
    WireToApiConversionFailed(#[from] WireToApiConversionError),
    #[error(transparent)]
    ServerError(#[from] EdenApiServerError),
    #[error("expected response, but none returned by the server")]
    NoResponse,
    #[error(transparent)]
    Other(#[from] anyhow::Error),
    #[error("Not supported by the server")]
    NotSupported,
    #[error(transparent)]
    MissingCerts(#[from] auth::MissingCerts),
}

#[derive(Debug, Error)]
pub enum ConfigError {
    #[error("Missing required config item: {0}")]
    Missing(String),
    #[error("Invalid config item: '{0}' ({1})")]
    Invalid(String, #[source] anyhow::Error),
}

impl EdenApiError {
    pub fn is_retryable(&self) -> bool {
        use http_client::HttpClientError::*;
        use EdenApiError::*;
        match self {
            Http(client_error) => match client_error {
                Tls(TlsError { kind, .. }) => kind == &TlsErrorKind::RecvError,
                _ => true,
            },
            HttpError { status, .. } => {
                // 300-399
                if status.is_redirection() {
                    false
                // 400-499
                } else if status.is_client_error() {
                    match *status {
                        StatusCode::REQUEST_TIMEOUT => true,
                        StatusCode::TOO_MANY_REQUESTS => true,
                        _ => false,
                    }
                // 500-599
                } else if status.is_server_error() {
                    true
                } else {
                    false
                }
            }
            _ => false,
        }
    }

    // Report whether this error may be a network error. Err on the side of saying "yes".
    pub fn maybe_network_error(&self) -> bool {
        use EdenApiError::*;

        match self {
            Http(_) | HttpError { .. } | ServerError(_) | NoResponse | Other(_) => true,

            RequestSerializationFailed(_)
            | ParseResponse(_)
            | BadConfig(_)
            | InvalidUrl(_)
            | WireToApiConversionFailed(_)
            | NotSupported
            | MissingCerts(_) => false,
        }
    }

    pub fn tag_network(self) -> anyhow::Error {
        if self.maybe_network_error() {
            NetworkError::wrap(self)
        } else {
            self.into()
        }
    }
}


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

pub mod api;
pub mod errors;
pub mod response;

// Re-export for convenience.
pub use configmodel;
pub use edenapi_types as types;

pub use crate::api::EdenApi;
pub use crate::errors::ConfigError;
pub use crate::errors::EdenApiError;
pub use crate::response::Entries;
pub use crate::response::Response;
pub use crate::response::ResponseMeta;

pub type Result<T> = std::result::Result<T, EdenApiError>;


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

use futures::prelude::*;
use http::header;
use http::HeaderMap;
use http::StatusCode;
use http::Version;
use http_client::AsyncResponse as AsyncHttpResponse;
use http_client::Response as HttpResponse;
use http_client::Stats;

use crate::errors::EdenApiError;

const SERVER_HEADER: &str = "server";
const REQUEST_ID_HEADER: &str = "x-request-id";
const TW_TASK_HEADER: &str = "x-tw-task";
const TW_VERSION_HEADER: &str = "x-tw-task-version";
const TW_CANARY_HEADER: &str = "x-tw-canary-id";
const SERVER_LOAD_HEADER: &str = "x-load";
const MONONOKE_HOST: &str = "x-mononoke-host";

/// A generic `Stream` of "entries" representing the deserialized content
/// of a streaming response from the server.
pub type Entries<T> = Pin<Box<dyn Stream<Item = Result<T, EdenApiError>> + Send + 'static>>;
pub type StatsFuture = Pin<Box<dyn Future<Output = Result<Stats, EdenApiError>> + Send + 'static>>;

/// The result of a data fetching operation, which may have involved
/// several individual HTTP requests.
pub struct Response<T> {
    /// A `Stream` containing the combined responses for all of the requests.
    /// There are no ordering guarantees; entries from different HTTP responses
    /// may be arbitrarily interleaved.
    pub entries: Entries<T>,

    /// A `Future` that returns the aggregated transfer stastics for the
    /// all of the HTTP requests involved in the fetching operation. Will
    /// only resolve once all of the requests have completed.
    pub stats: StatsFuture,
}

impl<T: Send + 'static> Response<T> {
    pub fn empty() -> Self {
        Self {
            entries: stream::empty().boxed(),
            stats: future::ok(Stats::default()).boxed(),
        }
    }

    /// Flatten the response into a `Vec`.
    pub async fn flatten(self) -> Result<Vec<T>, EdenApiError> {
        self.entries.try_collect().await
    }

    /// Read one (and presumably the only) item from the response
    pub async fn single(mut self) -> Result<T, EdenApiError> {
        self.entries
            .try_next()
            .await
            .and_then(|opt| opt.ok_or(EdenApiError::NoResponse))
    }

    /// Wrap entries stream via then().
    pub fn then<Fut, F>(self, f: F) -> Self
    where
        Fut: Future<Output = Result<T, EdenApiError>> + Send + 'static,
        F: Fn(Result<T, EdenApiError>) -> Fut + Send + 'static,
    {
        Self {
            entries: self.entries.then(f).boxed(),
            stats: self.stats,
        }
    }
}

/// Metadata extracted from the headers of an individual HTTP response.
#[derive(Debug, Default)]
pub struct ResponseMeta {
    pub version: Version,
    pub status: StatusCode,
    pub server: Option<String>,
    pub request_id: Option<String>,
    pub tw_task_handle: Option<String>,
    pub tw_task_version: Option<String>,
    pub tw_canary_id: Option<String>,
    pub server_load: Option<usize>,
    pub content_length: Option<usize>,
    pub content_encoding: Option<String>,
    pub mononoke_host: Option<String>,
}

impl ResponseMeta {
    fn from_parts(version: Version, status: StatusCode, headers: &HeaderMap) -> Self {
        Self {
            version,
            status,
            server: get_header(headers, SERVER_HEADER),
            mononoke_host: get_header(headers, MONONOKE_HOST),
            request_id: get_header(headers, REQUEST_ID_HEADER),
            tw_task_handle: get_header(headers, TW_TASK_HEADER),
            tw_task_version: get_header(headers, TW_VERSION_HEADER),
            tw_canary_id: get_header(headers, TW_CANARY_HEADER),
            server_load: get_header(headers, SERVER_LOAD_HEADER).and_then(|l| l.parse().ok()),
            content_length: get_header(headers, header::CONTENT_LENGTH.as_str())
                .and_then(|l| l.parse().ok()),
            content_encoding: get_header(headers, header::CONTENT_ENCODING.as_str()),
        }
    }
}

impl From<&HttpResponse> for ResponseMeta {
    fn from(res: &HttpResponse) -> Self {
        Self::from_parts(res.version(), res.status(), res.headers())
    }
}

impl From<&AsyncHttpResponse> for ResponseMeta {
    fn from(res: &AsyncHttpResponse) -> Self {
        Self::from_parts(res.version(), res.status(), res.headers())
    }
}

fn get_header(headers: &HeaderMap, name: &str) -> Option<String> {
    Some(headers.get(name)?.to_str().ok()?.into())
}


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

extern crate proc_macro;

use std::collections::HashSet;

use proc_macro2::TokenStream;
use quote::format_ident;
use quote::quote;
use syn::spanned::Spanned;
use syn::*;

const ID: &str = "id";

/// Derive a default implementation for a wire type for this type
// TODO: Future improvements
// - Support fields that do not implement Default on Api obj
//    - add "no_default" attribute to field. Wire impl will wrap it in Option,
//      and fail when deserializing if it's not present
// - Support generics in type
//    - Might be possible to make it work with some adaptation, at least for
//      simple "wrapper" types that just have one generic T.
#[proc_macro_attribute]
pub fn auto_wire(
    attr: proc_macro::TokenStream,
    item: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
    let _ = parse_macro_input!(attr as syn::parse::Nothing);
    let mut item = parse_macro_input!(item as Item);

    let result = match &mut item {
        Item::Struct(ref mut item) => get_wire_struct(item),
        Item::Enum(ref mut item) => get_wire_enum(item),
        item => Err(Error::new(item.span(), "Only struct or enum is supported")),
    };
    match result {
        Ok(wire_item) => quote! {
            #item

            #wire_item
        },
        Err(e) => e.to_compile_error(),
    }
    .into()
}

struct Parenthesized<T>(T);
impl<T> parse::Parse for Parenthesized<T>
where
    T: std::str::FromStr,
    T::Err: std::fmt::Display,
{
    fn parse(input: parse::ParseStream) -> Result<Self> {
        let content;
        parenthesized!(content in input);
        Ok(Self(content.parse::<LitInt>()?.base10_parse::<T>()?))
    }
}

fn arbitrary_impl(wire_ident: &Ident, generics: &Generics) -> TokenStream {
    quote! {
        #[cfg(any(test, feature = "for-tests"))]
        impl #generics quickcheck::Arbitrary for #wire_ident #generics {
            fn arbitrary(g: &mut quickcheck::Gen) -> Self {
                use crate::ToWire;
                <Self as crate::ToApi>::Api::arbitrary(g).to_wire()
            }
        }
    }
}

fn extract_id(
    attrs: Vec<Attribute>,
    spanned: &impl Spanned,
    ids: &mut HashSet<u16>,
) -> Result<(u16, Vec<Attribute>)> {
    let (id, other_attrs): (Vec<_>, Vec<_>) =
        attrs.into_iter().partition(|attr| attr.path.is_ident(ID));
    if id.len() != 1 {
        return Err(Error::new(
            spanned.span(),
            "Must have exactly one attribute 'id'",
        ));
    }
    let id = id
        .into_iter()
        .next()
        // never panics because of if above
        .unwrap();
    let id = parse2::<Parenthesized<u16>>(id.tokens)?.0;
    if !ids.insert(id) {
        return Err(Error::new(
            spanned.span(),
            "'id' attribute should be unique across all fields/variants",
        ));
    }
    Ok((id, other_attrs))
}

fn remove_id(attrs: &mut Vec<Attribute>) {
    *attrs = std::mem::take(attrs)
        .into_iter()
        .filter(|attr| !attr.path.is_ident(ID))
        .collect();
}

fn extract_no_default(attrs: &mut Vec<Attribute>) -> bool {
    let mut no_default = false;
    attrs.retain(|a| match a.path.get_ident() {
        Some(id) if *id == "no_default" => {
            no_default = true;
            false
        }
        _ => true,
    });
    no_default
}

fn get_wire_struct(original: &mut ItemStruct) -> Result<TokenStream> {
    let mut item = original.clone();
    let ident = item.ident.clone();
    let wire_ident = format_ident!("Wire{}", ident);
    item.ident = wire_ident.clone();

    let mut fields = vec![];
    let mut has_no_default_field = false;

    let mut ids = HashSet::new();
    match &mut item.fields {
        Fields::Named(ref mut fs) => fs.named.iter_mut().try_for_each(|ref mut field| {
            fields.push(field.ident.clone().unwrap());
            let (id, other_attrs) = extract_id(std::mem::take(&mut field.attrs), &field, &mut ids)?;
            field.attrs = other_attrs;
            let ty = &field.ty;
            field.ty = parse_quote!( <#ty as crate::ToWire>::Wire );
            let name = format!("{}", id);

            if extract_no_default(&mut field.attrs) {
                has_no_default_field = true;
                field.attrs.push(
                    parse_quote!( #[serde(rename=#name)] ),
                );
            } else {
                field.attrs.push(
                    parse_quote!( #[serde(rename=#name, default, skip_serializing_if="crate::wire::is_default")] ),
                );
            }
            Result::Ok(())
        })?,
        _ => {
            return Err(Error::new(
                item.fields.span(),
                "Only structs with named fields supported",
            ));
        }
    }

    if has_no_default_field {
        item.attrs = vec![
            parse_quote!(#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]),
        ];
    } else {
        item.attrs = vec![
            parse_quote!(#[derive(Default, Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]),
        ];
    }

    // remove id() attribute from original struct
    match &mut original.fields {
        Fields::Named(ref mut fs) => fs.named.iter_mut().for_each(|ref mut field| {
            remove_id(&mut field.attrs);
            extract_no_default(&mut field.attrs);
        }),
        _ => unreachable!(),
    }

    let fields_to_wire = fields
        .iter()
        .map(|name| quote! { #name: self.#name.to_wire() });

    let generics = &original.generics;

    let to_wire_impl = quote! {
        impl #generics crate::ToWire for #ident #generics {
            type Wire = #wire_ident #generics;

            fn to_wire(self) -> Self::Wire {
                Self::Wire {
                    #( #fields_to_wire ),*
                }
            }
        }
    };

    let fields_to_api = fields
        .iter()
        .map(|name| quote! { #name: self.#name.to_api()? });

    let to_api_impl = quote! {
        impl #generics crate::ToApi for #wire_ident #generics {
            type Api = #ident #generics;
            type Error = crate::WireToApiConversionError;

            fn to_api(self) -> Result<Self::Api, Self::Error> {
                Ok(Self::Api {
                    #( #fields_to_api ),*
                })
            }
        }
    };

    let arbitrary_impl = arbitrary_impl(&wire_ident, generics);

    Ok(quote! {
        #item
        #to_wire_impl
        #to_api_impl
        #arbitrary_impl
    })
}

fn get_wire_enum(original: &mut ItemEnum) -> Result<TokenStream> {
    // original is the original Enum
    // item is the counterpart WireEnum
    let mut item = original.clone();
    let ident = item.ident.clone();
    let wire_ident = format_ident!("Wire{}", ident);
    item.ident = wire_ident.clone();
    item.attrs = vec![
        parse_quote!(#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]),
    ];

    let mut variants = vec![];
    let mut ids = HashSet::new();
    item.variants.iter_mut().try_for_each(|ref mut variant| {
        let (id, other_attrs) = extract_id(std::mem::take(&mut variant.attrs), &variant, &mut ids)?;
        variant.attrs = other_attrs;
        if id == 0 {
            return Err(Error::new(variant.span(), "Variant id can't be 0"));
        }
        let name = format!("{}", id);
        variant.attrs.push(parse_quote!( #[serde(rename=#name)]));
        let unit = match &mut variant.fields {
            Fields::Unit => true,
            Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
                let field = fields.unnamed.first_mut().unwrap();
                let ty = &field.ty;
                field.ty = parse_quote!( <#ty as crate::ToWire>::Wire );
                false
            }
            _ => {
                return Err(Error::new(
                    variant.fields.span(),
                    "Only unit variants or with a single field supported",
                ));
            }
        };
        variants.push((variant.ident.clone(), unit));
        Ok(())
    })?;
    item.variants.push_value(parse_quote!(
        #[serde(other, rename = "0")]
        UnknownVariant
    ));

    // remove id() attribute from original enum
    original.variants.iter_mut().for_each(|ref mut variant| {
        remove_id(&mut variant.attrs);
    });

    let variants_to_wire = variants.iter().map(|(name, unit)| {
        if *unit {
            quote! { Self::#name => Self::Wire::#name }
        } else {
            quote! { Self::#name(value) => Self::Wire::#name(value.to_wire()) }
        }
    });

    let generics = &original.generics;

    let to_wire_impl = quote! {
        impl #generics crate::ToWire for #ident #generics {
            type Wire = #wire_ident #generics;

            fn to_wire(self) -> Self::Wire {
                match self {
                    #( #variants_to_wire ),*
                }
            }
        }
    };

    let variants_to_api = variants.iter().map(|(name, unit)| {
        if *unit {
            quote! { Self::#name => Ok(Self::Api::#name) }
        } else {
            quote! { Self::#name(value) => Ok(Self::Api::#name(value.to_api()?)) }
        }
    });

    let to_api_impl = quote! {
        impl #generics crate::ToApi for #wire_ident #generics {
            type Api = #ident #generics;
            type Error = crate::WireToApiConversionError;

            fn to_api(self) -> Result<Self::Api, Self::Error> {
                match self {
                    #( #variants_to_api, )*
                    Self::UnknownVariant => Err(Self::Error::UnrecognizedEnumVariant(stringify!(#wire_ident))),
                }
            }
        }
    };

    let arbitrary_impl = arbitrary_impl(&wire_ident, generics);

    let default_impl = quote! {
        impl #generics Default for #wire_ident #generics {
            fn default() -> Self {
                use crate::ToWire;
                #ident::default().to_wire()
            }
        }
    };

    Ok(quote! {
        #item
        #to_wire_impl
        #to_api_impl
        #arbitrary_impl
        #default_impl
    })
}

#[test]
#[should_panic = "'id' attribute should be unique across all fields/variants"]
fn test_same_ids() {
    let mut input = parse_quote! {
        enum MyEnum {
            #[id(1)]
            A,
            #[id(1)]
            B,
        }
    };
    get_wire_enum(&mut input).unwrap();
}

#[test]
#[should_panic = "Must have exactly one attribute 'id'"]
fn test_two_ids() {
    let mut input = parse_quote! {
        enum MyEnum {
            #[id(1)]
            #[id(1)]
            A,
        }
    };
    get_wire_enum(&mut input).unwrap();
}


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

#[cfg(any(test, feature = "for-tests"))]
use quickcheck_arbitrary_derive::Arbitrary;
use serde_derive::Deserialize;
use serde_derive::Serialize;
use type_macros::auto_wire;
use types::HgId;

use crate::commitid::BonsaiChangesetId;
use crate::AnyFileContentId;
use crate::IndexableId;
use crate::UploadToken;

#[auto_wire]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Hash)]
#[cfg_attr(any(test, feature = "for-tests"), derive(Arbitrary))]
pub enum AnyId {
    #[id(1)]
    AnyFileContentId(AnyFileContentId),
    #[id(2)]
    HgFilenodeId(HgId),
    #[id(3)]
    HgTreeId(HgId),
    #[id(4)]
    HgChangesetId(HgId),
    #[id(5)]
    BonsaiChangesetId(BonsaiChangesetId),
}

impl Default for AnyId {
    fn default() -> Self {
        Self::AnyFileContentId(AnyFileContentId::default())
    }
}

#[auto_wire]
#[derive(Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq)]
#[cfg_attr(any(test, feature = "for-tests"), derive(Arbitrary))]
pub struct LookupRequest {
    #[id(1)]
    pub id: AnyId,
    #[id(2)]
    pub bubble_id: Option<NonZeroU64>,
    /// If present and the original id is not, lookup will also look into this
    /// bubble, and if the id is present, copy it to the requested bubble.
    #[id(3)]
    pub copy_from_bubble_id: Option<NonZeroU64>,
}

#[auto_wire]
#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
#[cfg_attr(any(test, feature = "for-tests"), derive(Arbitrary))]
pub enum LookupResult {
    /// Id was present, upload token for it is returned
    #[id(1)]
    Present(UploadToken),
    /// Id was not present, only its id is returned
    #[id(2)]
    NotPresent(IndexableId),
    // Possible to add an Error variant in the future if we don't want to
    // swallow the errors
}

impl Default for LookupResult {
    fn default() -> Self {
        Self::NotPresent(Default::default())
    }
}

#[auto_wire]
#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
#[cfg_attr(any(test, feature = "for-tests"), derive(Arbitrary))]
pub struct LookupResponse {
    #[id(3)]
    pub result: LookupResult,
}


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

#[cfg(any(test, feature = "for-tests"))]
use quickcheck::Arbitrary;
use serde::Deserialize;
use serde::Serialize;

/// Generic container for a bunch of uniform objects. This is primarily intended for requests and
/// responses which can be difficult to evolve when the top level object is an array.
/// For cases where evolution is required we would probably replace the Batch wrapper with a
/// specialized type. For example, starting from `Batch<MyRequest>` we would change to
/// struct MyRequestBatch {
///   pub batch: Vec<MyRequest>,
///   pub evolution: Evolution,
/// }
#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[derive(Serialize, Deserialize)]
pub struct Batch<T> {
    pub batch: Vec<T>,
}

#[cfg(any(test, feature = "for-tests"))]
impl<T> Arbitrary for Batch<T>
where
    T: Arbitrary,
{
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
        Batch {
            batch: Arbitrary::arbitrary(g),
        }
    }
}