//! An ergonomic wrapper around the `radicle` crate.
//!
//! The purpose of this module is to make it more convenient to use
//! the `radicle` crate to access a [Radicle](https://radicle.dev/)
//! node and information in the node. It is not, in any way, meant to
//! be a replacement for using the official crate directly.

use std::str::FromStr;

use radicle::{
    cob::patch::{Patch, PatchId, cache::Patches},
    identity::{Project, RepoId},
    profile::Profile,
    storage::{ReadStorage, RepositoryInfo, git::Repository},
};

pub use radicle::git::Oid;

/// A Radicle node.
///
/// This type represents a Radicle node, and exists to cache some
/// stuff so it doesn't need to be re-loaded on every function call.
/// Especially the node profile.
pub struct Radicle {
    profile: Profile,
}

impl Radicle {
    /// Create a new [`Radicle`]. This may fail.
    pub fn new() -> Result<Self, ErgoError> {
        Ok(Self {
            profile: Profile::load().map_err(ErgoError::load_profile)?,
        })
    }

    /// Return the loaded profile.
    pub fn profile(&self) -> &Profile {
        &self.profile
    }

    /// List all repositories on a node.
    pub fn repositories(&self) -> Result<Vec<RepositoryInfo>, ErgoError> {
        self.profile
            .storage
            .repositories()
            .map_err(ErgoError::list_repositories)
    }

    /// Load information about a specific repository.
    pub fn repository(&self, repo_id: &RepoId) -> Result<Repository, ErgoError> {
        self.profile
            .storage
            .repository(*repo_id)
            .map_err(|err| ErgoError::load_repository(repo_id, err))
    }

    /// Load a repository by name, if the name is unique.
    pub fn repository_by_name(&self, wanted: &str) -> Result<Repository, ErgoError> {
        let matching: Result<Vec<RepoId>, ErgoError> = self
            .repositories()?
            .iter()
            .filter_map(|ri| match self.project(&ri.rid) {
                Ok(project) if project.name() == wanted => Some(Ok(ri.rid)),
                Err(err) => Some(Err(err)),
                _ => None,
            })
            .collect();
        let matching = matching?;

        match matching[..] {
            [] => Err(ErgoError::no_repository_with_name(wanted)),
            [_] => {
                let repo_id = matching[0];
                let repo = self.repository(&repo_id)?;
                Ok(repo)
            }
            [_, _, ..] => Err(ErgoError::name_is_not_unique(wanted)),
        }
    }

    /// Load the project payload in the identity document of a
    /// repository.
    pub fn project(&self, repo_id: &RepoId) -> Result<Project, ErgoError> {
        let repo = self.repository(repo_id)?;
        repo.project()
            .map_err(|err| ErgoError::load_project(repo_id, err))
    }

    /// Load all patches in a repository.
    pub fn patches(&self, repo_id: &RepoId) -> Result<Vec<(PatchId, Patch)>, ErgoError> {
        let repo = self.repository(repo_id)?;
        let patches = self
            .profile
            .home
            .patches(&repo)
            .map_err(|err| ErgoError::load_patches(repo_id, err))?;
        let mut items = vec![];
        let list = patches
            .list()
            .map_err(|err| ErgoError::list_cache(repo_id, err))?;
        for result in list {
            let (id, patch) = result.map_err(|err| ErgoError::cache_list_item(repo_id, err))?;
            items.push((id, patch));
        }
        Ok(items)
    }

    /// Load a specific patch.
    pub fn patch(&self, repo_id: &RepoId, patch_id: &PatchId) -> Result<Patch, ErgoError> {
        let repo = self.repository(repo_id)?;
        let patches = self
            .profile
            .home
            .patches(&repo)
            .map_err(|err| ErgoError::LoadPatches(*repo_id, Box::new(err)))?;
        patches
            .get(patch_id)
            .map_err(|err| ErgoError::get_patch(repo_id, patch_id, err))?
            .ok_or(ErgoError::no_such_patch(repo_id, patch_id))
    }

    /// Resolve a shortened patch ID into a full patch ID.
    pub fn resolve_patch_id(&self, repo_id: &RepoId, id: &str) -> Result<PatchId, ErgoError> {
        let repo = self.repository(repo_id)?;
        let object = repo
            .backend
            .revparse_single(id)
            .map_err(|err| ErgoError::resolve_patch_id(id, err))?;
        Ok(PatchId::from(object.id()))
    }

    /// Resolve a short commit or name into a full commit ID.
    pub fn resolve_commit(&self, repo_id: &RepoId, gitref: &str) -> Result<Oid, ErgoError> {
        if let Ok(oid) = Oid::from_str(gitref) {
            Ok(oid)
        } else {
            let repo = self.repository(repo_id)?;
            let object = repo
                .backend
                .revparse_single(gitref)
                .map_err(|err| ErgoError::resolve_commit(gitref, repo_id, err))?;
            Ok(Oid::from(object.id()))
        }
    }
}

/// Errors from the `Radicle` type.
#[derive(Debug, thiserror::Error)]
pub enum ErgoError {
    #[error("failed to load Radicle profile")]
    LoadProfile(#[source] Box<dyn std::error::Error + Send + Sync>),

    #[error("failed to list repositories in Radicle node storage")]
    ListRepositories(#[source] Box<dyn std::error::Error + Send + Sync>),

    #[error("failed to load info from Radicle node storage for repository {0}")]
    LoadRepo(RepoId, #[source] Box<dyn std::error::Error + Send + Sync>),

    #[error("failed to load project info from Radicle node storage for repository {0}")]
    LoadProject(RepoId, #[source] Box<dyn std::error::Error + Send + Sync>),

    #[error("failed to load patch list from Radicle node storage for repository {0}")]
    LoadPatches(RepoId, #[source] Box<dyn std::error::Error + Send + Sync>),

    #[error("failed to list patches for repository {0}")]
    ListCache(RepoId, #[source] Box<dyn std::error::Error + Send + Sync>),

    #[error("failed to list info for patch {0}")]
    CacheListItem(RepoId, #[source] Box<dyn std::error::Error + Send + Sync>),

    #[error("failed to resolve patch id {0:?} in repository {1}")]
    ResolvePatchId(String, #[source] Box<dyn std::error::Error + Send + Sync>),

    #[error("failed to resolve commit id {0:?} in repository {1}")]
    ResolveCommit(
        String,
        RepoId,
        #[source] Box<dyn std::error::Error + Send + Sync>,
    ),

    #[error("failed to load patch {1} from Radicle node storage for repository {0}")]
    GetPatch(
        RepoId,
        PatchId,
        #[source] Box<dyn std::error::Error + Send + Sync>,
    ),

    #[error("no patch {1} in repository {0}")]
    NoSuchPatch(RepoId, PatchId),

    #[error("no repository called {0:?}")]
    NoRepositoryWithName(String),

    #[error("repository name is not unique: {0:?}")]
    NameIsNotUnique(String),
}

impl ErgoError {
    pub(crate) fn load_profile(err: radicle::profile::Error) -> Self {
        Self::LoadProfile(Box::new(err))
    }

    pub(crate) fn list_repositories(err: radicle::storage::Error) -> Self {
        Self::ListRepositories(Box::new(err))
    }

    pub(crate) fn load_repository(id: &RepoId, err: radicle::storage::RepositoryError) -> Self {
        Self::LoadRepo(*id, Box::new(err))
    }

    pub(crate) fn load_project(id: &RepoId, err: radicle::storage::RepositoryError) -> Self {
        Self::LoadRepo(*id, Box::new(err))
    }

    pub(crate) fn load_patches(id: &RepoId, err: radicle::profile::Error) -> Self {
        Self::LoadPatches(*id, Box::new(err))
    }

    pub(crate) fn list_cache(id: &RepoId, err: radicle::patch::cache::Error) -> Self {
        Self::ListCache(*id, Box::new(err))
    }

    pub(crate) fn cache_list_item(id: &RepoId, err: radicle::patch::cache::Error) -> Self {
        Self::CacheListItem(*id, Box::new(err))
    }

    pub(crate) fn resolve_patch_id<S: Into<String>>(id: S, err: radicle::git::raw::Error) -> Self {
        Self::ResolvePatchId(id.into(), Box::new(err))
    }

    pub(crate) fn resolve_commit<S: Into<String>>(
        commit: S,
        repo: &RepoId,
        err: radicle::git::raw::Error,
    ) -> Self {
        Self::ResolveCommit(commit.into(), *repo, Box::new(err))
    }

    pub(crate) fn get_patch(
        repo: &RepoId,
        patch: &PatchId,
        err: radicle::patch::cache::Error,
    ) -> Self {
        Self::GetPatch(*repo, *patch, Box::new(err))
    }

    pub(crate) fn no_such_patch(repo: &RepoId, patch: &PatchId) -> Self {
        Self::NoSuchPatch(*repo, *patch)
    }

    pub(crate) fn no_repository_with_name<S: Into<String>>(name: S) -> Self {
        Self::NoRepositoryWithName(name.into())
    }

    pub(crate) fn name_is_not_unique<S: Into<String>>(name: S) -> Self {
        Self::NameIsNotUnique(name.into())
    }
}
