use clap::builder::TypedValueParser;
use thiserror::Error;

use radicle::node::policy::Scope;
use radicle::prelude::{Did, NodeId, RepoId};

#[derive(thiserror::Error, Debug)]
pub(crate) enum Error {
    /// An error with a hint.
    #[error("{err}")]
    WithHint { err: anyhow::Error, hint: String },
}

impl Error {
    pub fn with_hint<E, H>(err: E, hint: H) -> Self
    where
        E: Into<anyhow::Error>,
        H: ToString,
    {
        Self::WithHint {
            err: err.into(),
            hint: hint.to_string(),
        }
    }
}

/// Targets used in the `block` and `unblock` commands
#[derive(Clone, Debug)]
pub(crate) enum BlockTarget {
    Node(NodeId),
    Repo(RepoId),
}

#[derive(Debug, Error)]
#[error(
    "invalid repository or node specified (RID parsing failed with: '{repo}', NID parsing failed with: '{node}'))"
)]
pub(crate) struct BlockTargetParseError {
    repo: radicle::identity::IdError,
    node: radicle::crypto::PublicKeyError,
}

impl std::str::FromStr for BlockTarget {
    type Err = BlockTargetParseError;

    fn from_str(val: &str) -> Result<Self, Self::Err> {
        val.parse::<RepoId>()
            .map(BlockTarget::Repo)
            .or_else(|repo| {
                val.parse::<NodeId>()
                    .map(BlockTarget::Node)
                    .map_err(|node| BlockTargetParseError { repo, node })
            })
    }
}

impl std::fmt::Display for BlockTarget {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Node(nid) => nid.fmt(f),
            Self::Repo(rid) => rid.fmt(f),
        }
    }
}

#[derive(Debug, thiserror::Error)]
#[error(
    "invalid Node ID specified (Node ID parsing failed with: '{nid}', DID parsing failed with: '{did}'))"
)]
pub(crate) struct NodeIdParseError {
    did: radicle::identity::did::DidError,
    nid: radicle::crypto::PublicKeyError,
}

pub(crate) fn parse_nid(value: &str) -> Result<NodeId, NodeIdParseError> {
    value.parse::<Did>().map(NodeId::from).or_else(|did| {
        value
            .parse::<NodeId>()
            .map_err(|nid| NodeIdParseError { nid, did })
    })
}

#[derive(Clone, Debug)]
pub(crate) struct ScopeParser;

impl TypedValueParser for ScopeParser {
    type Value = Scope;

    fn parse_ref(
        &self,
        cmd: &clap::Command,
        arg: Option<&clap::Arg>,
        value: &std::ffi::OsStr,
    ) -> Result<Self::Value, clap::Error> {
        <Scope as std::str::FromStr>::from_str.parse_ref(cmd, arg, value)
    }

    fn possible_values(
        &self,
    ) -> Option<Box<dyn Iterator<Item = clap::builder::PossibleValue> + '_>> {
        use clap::builder::PossibleValue;
        Some(Box::new(
            [PossibleValue::new("all"), PossibleValue::new("followed")].into_iter(),
        ))
    }
}

/// A wrapper around [`radicle::rad::cwd`] that should be preferred over direct
/// calls in this crate.
///
/// If the given [`Option<RepoId>`] is `Some`, it is returned as is.
/// No attempt is made to detect whether the current working directory is
/// a Radicle repository (therefore it is also not checked whether the given
/// [`RepoId`] matches the [`RepoId`] associated with the current working
/// directory), and the returned repository is `None`.
///
/// Otherwise, i.e, if the given [`Option<RepoId>`] is `None`, an attempt is
/// made to detect the [`RepoId`] associated with the current working directory,
/// by calling [`radicle::rad::cwd`]. If this detection fails, an error with
/// context is returned.
pub(crate) fn rid_or_cwd(
    rid: Option<RepoId>,
) -> anyhow::Result<(Option<radicle::git::raw::Repository>, RepoId)> {
    match rid {
        Some(rid) => Ok((None, rid)),
        None => {
            use anyhow::Context as _;

            let (repository, rid) =
                radicle::rad::cwd().context("Current directory is not a Radicle repository")?;
            Ok((Some(repository), rid))
        }
    }
}

#[cfg(test)]
mod test {
    use std::str::FromStr;

    use super::BlockTarget;
    use super::BlockTargetParseError;

    #[test]
    fn should_parse_nid() {
        let target = BlockTarget::from_str("z6MkiswaKJ85vafhffCGBu2gdBsYoDAyHVBWRxL3j297fwS9");
        assert!(target.is_ok())
    }

    #[test]
    fn should_parse_rid() {
        let target = BlockTarget::from_str("rad:z3Tr6bC7ctEg2EHmLvknUr29mEDLH");
        assert!(target.is_ok())
    }

    #[test]
    fn should_not_parse() {
        let err = BlockTarget::from_str("bee").unwrap_err();
        assert!(matches!(err, BlockTargetParseError { .. }));
    }
}
