use bstr::{BString, ByteSlice};
use either::Either;
use radicle::crypto::PublicKey;
use radicle::git::{
    self, Oid,
    fmt::{Component, Namespaced, Qualified},
};
use thiserror::Error;

pub use radicle::git::refs::storage::Special;

use crate::git::refs::{Policy, Update};

pub(crate) use radicle::git::refs::storage::IDENTITY_BRANCH as REFS_RAD_ID;

#[derive(Debug, Error)]
#[non_exhaustive]
pub enum Error {
    #[error("non-namespaced ref name '{0}' is not 'refs/rad/id'")]
    NotCanonicalRadID(Qualified<'static>),
    #[error("invalid remote peer id")]
    PublicKey(#[from] radicle::crypto::PublicKeyError),
    #[error(transparent)]
    Ref(#[from] radicle::git::RefError),
    #[error(transparent)]
    Utf8(#[from] bstr::Utf8Error),
}

pub(crate) fn unpack_ref<'a>(
    r: gix_protocol::handshake::Ref,
) -> Result<(ReceivedRefname<'a>, Oid), Error> {
    use gix_protocol::handshake::Ref;

    match r {
        Ref::Peeled {
            full_ref_name,
            object,
            ..
        }
        | Ref::Direct {
            full_ref_name,
            object,
        }
        | Ref::Symbolic {
            full_ref_name,
            object,
            ..
        } => ReceivedRefname::try_from(full_ref_name).map(|name| (name, object.into())),
        Ref::Unborn { full_ref_name, .. } => {
            unreachable!("BUG: unborn ref {}", full_ref_name)
        }
    }
}

/// A reference name received during an exchange with another peer. The
/// expected references are either namespaced references in the form
/// of [`RemoteRef`] or the canonical `rad/id` reference.
#[derive(Debug)]
pub(crate) enum ReceivedRefname<'a> {
    /// A reference name under a `remote` namespace.
    ///
    /// # Examples
    ///
    ///   * `refs/namespaces/<remote>/refs/rad/id`
    ///   * `refs/namespaces/<remote>/refs/rad/sigrefs`
    ///   * `refs/namespaces/<remote>/refs/heads/main`
    ///   * `refs/namespaces/<remote>/refs/cobs/issue.rad.xyz`
    Namespaced {
        /// The namespace of the remote.
        remote: PublicKey,
        /// The reference is expected to either be a [`Special`] reference
        /// or a generic reference name.
        suffix: Either<Special, Qualified<'a>>,
    },
    /// The canonical `refs/rad/id` reference
    RadId,
}

impl<'a> ReceivedRefname<'a> {
    pub fn remote(remote: PublicKey, suffix: Qualified<'a>) -> Self {
        Self::Namespaced {
            remote,
            suffix: Either::Right(suffix),
        }
    }

    pub fn to_qualified<'b>(&self) -> Qualified<'b> {
        match &self {
            Self::Namespaced { remote, suffix } => match suffix {
                Either::Left(s) => Qualified::from(*s)
                    .with_namespace(Component::from(remote))
                    .into(),
                Either::Right(name) => {
                    Qualified::from(name.with_namespace(Component::from(remote))).to_owned()
                }
            },
            Self::RadId => REFS_RAD_ID.clone(),
        }
    }

    pub fn to_namespaced<'b>(&self) -> Option<Namespaced<'b>> {
        match self {
            Self::Namespaced { remote, suffix } => Some(match suffix {
                Either::Left(special) => special.namespaced(remote),
                Either::Right(refname) => {
                    refname.with_namespace(Component::from(remote)).to_owned()
                }
            }),
            Self::RadId => None,
        }
    }
}

impl TryFrom<BString> for ReceivedRefname<'_> {
    type Error = Error;

    fn try_from(value: BString) -> Result<Self, Self::Error> {
        match git::parse_ref::<PublicKey>(value.to_str()?)? {
            (None, name) => (name == *REFS_RAD_ID)
                .then_some(ReceivedRefname::RadId)
                .ok_or_else(|| Error::NotCanonicalRadID(name.to_owned())),
            (Some(remote), name) => Ok(ReceivedRefname::Namespaced {
                remote,
                suffix: match Special::from_qualified(&name) {
                    None => Either::Right(name.to_owned()),
                    Some(special) => Either::Left(special),
                },
            }),
        }
    }
}

/// A reference name and the associated tip received during an
/// exchange with another peer.
#[derive(Debug)]
pub(crate) struct ReceivedRef {
    pub tip: Oid,
    pub name: ReceivedRefname<'static>,
}

impl ReceivedRef {
    pub fn new(tip: Oid, name: ReceivedRefname<'static>) -> Self {
        Self { tip, name }
    }

    pub fn to_qualified(&self) -> Qualified<'static> {
        self.name.to_qualified()
    }

    pub fn as_special_ref_update<F>(&self, is_delegate: F) -> Option<(PublicKey, Update<'static>)>
    where
        F: Fn(&PublicKey) -> bool,
    {
        match &self.name {
            ReceivedRefname::RadId => None,
            ReceivedRefname::Namespaced { remote, suffix } => {
                special_update(remote, suffix, self.tip, is_delegate).map(|up| (*remote, up))
            }
        }
    }
}

pub(crate) fn special_update<F>(
    remote: &PublicKey,
    suffix: &Either<Special, Qualified>,
    tip: Oid,
    is_delegate: F,
) -> Option<Update<'static>>
where
    F: Fn(&PublicKey) -> bool,
{
    suffix.as_ref().left().map(|special| Update::Direct {
        name: special.namespaced(remote).to_owned(),
        target: tip,
        // N.b. reject any updates if the remote is not a delegate,
        // since this is not fatal.
        no_ff: if is_delegate(remote) {
            Policy::Abort
        } else {
            Policy::Reject
        },
    })
}
