#![allow(clippy::result_large_err)]
use std::convert::TryInto;

use crate::{bstr::BStr, config, remote, remote::find, Remote};

impl crate::Repository {
    /// Create a new remote available at the given `url`.
    ///
    /// It's configured to fetch included tags by default, similar to git.
    /// See [`with_fetch_tags(…)`][Remote::with_fetch_tags()] for a way to change it.
    pub fn remote_at<Url, E>(&self, url: Url) -> Result<Remote<'_>, remote::init::Error>
    where
        Url: TryInto<gix_url::Url, Error = E>,
        gix_url::parse::Error: From<E>,
    {
        Remote::from_fetch_url(url, true, self)
    }

    /// Create a new remote available at the given `url` similarly to [`remote_at()`][crate::Repository::remote_at()],
    /// but don't rewrite the url according to rewrite rules.
    /// This eliminates a failure mode in case the rewritten URL is faulty, allowing to selectively [apply rewrite
    /// rules][Remote::rewrite_urls()] later and do so non-destructively.
    pub fn remote_at_without_url_rewrite<Url, E>(&self, url: Url) -> Result<Remote<'_>, remote::init::Error>
    where
        Url: TryInto<gix_url::Url, Error = E>,
        gix_url::parse::Error: From<E>,
    {
        Remote::from_fetch_url(url, false, self)
    }

    /// Find the configured remote with the given `name_or_url` or report an error,
    /// similar to [`try_find_remote(…)`][Self::try_find_remote()].
    ///
    /// Note that we will obtain remotes only if we deem them [trustworthy][crate::open::Options::filter_config_section()].
    pub fn find_remote<'a>(&self, name_or_url: impl Into<&'a BStr>) -> Result<Remote<'_>, find::existing::Error> {
        let name_or_url = name_or_url.into();
        Ok(self
            .try_find_remote(name_or_url)
            .ok_or_else(|| find::existing::Error::NotFound {
                name: name_or_url.into(),
            })??)
    }

    /// Find the default remote as configured, or `None` if no such configuration could be found.
    ///
    /// See [`remote_default_name()`](Self::remote_default_name()) for more information on the `direction` parameter.
    pub fn find_default_remote(
        &self,
        direction: remote::Direction,
    ) -> Option<Result<Remote<'_>, find::existing::Error>> {
        self.remote_default_name(direction)
            .map(|name| self.find_remote(name.as_ref()))
    }

    /// Find the configured remote with the given `name_or_url` or return `None` if it doesn't exist,
    /// for the purpose of fetching or pushing data.
    ///
    /// There are various error kinds related to partial information or incorrectly formatted URLs or ref-specs.
    /// Also note that the created `Remote` may have neither fetch nor push ref-specs set at all.
    ///
    /// Note that ref-specs are de-duplicated right away which may change their order. This doesn't affect matching in any way
    /// as negations/excludes are applied after includes.
    ///
    /// We will only include information if we deem it [trustworthy][crate::open::Options::filter_config_section()].
    pub fn try_find_remote<'a>(&self, name_or_url: impl Into<&'a BStr>) -> Option<Result<Remote<'_>, find::Error>> {
        self.try_find_remote_inner(name_or_url.into(), true)
    }

    /// This method emulate what `git fetch <remote>` does in order to obtain a remote to fetch from.
    ///
    /// As such, with `name_or_url` being `Some`, it will:
    ///
    /// * use `name_or_url` verbatim if it is a URL, creating a new remote in memory as needed.
    /// * find the named remote if `name_or_url` is a remote name
    ///
    /// If `name_or_url` is `None`:
    ///
    /// * use the current `HEAD` branch to find a configured remote
    /// * fall back to either a generally configured remote or the only configured remote.
    ///
    /// Fail if no remote could be found despite all of the above.
    pub fn find_fetch_remote(&self, name_or_url: Option<&BStr>) -> Result<Remote<'_>, find::for_fetch::Error> {
        Ok(match name_or_url {
            Some(name) => match self.try_find_remote(name).and_then(Result::ok) {
                Some(remote) => remote,
                None => self.remote_at(gix_url::parse(name)?)?,
            },
            None => self
                .head()?
                .into_remote(remote::Direction::Fetch)
                .transpose()?
                .map(Ok)
                .or_else(|| self.find_default_remote(remote::Direction::Fetch))
                .ok_or_else(|| find::for_fetch::Error::ExactlyOneRemoteNotAvailable)??,
        })
    }

    /// Similar to [`try_find_remote()`][Self::try_find_remote()], but removes a failure mode if rewritten URLs turn out to be invalid
    /// as it skips rewriting them.
    /// Use this in conjunction with [`Remote::rewrite_urls()`] to non-destructively apply the rules and keep the failed urls unchanged.
    pub fn try_find_remote_without_url_rewrite<'a>(
        &self,
        name_or_url: impl Into<&'a BStr>,
    ) -> Option<Result<Remote<'_>, find::Error>> {
        self.try_find_remote_inner(name_or_url.into(), false)
    }

    fn try_find_remote_inner<'a>(
        &self,
        name_or_url: impl Into<&'a BStr>,
        rewrite_urls: bool,
    ) -> Option<Result<Remote<'_>, find::Error>> {
        fn config_spec<T: config::tree::keys::Validate>(
            specs: Vec<std::borrow::Cow<'_, BStr>>,
            name_or_url: &BStr,
            key: &'static config::tree::keys::Any<T>,
            op: gix_refspec::parse::Operation,
        ) -> Result<Vec<gix_refspec::RefSpec>, find::Error> {
            let kind = key.name;
            specs
                .into_iter()
                .map(|spec| {
                    key.try_into_refspec(spec, op).map_err(|err| find::Error::RefSpec {
                        remote_name: name_or_url.into(),
                        kind,
                        source: err,
                    })
                })
                .collect::<Result<Vec<_>, _>>()
                .map(|mut specs| {
                    specs.sort();
                    specs.dedup();
                    specs
                })
        }

        let mut filter = self.filter_config_section();
        let name_or_url = name_or_url.into();
        let mut config_url = |key: &'static config::tree::keys::Url, kind: &'static str| {
            self.config
                .resolved
                .string_filter("remote", Some(name_or_url), key.name, &mut filter)
                .map(|url| {
                    key.try_into_url(url).map_err(|err| find::Error::Url {
                        kind,
                        remote_name: name_or_url.into(),
                        source: err,
                    })
                })
        };
        let url = config_url(&config::tree::Remote::URL, "fetch");
        let push_url = config_url(&config::tree::Remote::PUSH_URL, "push");
        let config = &self.config.resolved;

        let fetch_specs = config
            .strings_filter("remote", Some(name_or_url), "fetch", &mut filter)
            .map(|specs| {
                config_spec(
                    specs,
                    name_or_url,
                    &config::tree::Remote::FETCH,
                    gix_refspec::parse::Operation::Fetch,
                )
            });
        let push_specs = config
            .strings_filter("remote", Some(name_or_url), "push", &mut filter)
            .map(|specs| {
                config_spec(
                    specs,
                    name_or_url,
                    &config::tree::Remote::PUSH,
                    gix_refspec::parse::Operation::Push,
                )
            });
        let fetch_tags = config
            .string_filter("remote", Some(name_or_url), "tagOpt", &mut filter)
            .map(|value| {
                config::tree::Remote::TAG_OPT
                    .try_into_tag_opt(value)
                    .map_err(Into::into)
            });
        let fetch_tags = match fetch_tags {
            Some(Ok(v)) => v,
            Some(Err(err)) => return Some(Err(err)),
            None => Default::default(),
        };

        match (url, fetch_specs, push_url, push_specs) {
            (None, None, None, None) => None,
            (None, _, None, _) => Some(Err(find::Error::UrlMissing)),
            (url, fetch_specs, push_url, push_specs) => {
                let url = match url {
                    Some(Ok(v)) => Some(v),
                    Some(Err(err)) => return Some(Err(err)),
                    None => None,
                };
                let push_url = match push_url {
                    Some(Ok(v)) => Some(v),
                    Some(Err(err)) => return Some(Err(err)),
                    None => None,
                };
                let fetch_specs = match fetch_specs {
                    Some(Ok(v)) => v,
                    Some(Err(err)) => return Some(Err(err)),
                    None => Vec::new(),
                };
                let push_specs = match push_specs {
                    Some(Ok(v)) => v,
                    Some(Err(err)) => return Some(Err(err)),
                    None => Vec::new(),
                };

                Some(
                    Remote::from_preparsed_config(
                        Some(name_or_url.to_owned()),
                        url,
                        push_url,
                        fetch_specs,
                        push_specs,
                        rewrite_urls,
                        fetch_tags,
                        self,
                    )
                    .map_err(Into::into),
                )
            }
        }
    }
}
