use std::{
    convert::TryFrom,
    str::{self, FromStr},
};

use git_ext::ref_format::{component, lit, Component, Qualified, RefStr, RefString};

use crate::refs::refstr_join;

/// A `Branch` represents any git branch. It can be [`Local`] or [`Remote`].
///
/// Note that if a `Branch` is created from a [`git2::Reference`] then
/// any `refs/namespaces` will be stripped.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Branch {
    Local(Local),
    Remote(Remote),
}

impl Branch {
    /// Construct a [`Local`] branch.
    pub fn local<R>(name: R) -> Self
    where
        R: AsRef<RefStr>,
    {
        Self::Local(Local::new(name))
    }

    /// Construct a [`Remote`] branch.
    /// The `remote` is the remote name of the reference name while
    /// the `name` is the suffix, i.e. `refs/remotes/<remote>/<name>`.
    pub fn remote<R>(remote: Component<'_>, name: R) -> Self
    where
        R: AsRef<RefStr>,
    {
        Self::Remote(Remote::new(remote, name))
    }

    /// Return the short `Branch` refname,
    /// e.g. `fix/ref-format`.
    pub fn short_name(&self) -> &RefString {
        match self {
            Branch::Local(local) => local.short_name(),
            Branch::Remote(remote) => remote.short_name(),
        }
    }

    /// Give back the fully qualified `Branch` refname,
    /// e.g. `refs/remotes/origin/fix/ref-format`,
    /// `refs/heads/fix/ref-format`.
    pub fn refname(&self) -> Qualified {
        match self {
            Branch::Local(local) => local.refname(),
            Branch::Remote(remote) => remote.refname(),
        }
    }
}

impl TryFrom<&git2::Reference<'_>> for Branch {
    type Error = error::Branch;

    fn try_from(reference: &git2::Reference<'_>) -> Result<Self, Self::Error> {
        let name = str::from_utf8(reference.name_bytes())?;
        Self::from_str(name)
    }
}

impl TryFrom<&str> for Branch {
    type Error = error::Branch;

    fn try_from(name: &str) -> Result<Self, Self::Error> {
        Self::from_str(name)
    }
}

impl FromStr for Branch {
    type Err = error::Branch;

    fn from_str(name: &str) -> Result<Self, Self::Err> {
        let name = RefStr::try_from_str(name)?;
        let name = match name.to_namespaced() {
            None => name
                .qualified()
                .ok_or_else(|| error::Branch::NotQualified(name.to_string()))?,
            Some(name) => name.strip_namespace_recursive(),
        };

        let (_ref, category, c, cs) = name.non_empty_components();

        if category == component::HEADS {
            Ok(Self::Local(Local::new(refstr_join(c, cs))))
        } else if category == component::REMOTES {
            Ok(Self::Remote(Remote::new(c, cs.collect::<RefString>())))
        } else {
            Err(error::Branch::InvalidName(name.into()))
        }
    }
}

/// A `Local` represents a local branch, i.e. it is a reference under
/// `refs/heads`.
///
/// Note that if a `Local` is created from a [`git2::Reference`] then
/// any `refs/namespaces` will be stripped.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Local {
    name: RefString,
}

impl Local {
    /// Construct a new `Local` with the given `name`.
    ///
    /// If the name is qualified with `refs/heads`, this will be
    /// shortened to the suffix. To get the `Qualified` name again,
    /// use [`Local::refname`].
    pub(crate) fn new<R>(name: R) -> Self
    where
        R: AsRef<RefStr>,
    {
        match name.as_ref().qualified() {
            None => Self {
                name: name.as_ref().to_ref_string(),
            },
            Some(qualified) => {
                let (_refs, heads, c, cs) = qualified.non_empty_components();
                if heads == component::HEADS {
                    Self {
                        name: refstr_join(c, cs),
                    }
                } else {
                    Self {
                        name: name.as_ref().to_ref_string(),
                    }
                }
            },
        }
    }

    /// Return the short `Local` refname,
    /// e.g. `fix/ref-format`.
    pub fn short_name(&self) -> &RefString {
        &self.name
    }

    /// Return the fully qualified `Local` refname,
    /// e.g. `refs/heads/fix/ref-format`.
    pub fn refname(&self) -> Qualified {
        lit::refs_heads(&self.name).into()
    }
}

impl TryFrom<&git2::Reference<'_>> for Local {
    type Error = error::Local;

    fn try_from(reference: &git2::Reference) -> Result<Self, Self::Error> {
        let name = str::from_utf8(reference.name_bytes())?;
        Self::from_str(name)
    }
}

impl TryFrom<&str> for Local {
    type Error = error::Local;

    fn try_from(name: &str) -> Result<Self, Self::Error> {
        Self::from_str(name)
    }
}

impl FromStr for Local {
    type Err = error::Local;

    fn from_str(name: &str) -> Result<Self, Self::Err> {
        let name = RefStr::try_from_str(name)?;
        let name = match name.to_namespaced() {
            None => name
                .qualified()
                .ok_or_else(|| error::Local::NotQualified(name.to_string()))?,
            Some(name) => name.strip_namespace_recursive(),
        };

        let (_ref, heads, c, cs) = name.non_empty_components();
        if heads == component::HEADS {
            Ok(Self::new(refstr_join(c, cs)))
        } else {
            Err(error::Local::NotHeads(name.into()))
        }
    }
}

/// A `Remote` represents a remote branch, i.e. it is a reference under
/// `refs/remotes`.
///
/// Note that if a `Remote` is created from a [`git2::Reference`] then
/// any `refs/namespaces` will be stripped.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Remote {
    remote: RefString,
    name: RefString,
}

impl Remote {
    /// Construct a new `Remote` with the given `name` and `remote`.
    ///
    /// ## Note
    /// `name` is expected to be in short form, i.e. not begin with
    /// `refs`.
    ///
    /// If you are creating a `Remote` with a name that begins with
    /// `refs/remotes`, use [`Remote::from_refs_remotes`] instead.
    ///
    /// To get the `Qualified` name, use [`Remote::refname`].
    pub(crate) fn new<R>(remote: Component, name: R) -> Self
    where
        R: AsRef<RefStr>,
    {
        Self {
            name: name.as_ref().to_ref_string(),
            remote: remote.to_ref_string(),
        }
    }

    /// Parse the `name` from the form `refs/remotes/<remote>/<rest>`.
    ///
    /// If the `name` is not of this form, then `None` is returned.
    pub fn from_refs_remotes<R>(name: R) -> Option<Self>
    where
        R: AsRef<RefStr>,
    {
        let qualified = name.as_ref().qualified()?;
        let (_refs, remotes, remote, cs) = qualified.non_empty_components();
        (remotes == component::REMOTES).then_some(Self {
            name: cs.collect(),
            remote: remote.to_ref_string(),
        })
    }

    /// Return the short `Remote` refname,
    /// e.g. `fix/ref-format`.
    pub fn short_name(&self) -> &RefString {
        &self.name
    }

    /// Return the remote of the `Remote`'s refname,
    /// e.g. `origin`.
    pub fn remote(&self) -> &RefString {
        &self.remote
    }

    /// Give back the fully qualified `Remote` refname,
    /// e.g. `refs/remotes/origin/fix/ref-format`.
    pub fn refname(&self) -> Qualified {
        lit::refs_remotes(self.remote.join(&self.name)).into()
    }
}

impl TryFrom<&git2::Reference<'_>> for Remote {
    type Error = error::Remote;

    fn try_from(reference: &git2::Reference) -> Result<Self, Self::Error> {
        let name = str::from_utf8(reference.name_bytes())?;
        Self::from_str(name)
    }
}

impl TryFrom<&str> for Remote {
    type Error = error::Remote;

    fn try_from(name: &str) -> Result<Self, Self::Error> {
        Self::from_str(name)
    }
}

impl FromStr for Remote {
    type Err = error::Remote;

    fn from_str(name: &str) -> Result<Self, Self::Err> {
        let name = RefStr::try_from_str(name)?;
        let name = match name.to_namespaced() {
            None => name
                .qualified()
                .ok_or_else(|| error::Remote::NotQualified(name.to_string()))?,
            Some(name) => name.strip_namespace_recursive(),
        };

        let (_ref, remotes, remote, cs) = name.non_empty_components();
        if remotes == component::REMOTES {
            Ok(Self::new(remote, cs.collect::<RefString>()))
        } else {
            Err(error::Remote::NotRemotes(name.into()))
        }
    }
}

pub mod error {
    use radicle_git_ext::ref_format::{self, RefString};
    use thiserror::Error;

    #[derive(Debug, Error)]
    pub enum Branch {
        #[error("the refname '{0}' did not begin with 'refs/heads' or 'refs/remotes'")]
        InvalidName(RefString),
        #[error("the refname '{0}' did not begin with 'refs/heads' or 'refs/remotes'")]
        NotQualified(String),
        #[error(transparent)]
        RefFormat(#[from] ref_format::Error),
        #[error(transparent)]
        Utf8(#[from] std::str::Utf8Error),
    }

    #[derive(Debug, Error)]
    pub enum Local {
        #[error("the refname '{0}' did not begin with 'refs/heads'")]
        NotHeads(RefString),
        #[error("the refname '{0}' did not begin with 'refs/heads'")]
        NotQualified(String),
        #[error(transparent)]
        RefFormat(#[from] ref_format::Error),
        #[error(transparent)]
        Utf8(#[from] std::str::Utf8Error),
    }

    #[derive(Debug, Error)]
    pub enum Remote {
        #[error("the refname '{0}' did not begin with 'refs/remotes'")]
        NotQualified(String),
        #[error("the refname '{0}' did not begin with 'refs/remotes'")]
        NotRemotes(RefString),
        #[error(transparent)]
        RefFormat(#[from] ref_format::Error),
        #[error(transparent)]
        Utf8(#[from] std::str::Utf8Error),
    }
}
