pub(crate) mod command_input;
pub(crate) mod credentials;
pub(crate) mod status;
pub(crate) mod warm_up;

pub use status::*;

use std::{
    cmp::Ordering,
    io::Write,
    path::{Path, PathBuf},
    sync::Arc,
};

use bytes::Bytes;
use derive_setters::Setters;
use jiff::SignedDuration;
use log::info;
use serde_with::{DisplayFromStr, serde_as};

use crate::{
    ReadSource, RepositoryBackends, RusticError,
    backend::{
        FileType, FindInBackend, ReadBackend, WriteBackend,
        cache::{Cache, CachedBackend},
        decrypt::{DecryptBackend, DecryptReadBackend, DecryptWriteBackend},
        hotcold::HotColdBackend,
        local_destination::LocalDestination,
        node::Node,
        warm_up::WarmUpAccessBackend,
    },
    blob::{
        BlobId, BlobType, PackedId,
        tree::{
            FindMatches, FindNode, NodeStreamer, TreeId, TreeStreamerOptions as LsOptions,
            rewrite::RewriteTreesOptions,
        },
    },
    commands::{
        self,
        backup::BackupOptions,
        check::{CheckOptions, CheckResults, check_repository},
        config::{ConfigOptions, save_config_hot},
        copy::CopySnapshot,
        key::{KeyOptions, add_current_key_to_repo},
        prune::{PruneOptions, PrunePlan, prune_repository},
        repair::{
            hotcold::{repair_hotcold, repair_hotcold_packs},
            index::{RepairIndexOptions, index_checked_from_collector, repair_index},
            snapshots::{RepairSnapshotsOptions, repair_snapshots},
        },
        repoinfo::{IndexInfos, RepoFileInfos},
        restore::{RestoreOptions, RestorePlan, collect_and_prepare, restore_repository},
        rewrite::{RewriteOptions, rewrite_snapshots, rewrite_snapshots_and_trees},
    },
    crypto::aespoly1305::Key,
    error::{ErrorKind, RusticResult},
    index::{
        GlobalIndex, IndexEntry, ReadGlobalIndex, ReadIndex,
        binarysorted::{IndexCollector, IndexType},
    },
    progress::{HiddenProgress, NoProgressBars, Progress, ProgressBars, ProgressType},
    repofile::{
        ConfigFile, KeyId, PathList, RepoFile, RepoId, SnapshotFile, SnapshotSummary, Tree,
        configfile::ConfigId,
        keyfile::{MasterKey, find_key_in_backend},
        snapshotfile::SnapshotId,
    },
    repository::{
        command_input::CommandInput,
        credentials::Credentials,
        warm_up::{warm_up, warm_up_wait},
    },
    vfs::OpenFile,
};

#[cfg(feature = "clap")]
use clap::ValueHint;

mod constants {
    /// Estimated item capacity used for cache in [`FullIndex`](super::FullIndex)
    pub(super) const ESTIMATED_ITEM_CAPACITY: usize = 32;

    /// Estimated weight capacity used for cache in [`FullIndex`](super::FullIndex) (in bytes)
    pub(super) const WEIGHT_CAPACITY: u64 = 32_000_000;
}

/// Options for using and opening a [`Repository`]
#[serde_as]
#[cfg_attr(feature = "clap", derive(clap::Parser))]
#[cfg_attr(feature = "merge", derive(conflate::Merge))]
#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize, Setters)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
#[setters(into, strip_option)]
#[non_exhaustive]
pub struct RepositoryOptions {
    /// Don't use a cache.
    #[cfg_attr(feature = "clap", clap(long, global = true, env = "RUSTIC_NO_CACHE"))]
    #[cfg_attr(feature = "merge", merge(strategy = conflate::bool::overwrite_false))]
    pub no_cache: bool,

    /// Use this dir as cache dir instead of the standard cache dir
    #[cfg_attr(
        feature = "clap",
        clap(
            long,
            global = true,
            conflicts_with = "no_cache",
            env = "RUSTIC_CACHE_DIR",
            value_hint = ValueHint::DirPath,
        )
    )]
    #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
    pub cache_dir: Option<PathBuf>,

    /// Warm up needed data pack files by only requesting them without processing
    #[cfg_attr(feature = "clap", clap(long, global = true))]
    #[cfg_attr(feature = "merge", merge(strategy = conflate::bool::overwrite_false))]
    pub warm_up: bool,

    /// Warm up needed data pack files by running the command with %id replaced by pack id
    #[cfg_attr(
        feature = "clap",
        clap(long, global = true, conflicts_with = "warm_up",)
    )]
    #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
    pub warm_up_command: Option<CommandInput>,

    /// Wait for end of warm up by running the command with %id replaced by pack id
    #[cfg_attr(
        feature = "clap",
        clap(long, global = true, conflicts_with = "warm_up_wait",)
    )]
    #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
    pub warm_up_wait_command: Option<CommandInput>,

    /// Duration (e.g. 10m) to wait after warm up
    #[serde_as(as = "Option<DisplayFromStr>")]
    #[cfg_attr(feature = "clap", clap(long, global = true, value_name = "DURATION"))]
    #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
    pub warm_up_wait: Option<SignedDuration>,

    /// Batch size for warm-up command invocations
    #[cfg_attr(feature = "clap", clap(long, global = true))]
    #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
    pub warm_up_batch: Option<usize>,
}

#[derive(Debug, Clone)]
/// A `Repository` allows all kind of actions to be performed.
///
/// # Type Parameters
///
/// * `P` - The type of the progress bar
/// * `S` - The type of the status
///
/// # Notes
///
/// A repository can be in different states and allows some actions only when in certain state(s).
pub struct Repository<S> {
    /// The name of the repository
    pub name: String,

    /// The `HotColdBackend` to use for this repository
    pub(crate) be: Arc<dyn WriteBackend>,

    /// The Backend to use for hot files
    pub(crate) be_hot: Option<Arc<dyn WriteBackend>>,

    /// The Backend to use for cold files
    pub(crate) be_cold: Arc<dyn WriteBackend>,

    /// The options used for this repository
    opts: RepositoryOptions,

    /// The progress bar to use
    pb: Arc<dyn ProgressBars>,

    /// The status
    status: S,
}

impl Repository<()> {
    /// Create a new repository from the given [`RepositoryOptions`] (without progress bars)
    ///
    /// # Arguments
    ///
    /// * `opts` - The options to use for the repository
    /// * `backends` - The backends to create/access a repository on
    ///
    /// # Errors
    ///
    /// * If no repository is given
    /// * If the warm-up command does not contain `%id`
    /// * If the specified backend cannot be loaded, e.g. is not supported
    pub fn new(opts: &RepositoryOptions, backends: &RepositoryBackends) -> RusticResult<Self> {
        Self::new_with_progress(opts, backends, NoProgressBars {})
    }

    /// Create a new repository from the given [`RepositoryOptions`] with given progress bars
    ///
    /// # Type Parameters
    ///
    /// * `P` - The type of the progress bar
    ///
    /// # Arguments
    ///
    /// * `opts` - The options to use for the repository
    /// * `backends` - The backends to create/access a repository on
    /// * `pb` - The progress bars to use
    ///
    /// # Errors
    ///
    /// * If no repository is given
    /// * If the warm-up command does not contain `%id`
    /// * If the specified backend cannot be loaded, e.g. is not supported
    pub fn new_with_progress<P: ProgressBars>(
        opts: &RepositoryOptions,
        backends: &RepositoryBackends,
        pb: P,
    ) -> RusticResult<Self> {
        let mut be = backends.repository();
        let be_hot = backends.repo_hot();

        if let Some(warm_up) = &opts.warm_up_command {
            let _ = warm_up.uses_plural_placeholders()?;

            info!(
                "using warm-up command {warm_up} with batch size {}",
                opts.warm_up_batch.unwrap_or(1)
            );
        }

        let be_cold = be.clone();

        if opts.warm_up {
            be = WarmUpAccessBackend::new_warm_up(be);
        }

        let mut name = be.location();
        if let Some(be_hot) = &be_hot {
            be = Arc::new(HotColdBackend::new(be, be_hot.clone()));
            name.push('#');
            name.push_str(&be_hot.location());
        }

        Ok(Self {
            name,
            be,
            be_hot,
            be_cold,
            opts: opts.clone(),
            pb: Arc::new(pb),
            status: (),
        })
    }
}

impl<S> Repository<S> {
    /// Start a new progress, which is hidden
    pub fn progress_hidden(&self) -> Progress {
        Progress::new(HiddenProgress)
    }

    /// Start a new progress spinner. Note that this progress doesn't get a length and is not advanced, only finished.
    ///
    /// # Arguments
    ///
    /// * `prefix` - The prefix of the progress
    pub fn progress_spinner(&self, prefix: &str) -> Progress {
        self.pb.progress(ProgressType::Spinner, prefix)
    }

    /// Start a new progress which counts something
    ///
    /// # Arguments
    ///
    /// * `prefix` - The prefix of the progress
    pub fn progress_counter(&self, prefix: &str) -> Progress {
        self.pb.progress(ProgressType::Counter, prefix)
    }

    /// Start a new progress which counts bytes
    ///
    /// # Arguments
    ///
    /// * `prefix` - The prefix of the progress
    pub fn progress_bytes(&self, prefix: &str) -> Progress {
        self.pb.progress(ProgressType::Bytes, prefix)
    }

    /// Returns the Id of the config file
    ///
    /// # Errors
    ///
    /// * If listing the repository config file failed
    /// * If there is more than one repository config file
    ///
    /// # Returns
    ///
    /// The id of the config file or `None` if no config file is found
    pub fn config_id(&self) -> RusticResult<Option<ConfigId>> {
        self.config_id_with_backend(&self.be)
    }

    /// Returns the Id of the config file corresponding to a specific backend.
    ///
    /// # Errors
    ///
    /// * If listing the repository config file failed
    /// * If there is more than one repository config file.
    ///
    /// # Arguments
    ///
    /// * `be` - The backend to use
    ///
    /// # Returns
    ///
    /// The id of the config file or `None` if no config file is found
    fn config_id_with_backend(&self, be: &dyn WriteBackend) -> RusticResult<Option<ConfigId>> {
        let config_ids = be.list(FileType::Config)?;
        match config_ids.len() {
            1 => Ok(Some(ConfigId::from(config_ids[0]))),
            0 => Ok(None),
            _ => Err(RusticError::new(
                ErrorKind::Configuration,
                "More than one repository found for `{name}`. Please check the config file.",
            )
            .attach_context("name", self.name.clone())),
        }
    }

    /// Open the repository with a given credentials.
    ///
    /// This gets the decryption key and reads the config file
    ///
    /// # Arguments
    ///
    /// * `password` - The password to use
    ///
    /// # Errors
    ///
    /// * If no repository config file is found
    /// * If the keys of the hot and cold backend don't match
    /// * If the password is incorrect
    /// * If no suitable key is found
    /// * If listing the repository config file failed
    /// * If there is more than one repository config file
    pub fn open(self, credentials: &Credentials) -> RusticResult<Repository<OpenStatus>> {
        self.open_may_use_hot(credentials, true)
    }

    /// Open the repository with a given password using only the cold repository.
    ///
    /// This gets the decryption key and reads the config file
    ///
    /// # Arguments
    ///
    /// * `password` - The password to use
    ///
    /// # Errors
    ///
    /// * If no repository config file is found
    /// * If the keys of the hot and cold backend don't match
    /// * If the password is incorrect
    /// * If no suitable key is found
    /// * If listing the repository config file failed
    /// * If there is more than one repository config file
    pub fn open_only_cold(self, credentials: &Credentials) -> RusticResult<Repository<OpenStatus>> {
        self.open_may_use_hot(credentials, false)
    }

    fn open_may_use_hot(
        self,
        credentials: &Credentials,
        use_hot: bool,
    ) -> RusticResult<Repository<OpenStatus>> {
        let config_id = if use_hot && let Some(be) = &self.be_hot {
            self.config_id_with_backend(be)?
        } else {
            self.config_id()?
        }
        .ok_or_else(|| {
            RusticError::new(
                ErrorKind::Configuration,
                "No repository config file found for `{name}`. Please check the repository.",
            )
            .attach_context("name", self.name.clone())
        })?;

        if use_hot {
            if let Some(be_hot) = &self.be_hot {
                // check keys
                let mut keys = self.be.list_with_size(FileType::Key)?;
                keys.sort_unstable_by_key(|key| key.0);
                let mut hot_keys = be_hot.list_with_size(FileType::Key)?;
                hot_keys.sort_unstable_by_key(|key| key.0);
                if keys != hot_keys {
                    return Err(RusticError::new(
                    ErrorKind::Key,
                    "Keys of hot and cold repositories don't match for `{name}`. Please check the keys.",
                )
                .attach_context("name", self.name.clone()));
                }
            }
        } else {
            // warm-up keys
            let keys = self.be_cold.list(FileType::Key)?;
            if !keys.is_empty() {
                warm_up_wait(&self, FileType::Key, keys.into_iter())?;
            }
        }

        let (key, key_id) = match credentials {
            Credentials::Password(password) => {
                let (key, key_id) = if use_hot {
                    find_key_in_backend(&self.be, &password, None)?
                } else {
                    find_key_in_backend(&self.be_cold, &password, None)?
                };
                info!("repository {}: password is correct.", self.name);
                (key, Some(key_id))
            }
            Credentials::Masterkey(key) => (key.key(), None),
        };

        // Initialize a new repository with given credentials and options.
        let be = if use_hot {
            self.be.clone()
        } else {
            // warm-up config file
            self.warm_up_wait(std::iter::once(config_id))?;
            self.be_cold.clone()
        };
        let dbe = DecryptBackend::new(be, key);
        let mut config: ConfigFile = dbe.get_file(&config_id)?;
        if !use_hot && self.be_hot.is_some() {
            config.is_hot = Some(true);
        }
        self.open_raw(key, key_id, config)
    }

    /// Initialize a new repository with given credentials and options.
    ///
    /// This returns an open repository which can be directly used.
    ///
    /// # Arguments
    ///
    /// * `credentials` - The credentials
    /// * `key_opts` - The options to use for the key
    /// * `config_opts` - The options to use for the config
    ///
    /// # Errors
    ///
    /// * If a config file already exists
    /// * If listing the repository config file failed
    /// * If there is more than one repository config file
    pub fn init(
        self,
        credentials: &Credentials,
        key_opts: &KeyOptions,
        config_opts: &ConfigOptions,
    ) -> RusticResult<Repository<OpenStatus>> {
        let config_exists = self.config_id_with_backend(&self.be)?.is_some();
        let hot_config_exists = match self.be_hot {
            None => false,
            Some(ref be) => self.config_id_with_backend(be)?.is_some(),
        };
        if config_exists || hot_config_exists {
            return Err(RusticError::new(
                ErrorKind::Configuration,
                "Config file already exists for `{name}`. Please check the repository.",
            )
            .attach_context("name", self.name));
        }

        let (key, key_id, config) =
            commands::init::init(&self, credentials, key_opts, config_opts)?;

        self.open_raw(key, key_id, config)
    }

    /// Initialize a new repository with given password and a ready [`ConfigFile`].
    ///
    /// This returns an open repository which can be directly used.
    ///
    /// # Type Parameters
    ///
    /// * `P` - The type of the progress bar
    ///
    /// # Arguments
    ///
    /// * `password` - The password to use
    /// * `key_opts` - The options to use for the key
    /// * `config` - The config file to use
    ///
    /// # Errors
    ///
    // TODO: Document errors
    pub fn init_with_config(
        self,
        credentials: &Credentials,
        key_opts: &KeyOptions,
        config: ConfigFile,
    ) -> RusticResult<Repository<OpenStatus>> {
        let (key, key_id) =
            commands::init::init_with_config(&self, credentials, key_opts, &config)?;
        info!("repository {} successfully created.", config.id);
        self.open_raw(key, key_id, config)
    }

    /// Open the repository with given [`Key`] and [`ConfigFile`].
    ///
    /// # Type Parameters
    ///
    /// * `P` - The type of the progress bar
    ///
    /// # Arguments
    ///
    /// * `key` - The key to use
    /// * `config` - The config file to use
    ///
    /// # Errors
    ///
    /// * If the config file has `is_hot` set to `true` but the repository is not hot
    /// * If the config file has `is_hot` set to `false` but the repository is hot
    fn open_raw(
        mut self,
        key: Key,
        key_id: Option<KeyId>,
        config: ConfigFile,
    ) -> RusticResult<Repository<OpenStatus>> {
        match (config.is_hot == Some(true), self.be_hot.is_some()) {
            (true, false) => {
                return Err(RusticError::new(
                    ErrorKind::Repository,
                    "The given repository is a hot repository! Please use `--repo-hot` in combination with the normal repo. Aborting.",
                ));
            }
            (false, true) => {
                return Err(RusticError::new(
                    ErrorKind::Repository,
                    "The given repository is not a hot repository! Aborting.",
                ));
            }
            _ => {}
        }

        let cache = (!self.opts.no_cache)
            .then(|| Cache::new(config.id, self.opts.cache_dir.clone()).ok())
            .flatten();

        if let Some(cache) = &cache {
            self.be = CachedBackend::new_cache(self.be.clone(), cache.clone());
            info!("using cache at {}", cache.location());
        } else {
            info!("using no cache");
        }

        let mut dbe = DecryptBackend::new(self.be.clone(), key);
        dbe.set_zstd(config.zstd()?);
        dbe.set_extra_verify(config.extra_verify());

        let open = OpenStatus {
            cache,
            dbe,
            config,
            key_id,
        };

        Ok(Repository {
            name: self.name,
            be: self.be,
            be_hot: self.be_hot,
            be_cold: self.be_cold,
            opts: self.opts,
            pb: self.pb,
            status: open,
        })
    }

    /// List all file [`Id`]s of a [`FileType`] which are present in the repository
    ///
    /// # Errors
    ///
    // TODO: Document errors
    pub fn list<T: RepoId>(&self) -> RusticResult<impl Iterator<Item = T>> {
        Ok(self.be.list(T::TYPE)?.into_iter().map(Into::into))
    }

    /// Searches for matching ids of a [`FileType`] which are present in the repository
    ///
    /// # Arguments
    ///
    /// * `ids` - The list of (parts of the) ids of the snapshots
    ///
    /// # Errors
    ///
    // TODO: Document errors
    pub fn find_ids<I: RepoId, T: AsRef<str>>(
        &self,
        ids: &[T],
    ) -> RusticResult<impl Iterator<Item = I>> {
        Ok(self.be.find_ids(I::TYPE, ids)?.into_iter().map(I::from))
    }

    /// Collect information about repository files
    ///
    /// # Errors
    ///
    /// * If files could not be listed.
    pub fn infos_files(&self) -> RusticResult<RepoFileInfos> {
        commands::repoinfo::collect_file_infos(self)
    }

    /// Warm up the given repository files without waiting.
    ///
    /// # Arguments
    ///
    /// * `ids` - The ids to warm up
    ///
    /// # Errors
    ///
    /// * If the command could not be parsed.
    /// * If the thread pool could not be created.
    ///
    /// # Returns
    ///
    /// The result of the warm up
    pub fn warm_up<I: RepoId>(&self, ids: impl ExactSizeIterator<Item = I>) -> RusticResult<()> {
        warm_up(self, I::TYPE, ids.map(|id| *id))
    }

    /// Warm up the given pack files and wait the configured waiting time.
    ///
    /// # Arguments
    ///
    /// * `ids` - The ids to warm up
    ///
    /// # Errors
    ///
    /// * If the command could not be parsed.
    /// * If the thread pool could not be created.
    pub(crate) fn warm_up_wait<I: RepoId>(
        &self,
        ids: impl ExactSizeIterator<Item = I> + Clone,
    ) -> RusticResult<()> {
        warm_up_wait(self, I::TYPE, ids.map(|id| *id))
    }

    /// Repair hotcold files except packs
    ///
    /// This compares the files in the hot and cold repo part and copies missing ones.
    ///
    /// # Arguments
    ///
    /// * `dry_run` - If true, only print what would be done
    ///
    /// # Errors
    ///
    // TODO: Document errors
    pub fn repair_hotcold_except_packs(&self, dry_run: bool) -> RusticResult<()> {
        repair_hotcold(self, dry_run)
    }
}

impl<S: Open> Repository<S> {
    /// Get the content of the decrypted repository file given by id and [`FileType`]
    ///
    /// # Arguments
    ///
    /// * `tpe` - The type of the file to get
    /// * `id` - The id of the file to get
    ///
    /// # Errors
    ///
    /// * If the string is not a valid hexadecimal string
    /// * If no id could be found.
    /// * If the id is not unique.
    pub fn cat_file(&self, tpe: FileType, id: &str) -> RusticResult<Bytes> {
        commands::cat::cat_file(self, tpe, id)
    }

    /// Add a new key to the repository
    ///
    /// # Arguments
    ///
    /// * `pass` - The password to use for the new key
    /// * `opts` - The options to use for the new key
    ///
    /// # Errors
    ///
    /// * If the key could not be serialized.
    pub fn add_key(&self, pass: &str, opts: &KeyOptions) -> RusticResult<KeyId> {
        add_current_key_to_repo(self, opts, pass)
    }

    /// Update the repository config by applying the given [`ConfigOptions`]
    ///
    /// # Arguments
    ///
    /// * `opts` - The options to apply
    ///
    /// # Errors
    ///
    /// * If the version is not supported
    /// * If the version is lower than the current version
    /// * If compression is set for a v1 repo
    /// * If the compression level is not supported
    /// * If the size is too large
    /// * If the min pack size tolerance percent is wrong
    /// * If the max pack size tolerance percent is wrong
    /// * If the file could not be serialized to json.
    pub fn apply_config(&mut self, opts: &ConfigOptions) -> RusticResult<bool> {
        commands::config::apply_config(self, opts)
    }

    /// Get the repository configuration
    pub fn config(&self) -> &ConfigFile {
        &self.status.open_status().config
    }

    /// Set the repository configuration
    pub(crate) fn set_config(&mut self, config: ConfigFile) {
        self.status.open_status_mut().config = config;
    }

    // TODO: add documentation!
    pub(crate) fn dbe(&self) -> &DecryptBackend<Key> {
        &self.status.open_status().dbe
    }

    pub(crate) fn cache(&self) -> Option<&Cache> {
        self.status.open_status().cache.as_ref()
    }

    /// Get the [`KeyId`] of the key used to open the repository
    pub fn key_id(&self) -> &Option<KeyId> {
        &self.status.open_status().key_id
    }

    /// Get the [`MasterKey`] used to open the repository
    pub fn key(&self) -> MasterKey {
        MasterKey::from_key(*self.status.open_status().dbe.key())
    }

    /// Init only the hot repository, i.e. save the [`ConfigFile`] only to the hot part of a repository
    ///
    /// # Errors
    ///
    /// * If the config file could not be saved.
    pub fn init_hot(&self) -> RusticResult<()> {
        if let Some(hot_be) = self.be_hot.clone() {
            hot_be.create()?;
        }
        let config = self.config().clone();
        let key = *self.dbe().key();
        save_config_hot(self, config, key)
    }

    /// Delete the key with the given id
    ///
    /// # Errors
    ///
    /// * If the key could not be removed.
    pub fn delete_key(&self, id: &KeyId) -> RusticResult<()> {
        if self.key_id().as_ref() == Some(id) {
            return Err(RusticError::new(
                ErrorKind::Repository,
                "Cannot remove the currently used key",
            ));
        }
        self.dbe().remove(FileType::Key, id, false)
    }

    /// Get a single snapshot
    ///
    /// # Arguments
    ///
    /// * `id` - The id of the snapshot to get
    /// * `filter` - The filter to use
    ///
    /// # Errors
    ///
    /// * If the string is not a valid hexadecimal string
    /// * If no id could be found.
    /// * If the id is not unique.
    ///
    /// # Returns
    ///
    /// If `id` is (part of) an `Id`, return this snapshot.
    /// If `id` is "latest" or "latest~N", return the latest (or Nth latest) snapshot respecting the giving filter.
    pub fn get_snapshot_from_str(
        &self,
        id: &str,
        filter: impl FnMut(&SnapshotFile) -> bool + Send + Sync,
    ) -> RusticResult<SnapshotFile> {
        let p = self.progress_counter("getting snapshot...");
        let snap = SnapshotFile::from_str(self.dbe(), id, filter, &p)?;
        p.finish();
        Ok(snap)
    }

    /// Get a single snapshot
    ///
    /// # Arguments
    ///
    /// * `ids` - The ids of the snapshot to get
    ///   * each `id` can use an actual (short) id "01a2b3c4" or "latest" or "latest~N"
    /// * `filter` - The filter to use
    ///
    /// # Errors
    ///
    /// * If the string is not a valid hexadecimal string
    /// * If no id could be found.
    /// * If the id is not unique.
    ///
    /// # Returns
    ///
    /// If `id` is (part of) an `Id`, return this snapshot.
    /// If `id` is "latest" or "latest~N", return the latest (or Nth latest) snapshot respecting the giving filter.
    pub fn get_snapshots_from_strs<T: AsRef<str>>(
        &self,
        ids: &[T],
        filter: impl FnMut(&SnapshotFile) -> bool + Send + Sync,
    ) -> RusticResult<Vec<SnapshotFile>> {
        let p = self.progress_counter("getting snapshots...");
        let snaps = SnapshotFile::from_strs(self.dbe(), ids, filter, &p)?;
        p.finish();
        Ok(snaps)
    }

    /// Get the given snapshots.
    ///
    /// # Arguments
    ///
    /// * `ids` - The ids of the snapshots to get
    ///
    /// # Notes
    ///
    /// `ids` may contain part of snapshots id which will be resolved.
    /// However, "latest" is not supported in this function.
    ///
    /// # Errors
    ///
    // TODO: Document errors
    pub fn get_snapshots<T: AsRef<str>>(&self, ids: &[T]) -> RusticResult<Vec<SnapshotFile>> {
        self.update_snapshots(Vec::new(), ids)
    }

    /// Update the given snapshots.
    ///
    /// # Arguments
    ///
    /// * `current` - The existing snapshots
    /// * `ids` - The ids of the snapshots to get
    ///
    /// # Notes
    ///
    /// `ids` may contain part of snapshots id which will be resolved.
    /// However, "latest" is not supported in this function.
    ///
    /// # Errors
    ///
    // TODO: Document errors
    pub fn update_snapshots<T: AsRef<str>>(
        &self,
        current: Vec<SnapshotFile>,
        ids: &[T],
    ) -> RusticResult<Vec<SnapshotFile>> {
        let p = self.progress_counter("getting snapshots...");
        let result = SnapshotFile::update_from_ids(self.dbe(), current, ids, &p);
        p.finish();
        result
    }

    /// Get all snapshots from the repository
    ///
    /// # Errors
    ///
    // TODO: Document errors
    pub fn get_all_snapshots(&self) -> RusticResult<Vec<SnapshotFile>> {
        self.get_matching_snapshots(|_| true)
    }

    /// Update existing snapshots to all from the repository
    ///
    /// # Arguments
    ///
    /// * `current` - The existing snapshots
    ///
    /// # Errors
    ///
    // TODO: Document errors
    pub fn update_all_snapshots(
        &self,
        current: Vec<SnapshotFile>,
    ) -> RusticResult<Vec<SnapshotFile>> {
        self.update_matching_snapshots(current, |_| true)
    }

    /// Get all snapshots from the repository respecting the given `filter`
    ///
    /// # Arguments
    ///
    /// * `filter` - The filter to use
    ///
    /// # Errors
    ///
    // TODO: Document errors
    ///
    /// # Note
    ///
    /// The result is not sorted and may come in random order!
    pub fn get_matching_snapshots(
        &self,
        filter: impl FnMut(&SnapshotFile) -> bool,
    ) -> RusticResult<Vec<SnapshotFile>> {
        self.update_matching_snapshots(Vec::new(), filter)
    }

    /// Update existing snapshots to all from the repository respecting the given `filter`
    ///
    /// # Arguments
    ///
    /// * `current` - The existing snapshots
    /// * `filter` - The filter to use
    ///
    /// # Errors
    ///
    // TODO: Document errors
    ///
    /// # Note
    ///
    /// The result is not sorted and may come in random order!
    pub fn update_matching_snapshots(
        &self,
        current: Vec<SnapshotFile>,
        filter: impl FnMut(&SnapshotFile) -> bool,
    ) -> RusticResult<Vec<SnapshotFile>> {
        let p = self.progress_counter("getting snapshots...");
        let result = SnapshotFile::update_from_backend(self.dbe(), current, filter, &p);
        p.finish();
        result
    }

    /// Get snapshots which are not already present and should be present.
    ///
    /// # Arguments
    ///
    /// * `filter` - The filter to use
    /// * `snaps` - The snapshots to check
    ///
    /// # Errors
    ///
    // TODO: Document errors
    ///
    /// # Note
    ///
    /// This method should be called on the *destination repository*
    pub fn relevant_copy_snapshots(
        &self,
        filter: impl FnMut(&SnapshotFile) -> bool,
        snaps: &[SnapshotFile],
    ) -> RusticResult<Vec<CopySnapshot>> {
        commands::copy::relevant_snapshots(snaps, self, filter)
    }

    // TODO: Maybe only offer a method to remove &[Snapshotfile] and check if they must be kept.
    // See e.g. the merge command of the CLI
    /// Remove the given snapshots from the repository
    ///
    /// # Arguments
    ///
    /// * `ids` - The ids of the snapshots to remove
    ///
    /// # Errors
    ///
    // TODO: Document errors
    ///
    /// # Panics
    ///
    /// * If the files could not be deleted.
    pub fn delete_snapshots(&self, ids: &[SnapshotId]) -> RusticResult<()> {
        if self.config().append_only == Some(true) {
            return Err(RusticError::new(
                ErrorKind::Repository,
                "Repository is in append-only mode and snapshots cannot be deleted from it. Aborting.",
            ));
        }
        let p = self.progress_counter("removing snapshots...");
        self.dbe().delete_list(true, ids.iter(), p)?;
        Ok(())
    }

    /// Save the given snapshots to the repository.
    ///
    /// # Arguments
    ///
    /// * `snaps` - The snapshots to save
    ///
    /// # Errors
    ///
    /// * If the file could not be serialized to json.
    pub fn save_snapshots(&self, mut snaps: Vec<SnapshotFile>) -> RusticResult<()> {
        for snap in &mut snaps {
            snap.id = SnapshotId::default();
        }
        let p = self.progress_counter("saving snapshots...");
        self.dbe().save_list(snaps.iter(), p)?;
        Ok(())
    }

    /// Check the repository and all snapshot trees for errors or inconsistencies
    ///
    /// # Arguments
    ///
    /// * `opts` - The options to use
    ///
    /// # Errors
    ///
    // TODO: Document errors
    ///
    /// # Panics
    ///
    // TODO: Document panics
    pub fn check(&self, opts: CheckOptions) -> RusticResult<CheckResults> {
        let trees = self
            .get_all_snapshots()?
            .into_iter()
            .map(|snap| snap.tree)
            .collect();

        check_repository(self, opts, trees)
    }

    /// Check the repository and given trees for errors or inconsistencies
    ///
    /// # Arguments
    ///
    /// * `opts` - The options to use
    ///
    /// # Errors
    ///
    // TODO: Document errors
    /// # Panics
    ///
    // TODO: Document panics
    pub fn check_with_trees(
        &self,
        opts: CheckOptions,
        trees: Vec<TreeId>,
    ) -> RusticResult<CheckResults> {
        check_repository(self, opts, trees)
    }

    /// Get the plan about what should be pruned and/or repacked.
    ///
    /// # Arguments
    ///
    /// * `opts` - The options to use
    ///
    /// # Errors
    ///
    // TODO: Document errors
    ///
    /// # Returns
    ///
    /// The plan about what should be pruned and/or repacked.
    pub fn prune_plan(&self, opts: &PruneOptions) -> RusticResult<PrunePlan> {
        PrunePlan::from_prune_options(self, opts)
    }

    /// Perform the pruning on the repository.
    ///
    /// # Arguments
    ///
    /// * `opts` - The options for the pruning
    /// * `prune_plan` - The plan about what should be pruned and/or repacked
    ///
    /// # Errors
    ///
    /// * If the repository is in append-only mode
    /// * If a pack has no decision
    ///
    /// # Returns
    ///
    /// * `Ok(())` - If the pruning was successful
    ///
    /// # Panics
    ///
    // TODO: Document panics
    pub fn prune(&self, opts: &PruneOptions, prune_plan: PrunePlan) -> RusticResult<()> {
        prune_repository(self, opts, prune_plan)
    }

    /// Turn the repository into the `IndexedFull` state by reading and storing the index
    ///
    /// # Errors
    ///
    // TODO: Document errors
    ///
    /// # Note
    ///
    /// This saves the full index in memory which can be quite memory-consuming!
    pub fn to_indexed(self) -> RusticResult<Repository<IndexedFullStatus>> {
        let index = GlobalIndex::new(self.dbe(), &self.progress_counter(""))?;
        Ok(self.into_indexed_with_index(index))
    }

    /// Turn the repository into the `IndexedFull` state by reading and storing the index
    ///
    /// This is similar to `to_indexed()`, but also lists the pack files and reads pack headers
    /// for packs is missing in the index.
    ///
    /// # Errors
    ///
    // TODO: Document errors
    ///
    /// # Note
    ///
    /// This saves the full index in memory which can be quite memory-consuming!
    pub fn to_indexed_checked(self) -> RusticResult<Repository<IndexedFullStatus>> {
        let collector = IndexCollector::new(IndexType::Full);
        let index = index_checked_from_collector(&self, collector)?;
        Ok(self.into_indexed_with_index(index))
    }

    // helper function to deduplicate code
    fn into_indexed_with_index(self, index: GlobalIndex) -> Repository<IndexedFullStatus> {
        let status = IndexedFullStatus {
            open: self.status.into_open_status(),
            index,
            // TODO: Make cache size (32MB currently) customizable!
            cache: quick_cache::sync::Cache::with_weighter(
                constants::ESTIMATED_ITEM_CAPACITY,
                constants::WEIGHT_CAPACITY,
                BytesWeighter {},
            ),
        };
        Repository {
            name: self.name,
            be: self.be,
            be_hot: self.be_hot,
            be_cold: self.be_cold,
            opts: self.opts,
            pb: self.pb,
            status,
        }
    }

    /// Turn the repository into the `IndexedIds` state by reading and storing a size-optimized index
    ///
    /// # Errors
    ///
    // TODO: Document errors
    ///
    /// # Returns
    ///
    /// The repository in the `IndexedIds` state
    ///
    /// # Note
    ///
    /// This saves only the `Id`s for data blobs. Therefore, not all operations are possible on the repository.
    /// However, operations which add data are fully functional.
    pub fn to_indexed_ids(self) -> RusticResult<Repository<IndexedIdsStatus>> {
        let index = GlobalIndex::only_full_trees(self.dbe(), &self.progress_counter(""))?;
        Ok(self.into_indexed_ids_with_index(index))
    }

    /// Turn the repository into the `IndexedIds` state by reading and storing a size-optimized index
    ///
    /// This is similar to `to_indexed_ids()`, but also lists the pack files and reads pack headers
    /// for packs is missing in the index.
    ///
    /// # Errors
    ///
    // TODO: Document errors
    ///
    /// # Returns
    ///
    /// The repository in the `IndexedIds` state
    ///
    /// # Note
    ///
    /// This saves only the `Id`s for data blobs. Therefore, not all operations are possible on the repository.
    /// However, operations which add data are fully functional.
    pub fn to_indexed_ids_checked(self) -> RusticResult<Repository<IndexedIdsStatus>> {
        let collector = IndexCollector::new(IndexType::DataIds);
        let index = index_checked_from_collector(&self, collector)?;
        Ok(self.into_indexed_ids_with_index(index))
    }

    // helper function to deduplicate code
    fn into_indexed_ids_with_index(self, index: GlobalIndex) -> Repository<IndexedIdsStatus> {
        let status = IndexedIdsStatus {
            open: self.status.into_open_status(),
            index,
        };
        Repository {
            name: self.name,
            be: self.be,
            be_hot: self.be_hot,
            be_cold: self.be_cold,
            opts: self.opts,
            pb: self.pb,
            status,
        }
    }

    /// Get statistical information from the index. This method reads all index files,
    /// even if an index is already available in memory.
    ///
    /// # Errors
    ///
    /// * If the index could not be read.
    ///
    /// # Returns
    ///
    /// The statistical information from the index.
    pub fn infos_index(&self) -> RusticResult<IndexInfos> {
        commands::repoinfo::collect_index_infos(self)
    }

    /// Read a given [`RepoFile`]
    ///
    /// # Errors
    ///
    /// If the file cannot be read or processed
    ///
    /// # Returns
    ///
    /// The file
    pub fn get_file<F: RepoFile>(&self, id: &F::Id) -> RusticResult<F> {
        self.dbe().get_file(id)
    }

    /// Read all files of a given [`RepoFile`]
    ///
    /// # Errors
    ///
    // TODO: Document errors
    ///
    /// # Returns
    ///
    /// An iterator over all files of the given type
    ///
    /// # Note
    ///
    /// The result is not sorted and may come in random order!
    pub fn stream_files<F: RepoFile>(
        &self,
    ) -> RusticResult<impl Iterator<Item = RusticResult<(F::Id, F)>>> {
        Ok(self
            .dbe()
            .stream_all::<F>(&self.progress_hidden())?
            .into_iter())
    }

    /// Read the files [`RepoFile`] of a given list of IDs.
    ///
    /// # Errors
    ///
    // TODO: Document errors
    ///
    /// # Returns
    ///
    /// An iterator over the resulting files of the given type
    ///
    /// # Note
    ///
    /// The result is not sorted and may come in random order!
    pub fn stream_files_list<F: RepoFile>(
        &self,
        list: Vec<F::Id>,
    ) -> RusticResult<impl Iterator<Item = RusticResult<(F::Id, F)>>> {
        Ok(self
            .dbe()
            .stream_list::<F>(list, &self.progress_hidden())?
            .into_iter())
    }

    /// Repair the index
    ///
    /// This compares the index with existing pack files and reads packfile headers to ensure the index
    /// correctly represents the pack files.
    ///
    /// # Arguments
    ///
    /// * `opts` - The options to use
    /// * `dry_run` - If true, only print what would be done
    ///
    /// # Errors
    ///
    // TODO: Document errors
    pub fn repair_index(&self, opts: &RepairIndexOptions, dry_run: bool) -> RusticResult<()> {
        repair_index(self, *opts, dry_run)
    }

    /// Repair hotcold packs
    ///
    /// This compares the pack files in the hot and cold repo part and copies missing ones.
    ///
    /// # Arguments
    ///
    /// * `dry_run` - If true, only print what would be done
    ///
    /// # Errors
    ///
    // TODO: Document errors
    pub fn repair_hotcold_packs(&self, dry_run: bool) -> RusticResult<()> {
        repair_hotcold_packs(self, dry_run)
    }

    /// Rewrite snapshots using snapshot modifications.
    ///
    /// # Arguments
    ///
    /// * `snapshots` - The snapshots to rewrite
    /// * `modification` - The modification(s) to apply to each snapshot
    /// * `remove` - If true, remove old snapshots
    /// * `dry_run` - If true, only print what would be done
    ///  
    /// # Errors
    ///
    // TODO: Document errors
    pub fn rewrite_snapshots(
        &self,
        snapshots: Vec<SnapshotFile>,
        opts: &RewriteOptions,
    ) -> RusticResult<Vec<SnapshotFile>> {
        rewrite_snapshots(self, snapshots, opts)
    }
}

impl<S: IndexedFull> Repository<S> {
    /// Get the [`IndexEntry`] of the given blob
    ///
    /// # Arguments
    ///
    /// * `tpe` - The type of the blob
    /// * `id` - The id of the blob
    ///
    /// # Errors
    ///
    /// * If the id is not found in the index
    pub fn get_index_entry<T: PackedId>(&self, id: &T) -> RusticResult<IndexEntry> {
        let blob_id: BlobId = (*id).into();
        let ie = self.index().get_id(T::TYPE, &blob_id).ok_or_else(|| {
            RusticError::new(
                ErrorKind::Internal,
                "Blob ID `{id}` not found in index, but should be there.",
            )
            .attach_context("id", blob_id.to_string())
            .ask_report()
        })?;

        Ok(ie)
    }

    /// Open a file in the repository for reading
    ///
    /// # Arguments
    ///
    /// * `node` - The node to open
    ///
    /// # Errors
    ///
    // TODO: Document errors
    pub fn open_file(&self, node: &Node) -> RusticResult<OpenFile> {
        OpenFile::from_node(self, node)
    }

    /// Reads an opened file at the given position
    ///
    /// # Arguments
    ///
    /// * `open_file` - The opened file
    /// * `offset` - The offset to start reading
    /// * `length` - The length to read
    ///
    /// # Returns
    ///
    /// The read bytes from the given offset and length.
    /// If offset is behind the end of the file, an empty `Bytes` is returned.
    /// If length is too large, the result up to the end of the file is returned.
    ///
    /// # Errors
    ///
    // TODO: Document errors
    pub fn read_file_at(
        &self,
        open_file: &OpenFile,
        offset: usize,
        length: usize,
    ) -> RusticResult<Bytes> {
        open_file.read_at(self, offset, length)
    }
}

impl<S: IndexedTree> Repository<S> {
    /// Get a [`Tree`] by [`Id`] from the repository.
    ///
    /// # Arguments
    ///
    /// * `id` - The `Id` of the tree
    // TODO!: This ID should be a tree ID, we should refactor it to wrap it in a TreeId type
    ///
    /// # Errors
    ///
    /// * If the tree ID is not found in the backend.
    /// * If deserialization fails.
    ///
    /// # Returns
    ///
    /// The tree with the given `Id`
    pub fn get_tree(&self, id: &TreeId) -> RusticResult<Tree> {
        Tree::from_backend(self.dbe(), self.index(), *id)
    }

    /// Get a [`Node`] from a root tree and a path
    ///
    /// This traverses into the path to get the node.
    ///
    /// # Arguments
    ///
    /// * `root_tree` - The `TreeId` of the root tree
    /// * `path` - The path
    ///
    /// # Errors
    ///
    /// * If the path is not a directory.
    /// * If the path is not found.
    /// * If the path is not UTF-8 conform.
    pub fn node_from_path(&self, root_tree: TreeId, path: &Path) -> RusticResult<Node> {
        Tree::node_from_path(self.dbe(), self.index(), root_tree, Path::new(path))
    }

    /// Get all [`Node`]s from given root trees and a path
    ///
    /// # Arguments
    ///
    /// * `ids` - The tree ids to search in
    /// * `path` - The path
    ///
    /// # Errors
    ///
    /// * If loading trees from the backend fails
    pub fn find_nodes_from_path(
        &self,
        ids: impl IntoIterator<Item = TreeId>,
        path: &Path,
    ) -> RusticResult<FindNode> {
        Tree::find_nodes_from_path(self.dbe(), self.index(), ids, path)
    }

    /// Get all [`Node`]s/[`Path`]s from given root trees and a matching criterion
    ///
    /// # Arguments
    ///
    /// * `ids` - The tree ids to search in
    /// * `matches` - The matching criterion
    ///
    /// # Errors
    ///
    /// * If loading trees from the backend fails
    pub fn find_matching_nodes(
        &self,
        ids: impl IntoIterator<Item = TreeId>,
        matches: &impl Fn(&Path, &Node) -> bool,
    ) -> RusticResult<FindMatches> {
        Tree::find_matching_nodes(self.dbe(), self.index(), ids, matches)
    }

    /// drop the `Repository` index leaving an `Open` `Repository`
    pub fn drop_index(self) -> Repository<OpenStatus> {
        Repository {
            name: self.name,
            be: self.be,
            be_hot: self.be_hot,
            be_cold: self.be_cold,
            opts: self.opts,
            pb: self.pb,
            status: self.status.into_open_status(),
        }
    }
}

impl<S: IndexedTree> Repository<S> {
    pub(crate) fn index(&self) -> &impl ReadGlobalIndex {
        self.status.index()
    }

    /// Get a [`Node`] from a "SNAP\[:PATH\]" syntax
    ///
    /// This parses for a snapshot (using the filter when "latest" is used) and then traverses into the path to get the node.
    ///
    /// # Arguments
    ///
    /// * `snap_path` - The path to the snapshot
    /// * `filter` - The filter to use
    ///
    /// # Errors
    ///
    /// * If the string is not a valid hexadecimal string
    /// * If no id could be found.
    /// * If the id is not unique.
    pub fn node_from_snapshot_path(
        &self,
        snap_path: &str,
        filter: impl FnMut(&SnapshotFile) -> bool + Send + Sync,
    ) -> RusticResult<Node> {
        let (id, path) = snap_path.split_once(':').unwrap_or((snap_path, ""));

        let p = &self.progress_counter("getting snapshot...");
        let snap = SnapshotFile::from_str(self.dbe(), id, filter, p)?;

        Tree::node_from_path(self.dbe(), self.index(), snap.tree, Path::new(path))
    }

    /// Get a [`Node`] from a [`SnapshotFile`] and a `path`
    ///
    /// This traverses into the path to get the node.
    ///
    /// # Arguments
    ///
    /// * `snap` - The snapshot to use
    /// * `path` - The path to the node
    ///
    /// # Errors
    ///
    // TODO: Document errors
    pub fn node_from_snapshot_and_path(
        &self,
        snap: &SnapshotFile,
        path: &str,
    ) -> RusticResult<Node> {
        Tree::node_from_path(self.dbe(), self.index(), snap.tree, Path::new(path))
    }
    /// Reads a raw tree from a "SNAP\[:PATH\]" syntax
    ///
    /// This parses a snapshot (using the filter when "latest" is used) and then traverses into the path to get the tree.
    ///
    /// # Arguments
    ///
    /// * `snap` - The snapshot to use
    /// * `sn_filter` - The filter to use
    ///
    /// # Errors
    ///
    // TODO: Document errors
    pub fn cat_tree(
        &self,
        snap: &str,
        sn_filter: impl FnMut(&SnapshotFile) -> bool + Send + Sync,
    ) -> RusticResult<Bytes> {
        commands::cat::cat_tree(self, snap, sn_filter)
    }

    /// List the contents of a given [`Node`]
    ///
    /// # Arguments
    ///
    /// * `node` - The node to list
    /// * `ls_opts` - The options to use
    ///
    /// # Returns
    ///
    /// If `node` is a tree node, this will list the content of that tree.
    /// If `node` is a file node, this will only return one element.
    ///
    /// # Note
    ///
    /// The `PathBuf` returned will be relative to the given `node`.
    ///
    /// # Errors
    ///
    // TODO: Document errors
    pub fn ls(
        &self,
        node: &Node,
        ls_opts: &LsOptions,
    ) -> RusticResult<impl Iterator<Item = RusticResult<(PathBuf, Node)>> + Clone + '_> {
        NodeStreamer::new_with_glob(self.dbe().clone(), self.index(), node, ls_opts)
    }

    /// Restore a given [`RestorePlan`] to a local destination
    ///
    /// # Arguments
    ///
    /// * `restore_infos` - The restore plan to use
    /// * `opts` - The options to use
    /// * `node_streamer` - The node streamer to use
    /// * `dest` - The destination to use
    ///
    /// # Errors
    ///
    // TODO: Document errors
    pub fn restore(
        &self,
        restore_infos: RestorePlan,
        opts: &RestoreOptions,
        node_streamer: impl Iterator<Item = RusticResult<(PathBuf, Node)>>,
        dest: &LocalDestination,
    ) -> RusticResult<()> {
        restore_repository(restore_infos, self, *opts, node_streamer, dest)
    }

    /// Merge the given trees.
    ///
    /// This method creates needed tree blobs within the repository.
    /// Merge conflicts (identical filenames which do not match) will be resolved using the ordering given by `cmp`.
    ///
    /// # Arguments
    ///
    /// * `trees` - The trees to merge
    /// * `cmp` - The comparison function to use for merge conflicts
    /// * `summary` - The summary to use
    ///
    /// # Errors
    ///
    // TODO: Document errors
    ///
    /// # Returns
    ///
    /// This method returns the blob [`Id`] of the merged tree.
    pub fn merge_trees(
        &self,
        trees: &[TreeId],
        cmp: &impl Fn(&Node, &Node) -> Ordering,
        summary: &mut SnapshotSummary,
    ) -> RusticResult<TreeId> {
        commands::merge::merge_trees(self, trees, cmp, summary)
    }

    /// Merge the given snapshots.
    ///
    /// This method will create needed tree blobs within the repository.
    /// Merge conflicts (identical filenames which do not match) will be resolved using the ordering given by `cmp`.
    ///
    /// # Arguments
    ///
    /// * `snaps` - The snapshots to merge
    /// * `cmp` - The comparison function to use for merge conflicts
    /// * `snap` - The snapshot to save
    ///
    /// # Errors
    ///
    // TODO: Document errors
    ///
    /// # Returns
    ///
    /// This method returns the modified and already saved [`SnapshotFile`].
    pub fn merge_snapshots(
        &self,
        snaps: &[SnapshotFile],
        cmp: &impl Fn(&Node, &Node) -> Ordering,
        snap: SnapshotFile,
    ) -> RusticResult<SnapshotFile> {
        commands::merge::merge_snapshots(self, snaps, cmp, snap)
    }
}

impl<S: IndexedIds> Repository<S> {
    /// Run a backup of `source` using the given options.
    ///
    /// You have to give a preflled [`SnapshotFile`] which is modified and saved.
    ///
    /// # Arguments
    ///
    /// * `opts` - The options to use
    /// * `source` - The source to backup
    /// * `snap` - The snapshot to modify and save
    ///
    /// # Errors
    ///
    // TODO: Document errors
    ///
    /// # Returns
    ///  
    /// The saved snapshot.
    pub fn backup(
        &self,
        opts: &BackupOptions,
        source: &PathList,
        snap: SnapshotFile,
    ) -> RusticResult<SnapshotFile> {
        commands::backup::backup(self, opts, source, snap)
    }

    /// Run a backup of `source` using a `ReadSource`.
    ///
    /// You have to give a preflled [`SnapshotFile`] which is modified and saved.
    ///
    /// # Arguments
    ///
    /// * `opts` - The options to use
    /// * `src` - The source to backup
    /// * `snap` - The snapshot to modify and save
    ///
    /// # Errors
    ///
    // TODO: Document errors
    ///
    /// # Returns
    ///  
    /// The saved snapshot.
    pub fn archive<R>(
        &self,
        opts: &BackupOptions,
        src: &R,
        snap: SnapshotFile,
        backup_paths: &[PathBuf],
    ) -> RusticResult<SnapshotFile>
    where
        S: IndexedIds,
        R: ReadSource + 'static,
        <R as ReadSource>::Open: Send,
        <R as ReadSource>::Iter: Send,
    {
        commands::backup::archive(self, opts, src, snap, backup_paths)
    }
}

impl<S: IndexedFull> Repository<S> {
    /// Get a blob utilizing the internal blob cache
    ///
    /// # Arguments
    ///
    /// * `id` - The id of the blob
    /// * `tpe` - The type of the blob
    ///
    /// # Errors
    ///
    /// * If the blob is not found in the index
    ///
    /// # Returns
    ///
    /// The cached blob in bytes.
    pub fn get_blob_cached(&self, id: &BlobId, tpe: BlobType) -> RusticResult<Bytes> {
        self.status
            .get_blob_or_insert_with(id, || self.index().blob_from_backend(self.dbe(), tpe, id))
    }

    /// drop the data pack information from the `Repository` index leaving an `IndexedTree` `Repository`
    pub fn drop_data_from_index(self) -> Repository<impl IndexedTree> {
        Repository {
            name: self.name,
            be: self.be,
            be_hot: self.be_hot,
            be_cold: self.be_cold,
            opts: self.opts,
            pb: self.pb,
            status: self.status.into_indexed_tree(),
        }
    }
}

impl<S: IndexedFull> Repository<S> {
    /// Read a raw blob
    ///
    /// # Arguments
    ///
    /// * `tpe` - The type of the blob
    /// * `id` - The id of the blob
    ///
    /// # Errors
    ///
    /// * If the string is not a valid hexadecimal string
    ///
    /// # Returns
    ///
    /// The raw blob in bytes.
    pub fn cat_blob(&self, tpe: BlobType, id: &str) -> RusticResult<Bytes> {
        commands::cat::cat_blob(self, tpe, id)
    }

    /// Dump a [`Node`] using the given writer.
    ///
    /// # Arguments
    ///
    /// * `node` - The node to dump
    /// * `w` - The writer to use
    ///
    /// # Errors
    ///
    /// * If the node is not a file.
    ///
    /// # Note
    ///
    /// Currently, only regular file nodes are supported.
    pub fn dump(&self, node: &Node, w: &mut impl Write) -> RusticResult<()> {
        commands::dump::dump(self, node, w)
    }

    /// Prepare the restore.
    ///
    /// If `dry_run` is set to false, it will also:
    /// - remove existing files from the destination, if `opts.delete` is set to true
    /// - create all dirs for the restore
    ///
    /// # Arguments
    ///
    /// * `opts` - The options to use
    /// * `node_streamer` - The node streamer to use
    /// * `dest` - The destination to use
    /// * `dry_run` - If true, only print what would be done
    ///
    /// # Errors
    ///
    /// * If a directory could not be created.
    /// * If the restore information could not be collected.
    ///
    /// # Returns
    ///
    /// The restore plan.
    pub fn prepare_restore(
        &self,
        opts: &RestoreOptions,
        node_streamer: impl Iterator<Item = RusticResult<(PathBuf, Node)>>,
        dest: &LocalDestination,
        dry_run: bool,
    ) -> RusticResult<RestorePlan> {
        collect_and_prepare(self, *opts, node_streamer, dest, dry_run)
    }

    /// Copy the given `snapshots` to `repo_dest`.
    ///
    /// # Type Parameters
    ///
    /// * `Q` - The type of the progress bar
    /// * `R` - The type of the index.
    ///
    /// # Arguments
    ///
    /// * `repo_dest` - The destination repository
    /// * `snapshots` - The snapshots to copy
    ///
    /// # Errors
    ///
    // TODO: Document errors
    ///
    /// # Note
    ///
    /// This command copies snapshots even if they already exist. For already existing snapshots, a
    /// copy will be created in the destination repository.
    ///
    /// To omit already existing snapshots, use `relevant_copy_snapshots` and filter out the non-relevant ones.
    pub fn copy<'a, R: IndexedIds>(
        &self,
        repo_dest: &Repository<R>,
        snapshots: impl IntoIterator<Item = &'a SnapshotFile>,
    ) -> RusticResult<()> {
        commands::copy::copy(self, repo_dest, snapshots)
    }

    /// Repair snapshots.
    ///
    /// This traverses all trees of all snapshots and repairs defect trees.
    ///
    /// # Arguments
    ///
    /// * `opts` - The options to use
    /// * `snapshots` - The snapshots to repair
    /// * `dry_run` - If true, only print what would be done
    ///  
    /// # Warning
    ///
    /// * If you remove the original snapshots, you may loose data!
    ///
    /// # Errors
    ///
    // TODO: Document errors
    pub fn repair_snapshots(
        &self,
        opts: &RepairSnapshotsOptions,
        snapshots: Vec<SnapshotFile>,
        dry_run: bool,
    ) -> RusticResult<()> {
        repair_snapshots(self, opts, snapshots, dry_run)
    }

    /// Rewrite snapshots and trees using snapshot modifications and excludes.
    ///
    /// This traverses all trees of all given snapshots.
    ///
    /// # Arguments
    ///
    /// * `snapshots` - The snapshots to rewrite
    /// * `modification` - The modification(s) to apply to each snapshot
    /// * `excludes` - The excludes to apply to each snapshot tree
    /// * `remove` - If true, remove old snapshots
    /// * `dry_run` - If true, only print what would be done
    ///  
    /// # Warning
    ///
    /// * If you remove the original snapshots, you may loose data!
    ///
    /// # Errors
    ///
    // TODO: Document errors
    pub fn rewrite_snapshots_and_trees(
        &self,
        snapshots: Vec<SnapshotFile>,
        opts: &RewriteOptions,
        tree_opts: &RewriteTreesOptions,
    ) -> RusticResult<Vec<SnapshotFile>> {
        rewrite_snapshots_and_trees(self, snapshots, opts, tree_opts)
    }
}
