#[cfg(any(feature = "blocking-client", feature = "async-client"))]
mod error {
    use crate::handshake::refs::parse;

    /// The error returned by invoking a [`super::function::LsRefsCommand`].
    #[derive(Debug, thiserror::Error)]
    #[expect(missing_docs)]
    pub enum Error {
        #[error(transparent)]
        Io(#[from] std::io::Error),
        #[error(transparent)]
        Transport(#[from] gix_transport::client::Error),
        #[error(transparent)]
        Parse(#[from] parse::Error),
        #[error(transparent)]
        ArgumentValidation(#[from] crate::command::validate_argument_prefixes::Error),
    }

    impl gix_transport::IsSpuriousError for Error {
        fn is_spurious(&self) -> bool {
            match self {
                Error::Io(err) => err.is_spurious(),
                Error::Transport(err) => err.is_spurious(),
                _ => false,
            }
        }
    }
}
#[cfg(any(feature = "blocking-client", feature = "async-client"))]
pub use error::Error;

#[cfg(any(feature = "blocking-client", feature = "async-client"))]
pub use self::function::RefPrefixes;

#[cfg(any(feature = "blocking-client", feature = "async-client"))]
pub(crate) mod function {
    use std::collections::HashSet;

    use bstr::{BString, ByteVec};
    use gix_features::progress::Progress;
    use gix_transport::client::Capabilities;

    use super::Error;
    #[cfg(feature = "async-client")]
    use crate::transport::client::async_io::TransportV2Ext as _;
    #[cfg(feature = "blocking-client")]
    use crate::transport::client::blocking_io::TransportV2Ext as _;
    use crate::{Command, handshake::Ref};

    /// [`RefPrefixes`] are the set of prefixes that are sent to the server for
    /// filtering purposes.
    ///
    /// These are communicated by sending zero or more `ref-prefix` values, and
    /// are documented in [gitprotocol-v2.adoc#ls-refs].
    ///
    /// These prefixes can be constructed from a set of [`RefSpec`]'s using
    /// [`RefPrefixes::from_refspecs`].
    ///
    /// Alternatively, they can be constructed using [`RefPrefixes::new`] and
    /// using [`RefPrefixes::extend`] to add new prefixes.
    ///
    /// [`RefSpec`]: gix_refspec::RefSpec
    /// [gitprotocol-v2.adoc#ls-refs]: https://github.com/git/git/blob/master/Documentation/gitprotocol-v2.adoc#ls-refs
    pub struct RefPrefixes {
        prefixes: Vec<BString>,
    }

    impl Default for RefPrefixes {
        fn default() -> Self {
            Self::new()
        }
    }

    impl RefPrefixes {
        /// Create an empty set of [`RefPrefixes`].
        pub fn new() -> RefPrefixes {
            RefPrefixes { prefixes: Vec::new() }
        }

        /// Convert a series of [`RefSpec`]'s into a set of [`RefPrefixes`].
        ///
        /// It attempts to expand each [`RefSpec`] into prefix references, e.g.
        /// `refs/heads/`, `refs/remotes/`, `refs/namespaces/foo/`, etc.
        ///
        /// Inputs that aren't fully qualified refs, like `HEAD` or `main`, are
        /// expanded in the same DWIM-style way that Git uses for `ref-prefix`
        /// generation, yielding prefixes like `HEAD`, `refs/heads/main`, and
        /// other rev-parse candidates.
        ///
        /// [`RefSpec`]: gix_refspec::RefSpec
        pub fn from_refspecs<'a>(refspecs: impl IntoIterator<Item = &'a gix_refspec::RefSpec>) -> Self {
            let mut seen = HashSet::new();
            let mut prefixes = Self::new();
            for spec in refspecs.into_iter() {
                let spec = spec.to_ref();
                if seen.insert(spec.instruction()) {
                    let mut out = Vec::with_capacity(1);
                    spec.expand_prefixes(&mut out);
                    prefixes.extend(out);
                }
            }
            prefixes
        }

        fn into_args(self) -> impl Iterator<Item = BString> {
            self.prefixes.into_iter().map(|mut prefix| {
                prefix.insert_str(0, "ref-prefix ");
                prefix
            })
        }
    }

    impl Extend<BString> for RefPrefixes {
        fn extend<T: IntoIterator<Item = BString>>(&mut self, iter: T) {
            for prefix in iter {
                if !self.prefixes.iter().any(|existing| existing == &prefix) {
                    self.prefixes.push(prefix);
                }
            }
        }
    }

    /// A command to list references from a remote Git repository.
    ///
    /// Its invocation uses the same implementation with either blocking or asynchronous I/O.
    pub struct LsRefsCommand<'a> {
        pub(crate) capabilities: &'a Capabilities,
        features: Vec<crate::command::Feature>,
        arguments: Vec<BString>,
    }

    macro_rules! invoke {
        ($name:ident, $bisync:path, $transport:path, $from_v2_refs:path, $mode:literal) => {
            /// Invoke a ls-refs V2 command on `transport`.
            ///
            /// `progress` is used to provide feedback.
            /// If `trace` is `true`, all packetlines received or sent will be passed to the facilities of the `gix-trace` crate.
            #[$bisync]
            pub async fn $name(
                self,
                mut transport: impl $transport,
                progress: &mut impl Progress,
                trace: bool,
            ) -> Result<Vec<Ref>, Error> {
                let _span = gix_features::trace::detail!("gix_protocol::LsRefsCommand::invoke()", mode = $mode);
                Command::LsRefs.validate_argument_prefixes(
                    gix_transport::Protocol::V2,
                    self.capabilities,
                    &self.arguments,
                    &self.features,
                )?;

                progress.step();
                progress.set_name("list refs".into());
                let mut remote_refs = transport
                    .invoke(
                        Command::LsRefs.as_str(),
                        self.features.into_iter(),
                        if self.arguments.is_empty() {
                            None
                        } else {
                            Some(self.arguments.into_iter())
                        },
                        trace,
                    )
                    .await?;
                Ok($from_v2_refs(&mut remote_refs).await?)
            }
        };
    }

    impl<'a> LsRefsCommand<'a> {
        /// Build a command to list refs from the given server `capabilities`,
        /// using `agent` information to identify ourselves.
        ///
        /// Use [`crate::ls_refs::RefPrefixes::from_refspecs()`] to construct `ref_prefixes`
        /// from refspecs, or [`crate::ls_refs::RefPrefixes::new()`] to build them manually.
        pub fn new(
            ref_prefixes: Option<RefPrefixes>,
            capabilities: &'a Capabilities,
            agent: crate::command::Feature,
        ) -> Self {
            let ls_refs = Command::LsRefs;
            let mut features = ls_refs.default_features(gix_transport::Protocol::V2, capabilities);
            features.push(agent);
            let mut arguments = ls_refs.initial_v2_arguments(&features);
            if capabilities
                .capability("ls-refs")
                .and_then(|cap| cap.supports("unborn"))
                .unwrap_or_default()
            {
                arguments.push("unborn".into());
            }

            if let Some(prefixes) = ref_prefixes {
                arguments.extend(prefixes.into_args());
            }

            Self {
                capabilities,
                features,
                arguments,
            }
        }

        #[cfg(feature = "async-client")]
        invoke!(
            invoke_async,
            ::bisync::asynchronous::bisync,
            crate::transport::client::async_io::Transport,
            crate::handshake::refs::async_io::from_v2_refs,
            "async"
        );

        #[cfg(feature = "blocking-client")]
        invoke!(
            invoke_blocking,
            ::bisync::synchronous::bisync,
            crate::transport::client::blocking_io::Transport,
            crate::handshake::refs::blocking_io::from_v2_refs,
            "blocking"
        );
    }

    #[cfg(test)]
    mod ref_prefixes {
        use bstr::{BString, ByteSlice};

        use super::RefPrefixes;

        #[test]
        fn extend_preserves_first_seen_order_and_deduplicates_prefixes() {
            let mut prefixes = RefPrefixes::new();
            prefixes.extend(
                [
                    "refs/tags",
                    "HEAD",
                    "main",
                    "refs/heads/main",
                    "refs/tags",
                    "HEAD",
                    "refs/heads/feature",
                    "refs/heads/main",
                ]
                .into_iter()
                .map(|prefix| prefix.as_bytes().as_bstr().to_owned()),
            );

            assert_eq!(
                prefixes.into_args().collect::<Vec<_>>(),
                [
                    "ref-prefix refs/tags",
                    "ref-prefix HEAD",
                    "ref-prefix main",
                    "ref-prefix refs/heads/main",
                    "ref-prefix refs/heads/feature"
                ]
                .into_iter()
                .map(BString::from)
                .collect::<Vec<_>>()
            );
        }

        #[test]
        fn from_refspecs_keeps_exact_refs_and_dwim_expansions() {
            let specs = [
                gix_refspec::parse("HEAD".into(), gix_refspec::parse::Operation::Fetch)
                    .expect("valid")
                    .to_owned(),
                gix_refspec::parse("dwim".into(), gix_refspec::parse::Operation::Fetch)
                    .expect("valid")
                    .to_owned(),
                gix_refspec::parse(
                    "refs/tags/prefix*:refs/tags/prefix*".into(),
                    gix_refspec::parse::Operation::Fetch,
                )
                .expect("valid")
                .to_owned(),
                gix_refspec::parse("refs/heads/main".into(), gix_refspec::parse::Operation::Fetch)
                    .expect("valid")
                    .to_owned(),
            ];

            let prefixes = RefPrefixes::from_refspecs(&specs);

            assert_eq!(
                prefixes.into_args().collect::<Vec<_>>(),
                [
                    "ref-prefix HEAD",
                    "ref-prefix dwim",
                    "ref-prefix refs/dwim",
                    "ref-prefix refs/tags/dwim",
                    "ref-prefix refs/heads/dwim",
                    "ref-prefix refs/remotes/dwim",
                    "ref-prefix refs/remotes/dwim/HEAD",
                    "ref-prefix refs/tags/prefix",
                    "ref-prefix refs/heads/main",
                ]
                .into_iter()
                .map(BString::from)
                .collect::<Vec<_>>()
            );
        }
    }
}
