/// Print the effective URL or URLs of the selected remote.
///
/// Without an explicit remote, selection follows the fetch or push configuration for the current branch according to `direction`.
pub fn url(
    repo: gix::Repository,
    name: Option<&str>,
    direction: gix::remote::Direction,
    all: bool,
    mut out: impl std::io::Write,
) -> anyhow::Result<()> {
    let remote = match (name, direction) {
        (Some(name), _) => repo.find_fetch_remote(Some(name.into()))?,
        (None, gix::remote::Direction::Fetch) => repo.find_fetch_remote(None)?,
        (None, gix::remote::Direction::Push) => repo
            .head()?
            .into_remote(gix::remote::Direction::Push)
            .or_else(|| repo.find_default_remote(gix::remote::Direction::Push))
            .transpose()?
            .ok_or_else(|| anyhow::anyhow!("Could not determine a remote for pushing"))?,
    };
    if all {
        let mut urls = remote.urls(direction).peekable();
        if urls.peek().is_none() {
            anyhow::bail!("The remote has no {} URL", direction.as_str());
        }
        for url in urls {
            out.write_all(&url.to_bstring())?;
            out.write_all(b"\n")?;
        }
    } else {
        let url = remote
            .url(direction)
            .ok_or_else(|| anyhow::anyhow!("The remote has no {} URL", direction.as_str()))?;
        out.write_all(&url.to_bstring())?;
        out.write_all(b"\n")?;
    }
    Ok(())
}

#[cfg(any(feature = "blocking-client", feature = "async-client"))]
mod refs_impl {
    use anyhow::bail;
    use gix::{
        protocol::handshake,
        refspec::{RefSpec, match_group::validate::Fix},
        remote::fetch::refmap::Source,
    };

    use super::by_name_or_url;
    use crate::OutputFormat;

    pub mod refs {
        use gix::bstr::BString;

        use crate::OutputFormat;

        pub const PROGRESS_RANGE: std::ops::RangeInclusive<u8> = 1..=2;

        pub enum Kind {
            Remote,
            Tracking {
                ref_specs: Vec<BString>,
                show_unmapped_remote_refs: bool,
            },
        }

        pub struct Options {
            pub format: OutputFormat,
            pub name_or_url: Option<String>,
            pub handshake_info: bool,
        }

        pub(crate) use super::{print, print_ref};
    }

    #[gix::protocol::bisync::bisync]
    pub async fn refs_fn(
        repo: gix::Repository,
        kind: refs::Kind,
        mut progress: impl gix::Progress,
        mut out: impl std::io::Write,
        err: impl std::io::Write,
        refs::Options {
            format,
            name_or_url,
            handshake_info,
        }: refs::Options,
    ) -> anyhow::Result<()> {
        use anyhow::Context;
        let mut remote = by_name_or_url(&repo, name_or_url.as_deref())?;
        let show_unmapped = if let refs::Kind::Tracking {
            ref_specs,
            show_unmapped_remote_refs,
        } = &kind
        {
            if format != OutputFormat::Human {
                bail!("JSON output isn't yet supported for listing ref-mappings.");
            }
            if !ref_specs.is_empty() {
                remote.replace_refspecs(ref_specs.iter(), gix::remote::Direction::Fetch)?;
                remote = remote.with_fetch_tags(gix::remote::fetch::Tags::None);
            }
            *show_unmapped_remote_refs
        } else {
            false
        };
        progress.info(format!(
            "Connecting to {:?}",
            remote
                .url(gix::remote::Direction::Fetch)
                .context("Remote didn't have a URL to connect to")?
                .to_bstring()
        ));
        let (map, handshake) = remote
            .connect(gix::remote::Direction::Fetch)
            .await?
            .ref_map(
                &mut progress,
                gix::remote::ref_map::Options {
                    prefix_from_spec_as_filter_on_remote: !matches!(kind, refs::Kind::Remote),
                    ..Default::default()
                },
            )
            .await?;

        if handshake_info {
            writeln!(out, "Handshake Information")?;
            writeln!(out, "\t{handshake:?}")?;
        }
        match kind {
            refs::Kind::Tracking { .. } => print_refmap(
                &repo,
                remote.refspecs(gix::remote::Direction::Fetch),
                map,
                show_unmapped,
                out,
                err,
            ),
            refs::Kind::Remote => {
                match format {
                    OutputFormat::Human => drop(print(out, &map.remote_refs)),
                    #[cfg(feature = "serde")]
                    OutputFormat::Json => serde_json::to_writer_pretty(
                        out,
                        &map.remote_refs.into_iter().map(JsonRef::from).collect::<Vec<_>>(),
                    )?,
                }
                Ok(())
            }
        }
    }

    pub(crate) fn print_refmap(
        repo: &gix::Repository,
        refspecs: &[RefSpec],
        mut map: gix::remote::fetch::RefMap,
        show_unmapped_remotes: bool,
        mut out: impl std::io::Write,
        mut err: impl std::io::Write,
    ) -> anyhow::Result<()> {
        let mut last_spec_index = gix::remote::fetch::refmap::SpecIndex::ExplicitInRemote(usize::MAX);
        map.mappings.sort_by_key(|m| m.spec_index);
        for mapping in &map.mappings {
            if mapping.spec_index != last_spec_index {
                last_spec_index = mapping.spec_index;
                let spec = mapping
                    .spec_index
                    .get(refspecs, &map.extra_refspecs)
                    .expect("refspecs here are the ones used for mapping");
                spec.to_ref().write_to(&mut out)?;
                let is_implicit = mapping.spec_index.implicit_index().is_some();
                if is_implicit {
                    write!(&mut out, " (implicit")?;
                    if spec.to_ref()
                        == gix::remote::fetch::Tags::Included
                            .to_refspec()
                            .expect("always yields refspec")
                    {
                        write!(&mut out, ", due to auto-tag")?;
                    }
                    write!(&mut out, ")")?;
                }
                writeln!(out)?;
            }

            write!(out, "\t")?;
            let target_id = match &mapping.remote {
                gix::remote::fetch::refmap::Source::ObjectId(id) => {
                    write!(out, "{id}")?;
                    id
                }
                gix::remote::fetch::refmap::Source::Ref(r) => print_ref(&mut out, r)?,
            };
            match &mapping.local {
                Some(local) => {
                    write!(out, " -> {local} ")?;
                    match repo.try_find_reference(local)? {
                        Some(tracking) => {
                            let msg = match tracking.try_id() {
                                Some(id) => {
                                    if id.as_ref() == target_id {
                                        "[up-to-date]"
                                    } else {
                                        "[changed]"
                                    }
                                }
                                None => "[skipped]",
                            };
                            writeln!(out, "{msg}")
                        }
                        None => writeln!(out, "[new]"),
                    }
                }
                None => writeln!(out, " (fetch only)"),
            }?;
        }
        if !map.fixes.is_empty() {
            writeln!(
                err,
                "The following destination refs were removed as they didn't start with 'ref/'"
            )?;
            map.fixes.sort_by(|l, r| match (l, r) {
                (
                    Fix::MappingWithPartialDestinationRemoved { spec: l, .. },
                    Fix::MappingWithPartialDestinationRemoved { spec: r, .. },
                ) => l.cmp(r),
            });
            let mut prev_spec = None;
            for fix in &map.fixes {
                match fix {
                    Fix::MappingWithPartialDestinationRemoved { name, spec } => {
                        if prev_spec.is_some_and(|prev_spec| prev_spec != spec) {
                            prev_spec = spec.into();
                            spec.to_ref().write_to(&mut err)?;
                            writeln!(err)?;
                        }
                        writeln!(err, "\t{name}")?;
                    }
                }
            }
        }
        if map.remote_refs.len() - map.mappings.len() != 0 {
            writeln!(
                err,
                "server sent {} tips, {} were filtered due to {} refspec(s).",
                map.remote_refs.len(),
                map.remote_refs.len() - map.mappings.len(),
                refspecs.len()
            )?;
            if show_unmapped_remotes {
                writeln!(&mut out, "\nFiltered: ")?;
                for remote_ref in map.remote_refs.iter().filter(|r| {
                    !map.mappings.iter().any(|m| match &m.remote {
                        Source::Ref(other) => other == *r,
                        Source::ObjectId(_) => false,
                    })
                }) {
                    print_ref(&mut out, remote_ref)?;
                    writeln!(&mut out)?;
                }
            }
        }
        if refspecs.is_empty() {
            bail!(
                "Without refspecs there is nothing to show here. Add refspecs as arguments or configure them in .git/config."
            )
        }
        Ok(())
    }

    #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
    pub enum JsonRef {
        Peeled {
            path: String,
            tag: String,
            object: String,
        },
        Direct {
            path: String,
            object: String,
        },
        Unborn {
            path: String,
            target: String,
        },
        Symbolic {
            path: String,
            #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
            tag: Option<String>,
            target: String,
            object: String,
        },
    }

    impl From<handshake::Ref> for JsonRef {
        fn from(value: handshake::Ref) -> Self {
            match value {
                handshake::Ref::Unborn { full_ref_name, target } => JsonRef::Unborn {
                    path: full_ref_name.to_string(),
                    target: target.to_string(),
                },
                handshake::Ref::Direct {
                    full_ref_name: path,
                    object,
                } => JsonRef::Direct {
                    path: path.to_string(),
                    object: object.to_string(),
                },
                handshake::Ref::Symbolic {
                    full_ref_name: path,
                    tag,
                    target,
                    object,
                } => JsonRef::Symbolic {
                    path: path.to_string(),
                    tag: tag.map(|t| t.to_string()),
                    target: target.to_string(),
                    object: object.to_string(),
                },
                handshake::Ref::Peeled {
                    full_ref_name: path,
                    tag,
                    object,
                } => JsonRef::Peeled {
                    path: path.to_string(),
                    tag: tag.to_string(),
                    object: object.to_string(),
                },
            }
        }
    }

    pub(crate) fn print_ref(mut out: impl std::io::Write, r: &handshake::Ref) -> std::io::Result<&gix::hash::oid> {
        match r {
            handshake::Ref::Direct {
                full_ref_name: path,
                object,
            } => write!(&mut out, "{object} {path}").map(|_| object.as_ref()),
            handshake::Ref::Peeled {
                full_ref_name: path,
                tag,
                object,
            } => write!(&mut out, "{tag} {path} object:{object}").map(|_| tag.as_ref()),
            handshake::Ref::Symbolic {
                full_ref_name: path,
                tag,
                target,
                object,
            } => match tag {
                Some(tag) => {
                    write!(&mut out, "{tag} {path} symref-target:{target} peeled:{object}").map(|_| tag.as_ref())
                }
                None => write!(&mut out, "{object} {path} symref-target:{target}").map(|_| object.as_ref()),
            },
            handshake::Ref::Unborn { full_ref_name, target } => {
                static NULL: gix::hash::ObjectId = gix::hash::ObjectId::null(gix::hash::Kind::Sha1);
                write!(&mut out, "unborn {full_ref_name} symref-target:{target}").map(|_| NULL.as_ref())
            }
        }
    }

    pub(crate) fn print(mut out: impl std::io::Write, refs: &[handshake::Ref]) -> std::io::Result<()> {
        for r in refs {
            print_ref(&mut out, r)?;
            writeln!(out)?;
        }
        Ok(())
    }
}
#[cfg(any(feature = "blocking-client", feature = "async-client"))]
pub use refs_impl::{JsonRef, refs, refs_fn as refs};

#[cfg(any(feature = "blocking-client", feature = "async-client"))]
pub(crate) fn by_name_or_url<'repo>(
    repo: &'repo gix::Repository,
    name_or_url: Option<&str>,
) -> anyhow::Result<gix::Remote<'repo>> {
    repo.find_fetch_remote(name_or_url.map(Into::into)).map_err(Into::into)
}
