use std::cmp::Ordering;
use std::collections::{BTreeMap, BTreeSet};
use std::ops::Bound;

use indexmap::IndexSet;
use itertools::Itertools;
use owo_colors::OwoColorize;
use pubgrub::{DerivationTree, Derived, External, Map, Range, ReportFormatter, Term};
use rustc_hash::FxHashMap;

use uv_configuration::{IndexStrategy, NoBinary, NoBuild};
use uv_distribution_types::{
    IncompatibleDist, IncompatibleSource, IncompatibleWheel, Index, IndexCapabilities,
    IndexLocations, IndexMetadata, IndexUrl, RequiresPython,
};
use uv_normalize::PackageName;
use uv_pep440::{Version, VersionSpecifiers};
use uv_platform_tags::{AbiTag, IncompatibleTag, LanguageTag, PlatformTag, Tags};

use crate::candidate_selector::CandidateSelector;
use crate::error::{ErrorTree, PrefixMatch};
use crate::fork_indexes::ForkIndexes;
use crate::fork_urls::ForkUrls;
use crate::prerelease::AllowPrerelease;
use crate::pubgrub::{PubGrubPackage, PubGrubPackageInner, PubGrubPython};
use crate::python_requirement::{PythonRequirement, PythonRequirementSource};
use crate::resolver::{
    MetadataUnavailable, UnavailableErrorChain, UnavailablePackage, UnavailableReason,
    UnavailableVersion,
};
use crate::{Flexibility, InMemoryIndex, Options, ResolverEnvironment, VersionsResponse};

#[derive(Debug)]
pub(crate) struct PubGrubReportFormatter<'a> {
    /// The versions that were available for each package.
    pub(crate) available_versions: &'a FxHashMap<PackageName, BTreeSet<Version>>,

    /// The versions that were available for each package.
    pub(crate) python_requirement: &'a PythonRequirement,

    /// The members of the workspace.
    pub(crate) workspace_members: &'a BTreeSet<PackageName>,

    /// The compatible tags for the resolution.
    pub(crate) tags: Option<&'a Tags>,
}

impl ReportFormatter<PubGrubPackage, Range<Version>, UnavailableReason>
    for PubGrubReportFormatter<'_>
{
    type Output = String;

    fn format_external(
        &self,
        external: &External<PubGrubPackage, Range<Version>, UnavailableReason>,
    ) -> Self::Output {
        match external {
            External::NotRoot(package, version) => {
                format!("we are solving dependencies of {package} {version}")
            }
            External::NoVersions(package, set) => {
                if matches!(
                    &**package,
                    PubGrubPackageInner::Python(PubGrubPython::Target)
                ) {
                    let target = self.python_requirement.target();
                    return format!(
                        "the requested {package} version ({target}) does not satisfy {}",
                        self.compatible_range(package, set)
                    );
                }
                if matches!(
                    &**package,
                    PubGrubPackageInner::Python(PubGrubPython::Installed)
                ) {
                    let installed = self.python_requirement.exact();
                    return format!(
                        "the current {package} version ({installed}) does not satisfy {}",
                        self.compatible_range(package, set)
                    );
                }

                if set == &Range::full() {
                    format!("there are no versions of {package}")
                } else if set.as_singleton().is_some() {
                    format!("there is no version of {package}{set}")
                } else {
                    let complement = set.complement();
                    let range =
                        // Note that sometimes we do not have a range of available versions, e.g.,
                        // when a package is from a non-registry source. In that case, we cannot
                        // perform further simplification of the range.
                        if let Some(available_versions) = package.name().and_then(|name| self.available_versions.get(name)) {
                            update_availability_range(&complement, available_versions)
                        } else {
                            complement
                        };
                    if range.is_empty() {
                        return format!("there are no versions of {package}");
                    }
                    if range.iter().count() == 1 {
                        format!(
                            "only {} is available",
                            self.availability_range(package, &range)
                        )
                    } else {
                        format!(
                            "only the following versions of {} {}",
                            package,
                            self.availability_range(package, &range)
                        )
                    }
                }
            }
            External::Custom(package, set, reason) => {
                if let Some(root) = self.format_root(package) {
                    format!("{root} cannot be used because {reason}")
                } else {
                    match reason {
                        UnavailableReason::Package(reason) => {
                            let message = reason.singular_message();
                            format!("{}{}", package, Padded::new(" ", &message, ""))
                        }
                        UnavailableReason::Version(reason) => {
                            let range = self.compatible_range(package, set);
                            let message = if range.plural() {
                                reason.plural_message()
                            } else {
                                reason.singular_message()
                            };
                            let context = reason.context_message(
                                self.tags,
                                self.python_requirement.target().abi_tag(),
                            );
                            if let Some(context) = context {
                                format!("{}{}{}", range, Padded::new(" ", &message, " "), context)
                            } else {
                                format!("{}{}", range, Padded::new(" ", &message, ""))
                            }
                        }
                    }
                }
            }
            External::FromDependencyOf(package, package_set, dependency, dependency_set) => {
                if package.name_no_root() == dependency.name_no_root() {
                    if let Some(member) = self.format_workspace_member(package) {
                        return format!(
                            "{member} depends on itself at an incompatible version ({})",
                            PackageRange::dependency(dependency, dependency_set, None)
                        );
                    }
                }

                if let Some(root) = self.format_root_requires(package) {
                    return format!(
                        "{root} {}",
                        self.dependency_range(dependency, dependency_set)
                    );
                }
                format!(
                    "{}",
                    self.compatible_range(package, package_set)
                        .depends_on(dependency, dependency_set),
                )
            }
        }
    }

    /// Try to print terms of an incompatibility in a human-readable way.
    fn format_terms(&self, terms: &Map<PubGrubPackage, Term<Range<Version>>>) -> String {
        let mut terms_vec: Vec<_> = terms.iter().collect();
        // We avoid relying on hashmap iteration order here by always sorting
        // by package first.
        terms_vec.sort_by(|&(pkg1, _), &(pkg2, _)| pkg1.cmp(pkg2));
        match terms_vec.as_slice() {
            [] => "the requirements are unsatisfiable".into(),
            [(root, _)] if matches!(&**(*root), PubGrubPackageInner::Root(_)) => {
                let root = self.format_root(root).unwrap();
                format!("{root} are unsatisfiable")
            }
            [(package, Term::Positive(range))]
                if matches!(&**(*package), PubGrubPackageInner::Package { .. }) =>
            {
                if let Some(member) = self.format_workspace_member(package) {
                    format!("{member}'s requirements are unsatisfiable")
                } else {
                    format!("{} cannot be used", self.compatible_range(package, range))
                }
            }
            [(package, Term::Negative(range))]
                if matches!(&**(*package), PubGrubPackageInner::Package { .. }) =>
            {
                format!("{} must be used", self.compatible_range(package, range))
            }
            [(p1, Term::Positive(r1)), (p2, Term::Negative(r2))] => self.format_external(
                &External::FromDependencyOf((*p1).clone(), r1.clone(), (*p2).clone(), r2.clone()),
            ),
            [(p1, Term::Negative(r1)), (p2, Term::Positive(r2))] => self.format_external(
                &External::FromDependencyOf((*p2).clone(), r2.clone(), (*p1).clone(), r1.clone()),
            ),
            slice => {
                let mut result = String::new();
                let str_terms: Vec<_> = slice
                    .iter()
                    .map(|(p, t)| format!("{}", PackageTerm::new(p, t, self)))
                    .collect();
                for (index, term) in str_terms.iter().enumerate() {
                    result.push_str(term);
                    match str_terms.len().cmp(&2) {
                        Ordering::Equal if index == 0 => {
                            result.push_str(" and ");
                        }
                        Ordering::Greater if index + 1 < str_terms.len() => {
                            result.push_str(", ");
                        }
                        _ => (),
                    }
                }
                if slice.len() == 1 {
                    result.push_str(" cannot be used");
                } else {
                    result.push_str(" are incompatible");
                }
                result
            }
        }
    }

    /// Simplest case, we just combine two external incompatibilities.
    fn explain_both_external(
        &self,
        external1: &External<PubGrubPackage, Range<Version>, UnavailableReason>,
        external2: &External<PubGrubPackage, Range<Version>, UnavailableReason>,
        current_terms: &Map<PubGrubPackage, Term<Range<Version>>>,
    ) -> String {
        let external = self.format_both_external(external1, external2);
        let terms = self.format_terms(current_terms);

        format!(
            "Because {}we can conclude that {}",
            Padded::from_string("", &external, ", "),
            Padded::from_string("", &terms, "."),
        )
    }

    /// Both causes have already been explained so we use their refs.
    fn explain_both_ref(
        &self,
        ref_id1: usize,
        derived1: &Derived<PubGrubPackage, Range<Version>, UnavailableReason>,
        ref_id2: usize,
        derived2: &Derived<PubGrubPackage, Range<Version>, UnavailableReason>,
        current_terms: &Map<PubGrubPackage, Term<Range<Version>>>,
    ) -> String {
        // TODO: order should be chosen to make it more logical.

        let derived1_terms = self.format_terms(&derived1.terms);
        let derived2_terms = self.format_terms(&derived2.terms);
        let current_terms = self.format_terms(current_terms);

        format!(
            "Because we know from ({}) that {}and we know from ({}) that {}{}",
            ref_id1,
            Padded::new("", &derived1_terms, " "),
            ref_id2,
            Padded::new("", &derived2_terms, ", "),
            Padded::new("", &current_terms, "."),
        )
    }

    /// One cause is derived (already explained so one-line),
    /// the other is a one-line external cause,
    /// and finally we conclude with the current incompatibility.
    fn explain_ref_and_external(
        &self,
        ref_id: usize,
        derived: &Derived<PubGrubPackage, Range<Version>, UnavailableReason>,
        external: &External<PubGrubPackage, Range<Version>, UnavailableReason>,
        current_terms: &Map<PubGrubPackage, Term<Range<Version>>>,
    ) -> String {
        // TODO: order should be chosen to make it more logical.

        let derived_terms = self.format_terms(&derived.terms);
        let external = self.format_external(external);
        let current_terms = self.format_terms(current_terms);

        format!(
            "Because we know from ({}) that {}and {}we can conclude that {}",
            ref_id,
            Padded::new("", &derived_terms, " "),
            Padded::new("", &external, ", "),
            Padded::new("", &current_terms, "."),
        )
    }

    /// Add an external cause to the chain of explanations.
    fn and_explain_external(
        &self,
        external: &External<PubGrubPackage, Range<Version>, UnavailableReason>,
        current_terms: &Map<PubGrubPackage, Term<Range<Version>>>,
    ) -> String {
        let external = self.format_external(external);
        let terms = self.format_terms(current_terms);

        format!(
            "And because {}we can conclude that {}",
            Padded::from_string("", &external, ", "),
            Padded::from_string("", &terms, "."),
        )
    }

    /// Add an already explained incompat to the chain of explanations.
    fn and_explain_ref(
        &self,
        ref_id: usize,
        derived: &Derived<PubGrubPackage, Range<Version>, UnavailableReason>,
        current_terms: &Map<PubGrubPackage, Term<Range<Version>>>,
    ) -> String {
        let derived = self.format_terms(&derived.terms);
        let current = self.format_terms(current_terms);

        format!(
            "And because we know from ({}) that {}we can conclude that {}",
            ref_id,
            Padded::from_string("", &derived, ", "),
            Padded::from_string("", &current, "."),
        )
    }

    /// Add an already explained incompat to the chain of explanations.
    fn and_explain_prior_and_external(
        &self,
        prior_external: &External<PubGrubPackage, Range<Version>, UnavailableReason>,
        external: &External<PubGrubPackage, Range<Version>, UnavailableReason>,
        current_terms: &Map<PubGrubPackage, Term<Range<Version>>>,
    ) -> String {
        let external = self.format_both_external(prior_external, external);
        let terms = self.format_terms(current_terms);

        format!(
            "And because {}we can conclude that {}",
            Padded::from_string("", &external, ", "),
            Padded::from_string("", &terms, "."),
        )
    }
}

impl PubGrubReportFormatter<'_> {
    /// Return the formatting for "the root package requires", if the given
    /// package is the root package.
    ///
    /// If not given the root package, returns `None`.
    fn format_root_requires(&self, package: &PubGrubPackage) -> Option<String> {
        if self.is_workspace() {
            if matches!(&**package, PubGrubPackageInner::Root(_)) {
                if self.is_single_project_workspace() {
                    return Some("your project requires".to_string());
                }
                return Some("your workspace requires".to_string());
            }
        }
        match &**package {
            PubGrubPackageInner::Root(Some(name)) => Some(format!("{name} depends on")),
            PubGrubPackageInner::Root(None) => Some("you require".to_string()),
            _ => None,
        }
    }

    /// Return the formatting for "the root package", if the given
    /// package is the root package.
    ///
    /// If not given the root package, returns `None`.
    fn format_root(&self, package: &PubGrubPackage) -> Option<String> {
        if self.is_workspace() {
            if matches!(&**package, PubGrubPackageInner::Root(_)) {
                if self.is_single_project_workspace() {
                    return Some("your project's requirements".to_string());
                }
                return Some("your workspace's requirements".to_string());
            }
        }
        match &**package {
            PubGrubPackageInner::Root(Some(_)) => Some("your requirements".to_string()),
            PubGrubPackageInner::Root(None) => Some("your requirements".to_string()),
            _ => None,
        }
    }

    /// Whether the resolution error is for a workspace.
    fn is_workspace(&self) -> bool {
        !self.workspace_members.is_empty()
    }

    /// Whether the resolution error is for a workspace with a exactly one project.
    fn is_single_project_workspace(&self) -> bool {
        self.workspace_members.len() == 1
    }

    /// Return a display name for the package if it is a workspace member.
    fn format_workspace_member(&self, package: &PubGrubPackage) -> Option<String> {
        match &**package {
            // TODO(zanieb): Improve handling of dev and extra for single-project workspaces
            PubGrubPackageInner::Package {
                name, extra, group, ..
            } if self.workspace_members.contains(name) => {
                if self.is_single_project_workspace() && extra.is_none() && group.is_none() {
                    Some("your project".to_string())
                } else {
                    Some(format!("{package}"))
                }
            }
            PubGrubPackageInner::Extra { name, .. } if self.workspace_members.contains(name) => {
                Some(format!("{package}"))
            }
            PubGrubPackageInner::Group { name, .. } if self.workspace_members.contains(name) => {
                Some(format!("{package}"))
            }
            _ => None,
        }
    }

    /// Return whether the given package is the root package.
    fn is_root(package: &PubGrubPackage) -> bool {
        matches!(&**package, PubGrubPackageInner::Root(_))
    }

    /// Return whether the given package is a workspace member.
    fn is_single_project_workspace_member(&self, package: &PubGrubPackage) -> bool {
        match &**package {
            // TODO(zanieb): Improve handling of dev and extra for single-project workspaces
            PubGrubPackageInner::Package {
                name, extra, group, ..
            } if self.workspace_members.contains(name) => {
                self.is_single_project_workspace() && extra.is_none() && group.is_none()
            }
            _ => false,
        }
    }

    /// Create a [`PackageRange::compatibility`] display with this formatter attached.
    fn compatible_range<'a>(
        &'a self,
        package: &'a PubGrubPackage,
        range: &'a Range<Version>,
    ) -> PackageRange<'a> {
        PackageRange::compatibility(package, range, Some(self))
    }

    /// Create a [`PackageRange::dependency`] display with this formatter attached.
    fn dependency_range<'a>(
        &'a self,
        package: &'a PubGrubPackage,
        range: &'a Range<Version>,
    ) -> PackageRange<'a> {
        PackageRange::dependency(package, range, Some(self))
    }

    /// Create a [`PackageRange::availability`] display with this formatter attached.
    fn availability_range<'a>(
        &'a self,
        package: &'a PubGrubPackage,
        range: &'a Range<Version>,
    ) -> PackageRange<'a> {
        PackageRange::availability(package, range, Some(self))
    }

    /// Format two external incompatibilities, combining them if possible.
    fn format_both_external(
        &self,
        external1: &External<PubGrubPackage, Range<Version>, UnavailableReason>,
        external2: &External<PubGrubPackage, Range<Version>, UnavailableReason>,
    ) -> String {
        match (external1, external2) {
            (
                External::FromDependencyOf(package1, package_set1, dependency1, dependency_set1),
                External::FromDependencyOf(package2, _, dependency2, dependency_set2),
            ) if package1 == package2 => {
                let dependency1 = self.dependency_range(dependency1, dependency_set1);
                let dependency2 = self.dependency_range(dependency2, dependency_set2);

                if let Some(root) = self.format_root_requires(package1) {
                    return format!(
                        "{root} {}and {}",
                        Padded::new("", &dependency1, " "),
                        dependency2,
                    );
                }

                format!(
                    "{}",
                    self.compatible_range(package1, package_set1)
                        .depends_on(dependency1.package, dependency_set1)
                        .and(dependency2.package, dependency_set2),
                )
            }
            (.., External::FromDependencyOf(package, _, dependency, _))
                if Self::is_root(package)
                    && self.is_single_project_workspace_member(dependency) =>
            {
                self.format_external(external1)
            }
            (External::FromDependencyOf(package, _, dependency, _), ..)
                if Self::is_root(package)
                    && self.is_single_project_workspace_member(dependency) =>
            {
                self.format_external(external2)
            }
            _ => {
                let external1 = self.format_external(external1);
                let external2 = self.format_external(external2);

                format!(
                    "{}and {}",
                    Padded::from_string("", &external1, " "),
                    &external2,
                )
            }
        }
    }

    /// Generate the [`PubGrubHints`] for a derivation tree.
    ///
    /// The [`PubGrubHints`] help users resolve errors by providing additional context or modifying
    /// their requirements.
    pub(crate) fn generate_hints(
        &self,
        derivation_tree: &ErrorTree,
        index: &InMemoryIndex,
        selector: &CandidateSelector,
        index_locations: &IndexLocations,
        index_capabilities: &IndexCapabilities,
        available_indexes: &FxHashMap<PackageName, BTreeSet<IndexUrl>>,
        unavailable_packages: &FxHashMap<PackageName, UnavailablePackage>,
        incomplete_packages: &FxHashMap<PackageName, BTreeMap<Version, MetadataUnavailable>>,
        fork_urls: &ForkUrls,
        fork_indexes: &ForkIndexes,
        env: &ResolverEnvironment,
        tags: Option<&Tags>,
        workspace_members: &BTreeSet<PackageName>,
        options: &Options,
        output_hints: &mut IndexSet<PubGrubHint>,
    ) {
        match derivation_tree {
            DerivationTree::External(External::Custom(package, set, reason)) => {
                if let Some(name) = package.name_no_root() {
                    // Check for no versions due to pre-release options.
                    if options.flexibility == Flexibility::Configurable {
                        if !fork_urls.contains_key(name) {
                            self.prerelease_available_hint(name, set, selector, env, output_hints);
                        }
                    }

                    // Check for no versions due to no `--find-links` flat index.
                    Self::index_hints(
                        name,
                        set,
                        selector,
                        index_locations,
                        index_capabilities,
                        available_indexes,
                        unavailable_packages,
                        incomplete_packages,
                        output_hints,
                    );

                    if let UnavailableReason::Version(UnavailableVersion::IncompatibleDist(
                        incompatibility,
                    )) = reason
                    {
                        match incompatibility {
                            // Check for unavailable versions due to `--no-build` or `--no-binary`.
                            IncompatibleDist::Wheel(IncompatibleWheel::NoBinary) => {
                                output_hints.insert(PubGrubHint::NoBinary {
                                    package: name.clone(),
                                    option: options.build_options.no_binary().clone(),
                                });
                            }
                            IncompatibleDist::Source(IncompatibleSource::NoBuild) => {
                                output_hints.insert(PubGrubHint::NoBuild {
                                    package: name.clone(),
                                    option: options.build_options.no_build().clone(),
                                });
                            }
                            // Check for unavailable versions due to incompatible tags.
                            IncompatibleDist::Wheel(IncompatibleWheel::Tag(tag)) => {
                                if let Some(hint) = self.tag_hint(
                                    name,
                                    set,
                                    *tag,
                                    index,
                                    selector,
                                    fork_indexes,
                                    env,
                                    tags,
                                ) {
                                    output_hints.insert(hint);
                                }
                            }
                            _ => {}
                        }
                    }
                }
            }
            DerivationTree::External(External::NoVersions(package, set)) => {
                if let Some(name) = package.name_no_root() {
                    // Check for no versions due to pre-release options.
                    if options.flexibility == Flexibility::Configurable {
                        if !fork_urls.contains_key(name) {
                            self.prerelease_available_hint(name, set, selector, env, output_hints);
                        }
                    }

                    // Check for no versions due to no `--find-links` flat index.
                    Self::index_hints(
                        name,
                        set,
                        selector,
                        index_locations,
                        index_capabilities,
                        available_indexes,
                        unavailable_packages,
                        incomplete_packages,
                        output_hints,
                    );
                }
            }
            DerivationTree::External(External::FromDependencyOf(
                package,
                package_set,
                dependency,
                dependency_set,
            )) => {
                // Check for a dependency on a workspace package by a non-workspace package.
                // Generally, this indicates that the workspace package is shadowing a transitive
                // dependency name.
                if let (Some(package_name), Some(dependency_name)) =
                    (package.name(), dependency.name())
                {
                    if workspace_members.contains(dependency_name)
                        && !workspace_members.contains(package_name)
                    {
                        output_hints.insert(PubGrubHint::DependsOnWorkspacePackage {
                            package: package_name.clone(),
                            dependency: dependency_name.clone(),
                            workspace: self.is_workspace() && !self.is_single_project_workspace(),
                        });
                    }

                    if package_name == dependency_name
                        && (dependency.extra().is_none() || package.extra() == dependency.extra())
                        && (dependency.group().is_none() || dependency.group() == package.group())
                        && workspace_members.contains(package_name)
                    {
                        output_hints.insert(PubGrubHint::DependsOnItself {
                            package: package_name.clone(),
                            workspace: self.is_workspace() && !self.is_single_project_workspace(),
                        });
                    }
                }
                // Check for no versions due to `Requires-Python`.
                if matches!(
                    &**dependency,
                    PubGrubPackageInner::Python(PubGrubPython::Target)
                ) {
                    if let Some(name) = package.name() {
                        output_hints.insert(PubGrubHint::RequiresPython {
                            source: self.python_requirement.source(),
                            requires_python: self.python_requirement.target().clone(),
                            name: name.clone(),
                            package_set: package_set.clone(),
                            package_requires_python: dependency_set.clone(),
                        });
                    }
                }
            }
            DerivationTree::External(External::NotRoot(..)) => {}
            DerivationTree::Derived(derived) => {
                self.generate_hints(
                    &derived.cause1,
                    index,
                    selector,
                    index_locations,
                    index_capabilities,
                    available_indexes,
                    unavailable_packages,
                    incomplete_packages,
                    fork_urls,
                    fork_indexes,
                    env,
                    tags,
                    workspace_members,
                    options,
                    output_hints,
                );
                self.generate_hints(
                    &derived.cause2,
                    index,
                    selector,
                    index_locations,
                    index_capabilities,
                    available_indexes,
                    unavailable_packages,
                    incomplete_packages,
                    fork_urls,
                    fork_indexes,
                    env,
                    tags,
                    workspace_members,
                    options,
                    output_hints,
                );
            }
        }
    }

    /// Generate a [`PubGrubHint`] for a package that doesn't have any wheels matching the current
    /// Python version, ABI, or platform.
    fn tag_hint(
        &self,
        name: &PackageName,
        set: &Range<Version>,
        tag: IncompatibleTag,
        index: &InMemoryIndex,
        selector: &CandidateSelector,
        fork_indexes: &ForkIndexes,
        env: &ResolverEnvironment,
        tags: Option<&Tags>,
    ) -> Option<PubGrubHint> {
        let response = if let Some(url) = fork_indexes.get(name).map(IndexMetadata::url) {
            index.explicit().get(&(name.clone(), url.clone()))
        } else {
            index.implicit().get(name)
        }?;

        let VersionsResponse::Found(version_maps) = &*response else {
            return None;
        };

        let candidate = selector.select_no_preference(name, set, version_maps, env)?;

        let prioritized = candidate.prioritized()?;

        match tag {
            IncompatibleTag::Invalid => None,
            IncompatibleTag::Python => {
                let best = tags.and_then(Tags::python_tag);
                let tags = prioritized.python_tags().collect::<BTreeSet<_>>();
                if tags.is_empty() {
                    None
                } else {
                    Some(PubGrubHint::LanguageTags {
                        package: name.clone(),
                        version: candidate.version().clone(),
                        tags,
                        best,
                    })
                }
            }
            IncompatibleTag::Abi | IncompatibleTag::AbiPythonVersion => {
                let best = tags.and_then(Tags::abi_tag);
                let tags = prioritized
                    .abi_tags()
                    // Ignore `none`, which is universally compatible.
                    //
                    // As an example, `none` can appear here if we're solving for Python 3.13, and
                    // the distribution includes a wheel for `cp312-none-macosx_11_0_arm64`.
                    //
                    // In that case, the wheel isn't compatible, but when solving for Python 3.13,
                    // the `cp312` Python tag _can_ be compatible (e.g., for `cp312-abi3-macosx_11_0_arm64.whl`),
                    // so this is considered an ABI incompatibility rather than Python incompatibility.
                    .filter(|tag| *tag != AbiTag::None)
                    .collect::<BTreeSet<_>>();
                if tags.is_empty() {
                    None
                } else {
                    Some(PubGrubHint::AbiTags {
                        package: name.clone(),
                        version: candidate.version().clone(),
                        tags,
                        best,
                    })
                }
            }
            IncompatibleTag::Platform => {
                // We don't want to report all available platforms, since it's plausible that there
                // are wheels for the current platform, but at a different ABI. For example, when
                // solving for Python 3.13 on macOS, `cp312-cp312-macosx_11_0_arm64` could be
                // available along with `cp313-cp313-manylinux2014`. In this case, we'd consider
                // the distribution to be platform-incompatible, since `cp313-cp313` matches the
                // compatible wheel tags. But showing `macosx_11_0_arm64` here would be misleading.
                //
                // So, instead, we only show the platforms that are linked to otherwise-compatible
                // wheels (e.g., `manylinux2014` in `cp313-cp313-manylinux2014`). In other words,
                // we only show platforms for ABI-compatible wheels.
                let tags = prioritized
                    .platform_tags(self.tags?)
                    .cloned()
                    .collect::<BTreeSet<_>>();
                if tags.is_empty() {
                    None
                } else {
                    Some(PubGrubHint::PlatformTags {
                        package: name.clone(),
                        version: candidate.version().clone(),
                        tags,
                    })
                }
            }
        }
    }

    fn index_hints(
        name: &PackageName,
        set: &Range<Version>,
        selector: &CandidateSelector,
        index_locations: &IndexLocations,
        index_capabilities: &IndexCapabilities,
        available_indexes: &FxHashMap<PackageName, BTreeSet<IndexUrl>>,
        unavailable_packages: &FxHashMap<PackageName, UnavailablePackage>,
        incomplete_packages: &FxHashMap<PackageName, BTreeMap<Version, MetadataUnavailable>>,
        hints: &mut IndexSet<PubGrubHint>,
    ) {
        let no_find_links = index_locations.flat_indexes().peekable().peek().is_none();

        // Add hints due to the package being entirely unavailable.
        match unavailable_packages.get(name) {
            Some(UnavailablePackage::NoIndex) => {
                if no_find_links {
                    hints.insert(PubGrubHint::NoIndex);
                }
            }
            Some(UnavailablePackage::Offline) => {
                hints.insert(PubGrubHint::Offline);
            }
            Some(UnavailablePackage::InvalidMetadata(reason)) => {
                hints.insert(PubGrubHint::InvalidPackageMetadata {
                    package: name.clone(),
                    reason: reason.clone(),
                });
            }
            Some(UnavailablePackage::InvalidStructure(reason)) => {
                hints.insert(PubGrubHint::InvalidPackageStructure {
                    package: name.clone(),
                    reason: reason.clone(),
                });
            }
            Some(UnavailablePackage::NotFound) => {}
            None => {}
        }

        // Add hints due to the package being unavailable at specific versions.
        if let Some(versions) = incomplete_packages.get(name) {
            for (version, incomplete) in versions.iter().rev() {
                if set.contains(version) {
                    match incomplete {
                        MetadataUnavailable::Offline => {
                            hints.insert(PubGrubHint::Offline);
                        }
                        MetadataUnavailable::InvalidMetadata(reason) => {
                            hints.insert(PubGrubHint::InvalidVersionMetadata {
                                package: name.clone(),
                                version: version.clone(),
                                reason: reason.to_string(),
                            });
                        }
                        MetadataUnavailable::InconsistentMetadata(reason) => {
                            hints.insert(PubGrubHint::InconsistentVersionMetadata {
                                package: name.clone(),
                                version: version.clone(),
                                reason: reason.to_string(),
                            });
                        }
                        MetadataUnavailable::InvalidStructure(reason) => {
                            hints.insert(PubGrubHint::InvalidVersionStructure {
                                package: name.clone(),
                                version: version.clone(),
                                reason: reason.to_string(),
                            });
                        }
                        MetadataUnavailable::RequiresPython(requires_python, python_version) => {
                            hints.insert(PubGrubHint::IncompatibleBuildRequirement {
                                package: name.clone(),
                                version: version.clone(),
                                requires_python: requires_python.clone(),
                                python_version: python_version.clone(),
                            });
                        }
                    }
                    break;
                }
            }
        }

        // Add hints due to the package being available on an index, but not at the correct version,
        // with subsequent indexes that were _not_ queried.
        if matches!(selector.index_strategy(), IndexStrategy::FirstIndex) {
            // Do not include the hint if the set is "all versions". This is an unusual but valid
            // case in which a package returns a 200 response, but without any versions or
            // distributions for the package.
            if !set
                .iter()
                .all(|range| matches!(range, (Bound::Unbounded, Bound::Unbounded)))
            {
                if let Some(found_index) = available_indexes.get(name).and_then(BTreeSet::first) {
                    // Determine whether the index is the last-available index. If not, then some
                    // indexes were not queried, and could contain a compatible version.
                    if let Some(next_index) = index_locations
                        .indexes()
                        .map(Index::url)
                        .skip_while(|url| *url != found_index)
                        .nth(1)
                    {
                        hints.insert(PubGrubHint::UncheckedIndex {
                            name: name.clone(),
                            range: set.clone(),
                            found_index: found_index.clone(),
                            next_index: next_index.clone(),
                        });
                    }
                }
            }
        }

        // Add hints due to an index returning an unauthorized response.
        for index in index_locations.allowed_indexes() {
            if index_capabilities.unauthorized(&index.url) {
                hints.insert(PubGrubHint::UnauthorizedIndex {
                    index: index.url.clone(),
                });
            }
            if index_capabilities.forbidden(&index.url) {
                hints.insert(PubGrubHint::ForbiddenIndex {
                    index: index.url.clone(),
                });
            }
        }
    }

    fn prerelease_available_hint(
        &self,
        name: &PackageName,
        set: &Range<Version>,
        selector: &CandidateSelector,
        env: &ResolverEnvironment,
        hints: &mut IndexSet<PubGrubHint>,
    ) {
        let any_prerelease = set.iter().any(|(start, end)| {
            // Ignore, e.g., `>=2.4.dev0,<2.5.dev0`, which is the desugared form of `==2.4.*`.
            if PrefixMatch::from_range(start, end).is_some() {
                return false;
            }

            let is_pre1 = match start {
                Bound::Included(version) => version.any_prerelease(),
                Bound::Excluded(version) => version.any_prerelease(),
                Bound::Unbounded => false,
            };
            if is_pre1 {
                return true;
            }

            let is_pre2 = match end {
                Bound::Included(version) => version.any_prerelease(),
                Bound::Excluded(version) => version.any_prerelease(),
                Bound::Unbounded => false,
            };
            if is_pre2 {
                return true;
            }

            false
        });

        if any_prerelease {
            // A pre-release marker appeared in the version requirements.
            if selector.prerelease_strategy().allows(name, env) != AllowPrerelease::Yes {
                hints.insert(PubGrubHint::PrereleaseRequested {
                    name: name.clone(),
                    range: set.clone(),
                });
            }
        } else if let Some(version) = self.available_versions.get(name).and_then(|versions| {
            versions
                .iter()
                .rev()
                .filter(|version| version.any_prerelease())
                .find(|version| set.contains(version))
        }) {
            // There are pre-release versions available for the package.
            if selector.prerelease_strategy().allows(name, env) != AllowPrerelease::Yes {
                hints.insert(PubGrubHint::PrereleaseAvailable {
                    package: name.clone(),
                    version: version.clone(),
                });
            }
        }
    }
}

#[derive(Debug, Clone)]
pub(crate) enum PubGrubHint {
    /// There are pre-release versions available for a package, but pre-releases weren't enabled
    /// for that package.
    ///
    PrereleaseAvailable {
        package: PackageName,
        // excluded from `PartialEq` and `Hash`
        version: Version,
    },
    /// A requirement included a pre-release marker, but pre-releases weren't enabled for that
    /// package.
    PrereleaseRequested {
        name: PackageName,
        // excluded from `PartialEq` and `Hash`
        range: Range<Version>,
    },
    /// Requirements were unavailable due to lookups in the index being disabled and no extra
    /// index was provided via `--find-links`
    NoIndex,
    /// A package was not found in the registry, but network access was disabled.
    Offline,
    /// Metadata for a package could not be parsed.
    InvalidPackageMetadata {
        package: PackageName,
        // excluded from `PartialEq` and `Hash`
        reason: UnavailableErrorChain,
    },
    /// The structure of a package was invalid (e.g., multiple `.dist-info` directories).
    InvalidPackageStructure {
        package: PackageName,
        // excluded from `PartialEq` and `Hash`
        reason: UnavailableErrorChain,
    },
    /// Metadata for a package version could not be parsed.
    InvalidVersionMetadata {
        package: PackageName,
        // excluded from `PartialEq` and `Hash`
        version: Version,
        // excluded from `PartialEq` and `Hash`
        reason: String,
    },
    /// Metadata for a package version was inconsistent (e.g., the package name did not match that
    /// of the file).
    InconsistentVersionMetadata {
        package: PackageName,
        // excluded from `PartialEq` and `Hash`
        version: Version,
        // excluded from `PartialEq` and `Hash`
        reason: String,
    },
    /// The structure of a package version was invalid (e.g., multiple `.dist-info` directories).
    InvalidVersionStructure {
        package: PackageName,
        // excluded from `PartialEq` and `Hash`
        version: Version,
        // excluded from `PartialEq` and `Hash`
        reason: String,
    },
    /// The source distribution has a `requires-python` requirement that is not met by the installed
    /// Python version (and static metadata is not available).
    IncompatibleBuildRequirement {
        package: PackageName,
        // excluded from `PartialEq` and `Hash`
        version: Version,
        // excluded from `PartialEq` and `Hash`
        requires_python: VersionSpecifiers,
        // excluded from `PartialEq` and `Hash`
        python_version: Version,
    },
    /// The `Requires-Python` requirement was not satisfied.
    RequiresPython {
        source: PythonRequirementSource,
        requires_python: RequiresPython,
        // excluded from `PartialEq` and `Hash`
        name: PackageName,
        // excluded from `PartialEq` and `Hash`
        package_set: Range<Version>,
        // excluded from `PartialEq` and `Hash`
        package_requires_python: Range<Version>,
    },
    /// A non-workspace package depends on a workspace package, which is likely shadowing a
    /// transitive dependency.
    DependsOnWorkspacePackage {
        package: PackageName,
        dependency: PackageName,
        workspace: bool,
    },
    /// A package depends on itself at an incompatible version.
    DependsOnItself {
        package: PackageName,
        workspace: bool,
    },
    /// A package was available on an index, but not at the correct version, and at least one
    /// subsequent index was not queried. As such, a compatible version may be available on
    /// one of the remaining indexes.
    UncheckedIndex {
        name: PackageName,
        // excluded from `PartialEq` and `Hash`
        range: Range<Version>,
        // excluded from `PartialEq` and `Hash`
        found_index: IndexUrl,
        // excluded from `PartialEq` and `Hash`
        next_index: IndexUrl,
    },
    /// No wheels are available for a package, and using source distributions was disabled.
    NoBuild {
        package: PackageName,
        // excluded from `PartialEq` and `Hash`
        option: NoBuild,
    },
    /// No source distributions are available for a package, and using pre-built wheels was disabled.
    NoBinary {
        package: PackageName,
        // excluded from `PartialEq` and `Hash`
        option: NoBinary,
    },
    /// An index returned an Unauthorized (401) response.
    UnauthorizedIndex { index: IndexUrl },
    /// An index returned a Forbidden (403) response.
    ForbiddenIndex { index: IndexUrl },
    /// None of the available wheels for a package have a compatible Python language tag (e.g.,
    /// `cp310` in `cp310-abi3-manylinux_2_17_x86_64.whl`).
    LanguageTags {
        package: PackageName,
        // excluded from `PartialEq` and `Hash`
        version: Version,
        // excluded from `PartialEq` and `Hash`
        tags: BTreeSet<LanguageTag>,
        // excluded from `PartialEq` and `Hash`
        best: Option<LanguageTag>,
    },
    /// None of the available wheels for a package have a compatible ABI tag (e.g., `abi3` in
    /// `cp310-abi3-manylinux_2_17_x86_64.whl`).
    AbiTags {
        package: PackageName,
        // excluded from `PartialEq` and `Hash`
        version: Version,
        // excluded from `PartialEq` and `Hash`
        tags: BTreeSet<AbiTag>,
        // excluded from `PartialEq` and `Hash`
        best: Option<AbiTag>,
    },
    /// None of the available wheels for a package have a compatible platform tag (e.g.,
    /// `manylinux_2_17_x86_64` in `cp310-abi3-manylinux_2_17_x86_64.whl`).
    PlatformTags {
        package: PackageName,
        // excluded from `PartialEq` and `Hash`
        version: Version,
        // excluded from `PartialEq` and `Hash`
        tags: BTreeSet<PlatformTag>,
    },
}

/// This private enum mirrors [`PubGrubHint`] but only includes fields that should be
/// used for `Eq` and `Hash` implementations. It is used to derive `PartialEq` and
/// `Hash` implementations for [`PubGrubHint`].
#[derive(PartialEq, Eq, Hash)]
enum PubGrubHintCore {
    PrereleaseAvailable {
        package: PackageName,
    },
    PrereleaseRequested {
        package: PackageName,
    },
    NoIndex,
    Offline,
    InvalidPackageMetadata {
        package: PackageName,
    },
    InvalidPackageStructure {
        package: PackageName,
    },
    InvalidVersionMetadata {
        package: PackageName,
    },
    InconsistentVersionMetadata {
        package: PackageName,
    },
    InvalidVersionStructure {
        package: PackageName,
    },
    IncompatibleBuildRequirement {
        package: PackageName,
    },
    RequiresPython {
        source: PythonRequirementSource,
        requires_python: RequiresPython,
    },
    DependsOnWorkspacePackage {
        package: PackageName,
        dependency: PackageName,
        workspace: bool,
    },
    DependsOnItself {
        package: PackageName,
        workspace: bool,
    },
    UncheckedIndex {
        package: PackageName,
    },
    UnauthorizedIndex {
        index: IndexUrl,
    },
    ForbiddenIndex {
        index: IndexUrl,
    },
    NoBuild {
        package: PackageName,
    },
    NoBinary {
        package: PackageName,
    },
    LanguageTags {
        package: PackageName,
    },
    AbiTags {
        package: PackageName,
    },
    PlatformTags {
        package: PackageName,
    },
}

impl From<PubGrubHint> for PubGrubHintCore {
    #[inline]
    fn from(hint: PubGrubHint) -> Self {
        match hint {
            PubGrubHint::PrereleaseAvailable { package, .. } => {
                Self::PrereleaseAvailable { package }
            }
            PubGrubHint::PrereleaseRequested { name: package, .. } => {
                Self::PrereleaseRequested { package }
            }
            PubGrubHint::NoIndex => Self::NoIndex,
            PubGrubHint::Offline => Self::Offline,
            PubGrubHint::InvalidPackageMetadata { package, .. } => {
                Self::InvalidPackageMetadata { package }
            }
            PubGrubHint::InvalidPackageStructure { package, .. } => {
                Self::InvalidPackageStructure { package }
            }
            PubGrubHint::InvalidVersionMetadata { package, .. } => {
                Self::InvalidVersionMetadata { package }
            }
            PubGrubHint::InconsistentVersionMetadata { package, .. } => {
                Self::InconsistentVersionMetadata { package }
            }
            PubGrubHint::InvalidVersionStructure { package, .. } => {
                Self::InvalidVersionStructure { package }
            }
            PubGrubHint::IncompatibleBuildRequirement { package, .. } => {
                Self::IncompatibleBuildRequirement { package }
            }
            PubGrubHint::RequiresPython {
                source,
                requires_python,
                ..
            } => Self::RequiresPython {
                source,
                requires_python,
            },
            PubGrubHint::DependsOnWorkspacePackage {
                package,
                dependency,
                workspace,
            } => Self::DependsOnWorkspacePackage {
                package,
                dependency,
                workspace,
            },
            PubGrubHint::DependsOnItself { package, workspace } => {
                Self::DependsOnItself { package, workspace }
            }
            PubGrubHint::UncheckedIndex { name: package, .. } => Self::UncheckedIndex { package },
            PubGrubHint::UnauthorizedIndex { index } => Self::UnauthorizedIndex { index },
            PubGrubHint::ForbiddenIndex { index } => Self::ForbiddenIndex { index },
            PubGrubHint::NoBuild { package, .. } => Self::NoBuild { package },
            PubGrubHint::NoBinary { package, .. } => Self::NoBinary { package },
            PubGrubHint::LanguageTags { package, .. } => Self::LanguageTags { package },
            PubGrubHint::AbiTags { package, .. } => Self::AbiTags { package },
            PubGrubHint::PlatformTags { package, .. } => Self::PlatformTags { package },
        }
    }
}

impl std::hash::Hash for PubGrubHint {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        let core = PubGrubHintCore::from(self.clone());
        core.hash(state);
    }
}

impl PartialEq for PubGrubHint {
    fn eq(&self, other: &Self) -> bool {
        let core = PubGrubHintCore::from(self.clone());
        let other_core = PubGrubHintCore::from(other.clone());
        core == other_core
    }
}

impl Eq for PubGrubHint {}

impl std::fmt::Display for PubGrubHint {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::PrereleaseAvailable { package, version } => {
                write!(
                    f,
                    "{}{} Pre-releases are available for `{}` in the requested range (e.g., {}), but pre-releases weren't enabled (try: `{}`)",
                    "hint".bold().cyan(),
                    ":".bold(),
                    package.cyan(),
                    version.cyan(),
                    "--prerelease=allow".green(),
                )
            }
            Self::PrereleaseRequested { name, range } => {
                write!(
                    f,
                    "{}{} `{}` was requested with a pre-release marker (e.g., {}), but pre-releases weren't enabled (try: `{}`)",
                    "hint".bold().cyan(),
                    ":".bold(),
                    name.cyan(),
                    PackageRange::compatibility(&PubGrubPackage::base(name), range, None).cyan(),
                    "--prerelease=allow".green(),
                )
            }
            Self::NoIndex => {
                write!(
                    f,
                    "{}{} Packages were unavailable because index lookups were disabled and no additional package locations were provided (try: `{}`)",
                    "hint".bold().cyan(),
                    ":".bold(),
                    "--find-links <uri>".green(),
                )
            }
            Self::Offline => {
                write!(
                    f,
                    "{}{} Packages were unavailable because the network was disabled. When the network is disabled, registry packages may only be read from the cache.",
                    "hint".bold().cyan(),
                    ":".bold(),
                )
            }
            Self::InvalidPackageMetadata { package, reason } => {
                write!(
                    f,
                    "{}{} Metadata for `{}` could not be parsed.\n{}",
                    "hint".bold().cyan(),
                    ":".bold(),
                    package.cyan(),
                    textwrap::indent(reason.to_string().as_str(), "  ")
                )
            }
            Self::InvalidPackageStructure { package, reason } => {
                write!(
                    f,
                    "{}{} The structure of `{}` was invalid\n{}",
                    "hint".bold().cyan(),
                    ":".bold(),
                    package.cyan(),
                    textwrap::indent(reason.to_string().as_str(), "  ")
                )
            }
            Self::InvalidVersionMetadata {
                package,
                version,
                reason,
            } => {
                write!(
                    f,
                    "{}{} Metadata for `{}` ({}) could not be parsed:\n{}",
                    "hint".bold().cyan(),
                    ":".bold(),
                    package.cyan(),
                    format!("v{version}").cyan(),
                    textwrap::indent(reason, "  ")
                )
            }
            Self::InvalidVersionStructure {
                package,
                version,
                reason,
            } => {
                write!(
                    f,
                    "{}{} The structure of `{}` ({}) was invalid:\n{}",
                    "hint".bold().cyan(),
                    ":".bold(),
                    package.cyan(),
                    format!("v{version}").cyan(),
                    textwrap::indent(reason, "  ")
                )
            }
            Self::InconsistentVersionMetadata {
                package,
                version,
                reason,
            } => {
                write!(
                    f,
                    "{}{} Metadata for `{}` ({}) was inconsistent:\n{}",
                    "hint".bold().cyan(),
                    ":".bold(),
                    package.cyan(),
                    format!("v{version}").cyan(),
                    textwrap::indent(reason, "  ")
                )
            }
            Self::RequiresPython {
                source: PythonRequirementSource::RequiresPython,
                requires_python,
                name,
                package_set,
                package_requires_python,
            } => {
                write!(
                    f,
                    "{}{} The `requires-python` value ({}) includes Python versions that are not supported by your dependencies (e.g., {} only supports {}). Consider using a more restrictive `requires-python` value (like {}).",
                    "hint".bold().cyan(),
                    ":".bold(),
                    requires_python.cyan(),
                    PackageRange::compatibility(&PubGrubPackage::base(name), package_set, None)
                        .cyan(),
                    package_requires_python.cyan(),
                    package_requires_python.cyan(),
                )
            }
            Self::RequiresPython {
                source: PythonRequirementSource::PythonVersion,
                requires_python,
                name,
                package_set,
                package_requires_python,
            } => {
                write!(
                    f,
                    "{}{} The `--python-version` value ({}) includes Python versions that are not supported by your dependencies (e.g., {} only supports {}). Consider using a higher `--python-version` value.",
                    "hint".bold().cyan(),
                    ":".bold(),
                    requires_python.cyan(),
                    PackageRange::compatibility(&PubGrubPackage::base(name), package_set, None)
                        .cyan(),
                    package_requires_python.cyan(),
                )
            }
            Self::RequiresPython {
                source: PythonRequirementSource::Interpreter,
                requires_python: _,
                name,
                package_set,
                package_requires_python,
            } => {
                write!(
                    f,
                    "{}{} The Python interpreter uses a Python version that is not supported by your dependencies (e.g., {} only supports {}). Consider passing a `--python-version` value to raise the minimum supported version.",
                    "hint".bold().cyan(),
                    ":".bold(),
                    PackageRange::compatibility(&PubGrubPackage::base(name), package_set, None)
                        .cyan(),
                    package_requires_python.cyan(),
                )
            }
            Self::IncompatibleBuildRequirement {
                package,
                version,
                requires_python,
                python_version,
            } => {
                write!(
                    f,
                    "{}{} The source distribution for `{}` ({}) does not include static metadata. Generating metadata for this package requires Python {}, but Python {} is installed.",
                    "hint".bold().cyan(),
                    ":".bold(),
                    package.cyan(),
                    format!("v{version}").cyan(),
                    requires_python.cyan(),
                    python_version.cyan(),
                )
            }
            Self::DependsOnWorkspacePackage {
                package,
                dependency,
                workspace,
            } => {
                let your_project = if *workspace {
                    "one of your workspace members"
                } else {
                    "your project"
                };
                let the_project = if *workspace {
                    "the workspace member"
                } else {
                    "the project"
                };
                write!(
                    f,
                    "{}{} The package `{}` depends on the package `{}` but the name is shadowed by {your_project}. Consider changing the name of {the_project}.",
                    "hint".bold().cyan(),
                    ":".bold(),
                    package.cyan(),
                    dependency.cyan(),
                )
            }
            Self::DependsOnItself { package, workspace } => {
                let project = if *workspace {
                    "workspace member"
                } else {
                    "project"
                };
                write!(
                    f,
                    "{}{} The {project} `{}` depends on itself at an incompatible version. This is likely a mistake. If you intended to depend on a third-party package named `{}`, consider renaming the {project} `{}` to avoid creating a conflict.",
                    "hint".bold().cyan(),
                    ":".bold(),
                    package.cyan(),
                    package.cyan(),
                    package.cyan(),
                )
            }
            Self::UncheckedIndex {
                name,
                range,
                found_index,
                next_index,
            } => {
                write!(
                    f,
                    "{}{} `{}` was found on {}, but not at the requested version ({}). A compatible version may be available on a subsequent index (e.g., {}). By default, uv will only consider versions that are published on the first index that contains a given package, to avoid dependency confusion attacks. If all indexes are equally trusted, use `{}` to consider all versions from all indexes, regardless of the order in which they were defined.",
                    "hint".bold().cyan(),
                    ":".bold(),
                    name.cyan(),
                    found_index.without_credentials().cyan(),
                    PackageRange::compatibility(&PubGrubPackage::base(name), range, None).cyan(),
                    next_index.cyan(),
                    "--index-strategy unsafe-best-match".green(),
                )
            }
            Self::UnauthorizedIndex { index } => {
                write!(
                    f,
                    "{}{} An index URL ({}) could not be queried due to a lack of valid authentication credentials ({}).",
                    "hint".bold().cyan(),
                    ":".bold(),
                    index.without_credentials().cyan(),
                    "401 Unauthorized".red(),
                )
            }
            Self::ForbiddenIndex { index } => {
                write!(
                    f,
                    "{}{} An index URL ({}) returned a {} error. This could indicate lack of valid authentication credentials, or the package may not exist on this index.",
                    "hint".bold().cyan(),
                    ":".bold(),
                    index.without_credentials().cyan(),
                    "403 Forbidden".red(),
                )
            }
            Self::NoBuild { package, option } => {
                let option = match option {
                    NoBuild::All => "for all packages (i.e., with `--no-build`)".to_string(),
                    NoBuild::Packages(_) => {
                        format!("for `{package}` (i.e., with `--no-build-package {package}`)")
                    }
                    NoBuild::None => unreachable!(),
                };
                write!(
                    f,
                    "{}{} Wheels are required for `{}` because building from source is disabled {option}",
                    "hint".bold().cyan(),
                    ":".bold(),
                    package.cyan(),
                )
            }
            Self::NoBinary { package, option } => {
                let option = match option {
                    NoBinary::All => "for all packages (i.e., with `--no-binary`)".to_string(),
                    NoBinary::Packages(_) => {
                        format!("for `{package}` (i.e., with `--no-binary-package {package}`)")
                    }
                    NoBinary::None => unreachable!(),
                };
                write!(
                    f,
                    "{}{} A source distribution is required for `{}` because using pre-built wheels is disabled {option}",
                    "hint".bold().cyan(),
                    ":".bold(),
                    package.cyan(),
                )
            }
            Self::LanguageTags {
                package,
                version,
                tags,
                best,
            } => {
                if let Some(best) = best {
                    let s = if tags.len() == 1 { "" } else { "s" };
                    let best = if let Some(pretty) = best.pretty() {
                        format!("{} (`{}`)", pretty.cyan(), best.cyan())
                    } else {
                        format!("{}", best.cyan())
                    };
                    write!(
                        f,
                        "{}{} You require {}, but we only found wheels for `{}` ({}) with the following Python implementation tag{s}: {}",
                        "hint".bold().cyan(),
                        ":".bold(),
                        best,
                        package.cyan(),
                        format!("v{version}").cyan(),
                        tags.iter()
                            .map(|tag| format!("`{}`", tag.cyan()))
                            .join(", "),
                    )
                } else {
                    let s = if tags.len() == 1 { "" } else { "s" };
                    write!(
                        f,
                        "{}{} Wheels are available for `{}` ({}) with the following Python implementation tag{s}: {}",
                        "hint".bold().cyan(),
                        ":".bold(),
                        package.cyan(),
                        format!("v{version}").cyan(),
                        tags.iter()
                            .map(|tag| format!("`{}`", tag.cyan()))
                            .join(", "),
                    )
                }
            }
            Self::AbiTags {
                package,
                version,
                tags,
                best,
            } => {
                if let Some(best) = best {
                    let s = if tags.len() == 1 { "" } else { "s" };
                    let best = if let Some(pretty) = best.pretty() {
                        format!("{} (`{}`)", pretty.cyan(), best.cyan())
                    } else {
                        format!("{}", best.cyan())
                    };
                    write!(
                        f,
                        "{}{} You require {}, but we only found wheels for `{}` ({}) with the following Python ABI tag{s}: {}",
                        "hint".bold().cyan(),
                        ":".bold(),
                        best,
                        package.cyan(),
                        format!("v{version}").cyan(),
                        tags.iter()
                            .map(|tag| format!("`{}`", tag.cyan()))
                            .join(", "),
                    )
                } else {
                    let s = if tags.len() == 1 { "" } else { "s" };
                    write!(
                        f,
                        "{}{} Wheels are available for `{}` ({}) with the following Python ABI tag{s}: {}",
                        "hint".bold().cyan(),
                        ":".bold(),
                        package.cyan(),
                        format!("v{version}").cyan(),
                        tags.iter()
                            .map(|tag| format!("`{}`", tag.cyan()))
                            .join(", "),
                    )
                }
            }
            Self::PlatformTags {
                package,
                version,
                tags,
            } => {
                let s = if tags.len() == 1 { "" } else { "s" };
                write!(
                    f,
                    "{}{} Wheels are available for `{}` ({}) on the following platform{s}: {}",
                    "hint".bold().cyan(),
                    ":".bold(),
                    package.cyan(),
                    format!("v{version}").cyan(),
                    tags.iter()
                        .map(|tag| format!("`{}`", tag.cyan()))
                        .join(", "),
                )
            }
        }
    }
}

/// A [`Term`] and [`PubGrubPackage`] combination for display.
struct PackageTerm<'a> {
    package: &'a PubGrubPackage,
    term: &'a Term<Range<Version>>,
    formatter: &'a PubGrubReportFormatter<'a>,
}

impl std::fmt::Display for PackageTerm<'_> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match &self.term {
            Term::Positive(set) => {
                write!(f, "{}", self.formatter.compatible_range(self.package, set))
            }
            Term::Negative(set) => {
                if let Some(version) = set.as_singleton() {
                    // Note we do not handle the "root" package here but we should never
                    // be displaying that the root package is inequal to some version
                    let package = self.package;
                    write!(f, "{package}!={version}")
                } else {
                    write!(
                        f,
                        "{}",
                        self.formatter
                            .compatible_range(self.package, &set.complement())
                    )
                }
            }
        }
    }
}

impl PackageTerm<'_> {
    /// Create a new [`PackageTerm`] from a [`PubGrubPackage`] and a [`Term`].
    fn new<'a>(
        package: &'a PubGrubPackage,
        term: &'a Term<Range<Version>>,
        formatter: &'a PubGrubReportFormatter<'a>,
    ) -> PackageTerm<'a> {
        PackageTerm {
            package,
            term,
            formatter,
        }
    }
}

/// The kind of version ranges being displayed in [`PackageRange`]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PackageRangeKind {
    Dependency,
    Compatibility,
    Available,
}

/// A [`Range`] and [`PubGrubPackage`] combination for display.
#[derive(Debug)]
struct PackageRange<'a> {
    package: &'a PubGrubPackage,
    range: &'a Range<Version>,
    kind: PackageRangeKind,
    formatter: Option<&'a PubGrubReportFormatter<'a>>,
}

impl PackageRange<'_> {
    fn compatibility<'a>(
        package: &'a PubGrubPackage,
        range: &'a Range<Version>,
        formatter: Option<&'a PubGrubReportFormatter<'a>>,
    ) -> PackageRange<'a> {
        PackageRange {
            package,
            range,
            kind: PackageRangeKind::Compatibility,
            formatter,
        }
    }

    fn dependency<'a>(
        package: &'a PubGrubPackage,
        range: &'a Range<Version>,
        formatter: Option<&'a PubGrubReportFormatter<'a>>,
    ) -> PackageRange<'a> {
        PackageRange {
            package,
            range,
            kind: PackageRangeKind::Dependency,
            formatter,
        }
    }

    fn availability<'a>(
        package: &'a PubGrubPackage,
        range: &'a Range<Version>,
        formatter: Option<&'a PubGrubReportFormatter<'a>>,
    ) -> PackageRange<'a> {
        PackageRange {
            package,
            range,
            kind: PackageRangeKind::Available,
            formatter,
        }
    }

    /// Returns a boolean indicating if the predicate following this package range should
    /// be singular or plural e.g. if false use "<range> depends on <...>" and
    /// if true use "<range> depend on <...>"
    fn plural(&self) -> bool {
        // If a workspace member, always use the singular form (otherwise, it'd be "all versions of")
        if self
            .formatter
            .and_then(|formatter| formatter.format_workspace_member(self.package))
            .is_some()
        {
            return false;
        }

        let mut segments = self.range.iter();
        if let Some(segment) = segments.next() {
            // A single unbounded compatibility segment is always plural ("all versions of").
            if self.kind == PackageRangeKind::Compatibility {
                if matches!(segment, (Bound::Unbounded, Bound::Unbounded)) {
                    return true;
                }
            }
            // Otherwise, multiple segments are always plural.
            segments.next().is_some()
        } else {
            // An empty range is always singular.
            false
        }
    }
}

/// Create a range with improved segments for reporting the available versions for a package.
fn update_availability_range(
    range: &Range<Version>,
    available_versions: &BTreeSet<Version>,
) -> Range<Version> {
    let mut new_range = Range::empty();

    // Construct an available range to help guide simplification. Note this is not strictly correct,
    // as the available range should have many holes in it. However, for this use-case it should be
    // okay — we just may avoid simplifying some segments _inside_ the available range.
    let (available_range, first_available, last_available) =
        match (available_versions.first(), available_versions.last()) {
            // At least one version is available
            (Some(first), Some(last)) => {
                let range = Range::<Version>::from_range_bounds((
                    Bound::Included(first.clone()),
                    Bound::Included(last.clone()),
                ));
                // If only one version is available, return this as the bound immediately
                if first == last {
                    return range;
                }
                (range, first, last)
            }
            // SAFETY: If there's only a single item, `first` and `last` should both
            // return `Some`.
            (Some(_), None) | (None, Some(_)) => unreachable!(),
            // No versions are available; nothing to do
            (None, None) => return Range::empty(),
        };

    for segment in range.iter() {
        let (lower, upper) = segment;
        let segment_range = Range::from_range_bounds((lower.clone(), upper.clone()));

        // Drop the segment if it's disjoint with the available range, e.g., if the segment is
        // `foo>999`, and the available versions are all `<10` it's useless to show.
        if segment_range.is_disjoint(&available_range) {
            continue;
        }

        // Replace the segment if it's captured by the available range, e.g., if the segment is
        // `foo<1000` and the available versions are all `<10` we can simplify to `foo<10`.
        if available_range.subset_of(&segment_range) {
            // If the segment only has a lower or upper bound, only take the relevant part of the
            // available range. This avoids replacing `foo<100` with `foo>1,<2`, instead using
            // `foo<2` to avoid extra noise.
            if matches!(lower, Bound::Unbounded) {
                new_range = new_range.union(&Range::from_range_bounds((
                    Bound::Unbounded,
                    Bound::Included(last_available.clone()),
                )));
            } else if matches!(upper, Bound::Unbounded) {
                new_range = new_range.union(&Range::from_range_bounds((
                    Bound::Included(first_available.clone()),
                    Bound::Unbounded,
                )));
            } else {
                new_range = new_range.union(&available_range);
            }
            continue;
        }

        // If the bound is inclusive, and the version is _not_ available, change it to an exclusive
        // bound to avoid confusion, e.g., if the segment is `foo<=10` and the available versions
        // do not include `foo 10`, we should instead say `foo<10`.
        let lower = match lower {
            Bound::Included(version) if !available_versions.contains(version) => {
                Bound::Excluded(version.clone())
            }
            _ => (*lower).clone(),
        };
        let upper = match upper {
            Bound::Included(version) if !available_versions.contains(version) => {
                Bound::Excluded(version.clone())
            }
            _ => (*upper).clone(),
        };

        // Note this repeated-union construction is not particularly efficient, but there's not
        // better API exposed by PubGrub. Since we're just generating an error message, it's
        // probably okay, but we should investigate a better upstream API.
        new_range = new_range.union(&Range::from_range_bounds((lower, upper)));
    }

    new_range
}

impl std::fmt::Display for PackageRange<'_> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        // Exit early for the root package — the range is not meaningful
        if let Some(root) = self
            .formatter
            .and_then(|formatter| formatter.format_root(self.package))
        {
            return write!(f, "{root}");
        }
        // Exit early for workspace members, only a single version is available
        if let Some(member) = self
            .formatter
            .and_then(|formatter| formatter.format_workspace_member(self.package))
        {
            return write!(f, "{member}");
        }
        let package = self.package;

        if self.range.is_empty() {
            return write!(f, "{package} ∅");
        }

        let segments: Vec<_> = self.range.iter().collect();
        if segments.len() > 1 {
            match self.kind {
                PackageRangeKind::Dependency => write!(f, "one of:")?,
                PackageRangeKind::Compatibility => write!(f, "all of:")?,
                PackageRangeKind::Available => write!(f, "are available:")?,
            }
        }
        for (lower, upper) in &segments {
            if segments.len() > 1 {
                write!(f, "\n    ")?;
            }
            match (lower, upper) {
                (Bound::Unbounded, Bound::Unbounded) => match self.kind {
                    PackageRangeKind::Dependency => write!(f, "{package}")?,
                    PackageRangeKind::Compatibility => write!(f, "all versions of {package}")?,
                    PackageRangeKind::Available => write!(f, "{package}")?,
                },
                (Bound::Unbounded, Bound::Included(v)) => write!(f, "{package}<={v}")?,
                (Bound::Unbounded, Bound::Excluded(v)) => write!(f, "{package}<{v}")?,
                (Bound::Included(v), Bound::Unbounded) => write!(f, "{package}>={v}")?,
                (Bound::Included(v), Bound::Included(b)) => {
                    if v == b {
                        write!(f, "{package}=={v}")?;
                    } else {
                        write!(f, "{package}>={v},<={b}")?;
                    }
                }
                (Bound::Included(v), Bound::Excluded(b)) => {
                    if let Some(prefix) = PrefixMatch::from_range(lower, upper) {
                        write!(f, "{package}{prefix}")?;
                    } else {
                        write!(f, "{package}>={v},<{b}")?;
                    }
                }
                (Bound::Excluded(v), Bound::Unbounded) => write!(f, "{package}>{v}")?,
                (Bound::Excluded(v), Bound::Included(b)) => write!(f, "{package}>{v},<={b}")?,
                (Bound::Excluded(v), Bound::Excluded(b)) => write!(f, "{package}>{v},<{b}")?,
            }
        }
        if segments.len() > 1 {
            writeln!(f)?;
        }
        Ok(())
    }
}

impl PackageRange<'_> {
    fn depends_on<'a>(
        &'a self,
        package: &'a PubGrubPackage,
        range: &'a Range<Version>,
    ) -> DependsOn<'a> {
        DependsOn {
            package: self,
            dependency1: PackageRange {
                package,
                range,
                kind: PackageRangeKind::Dependency,
                formatter: self.formatter,
            },
            dependency2: None,
        }
    }
}

/// A representation of A depends on B (and C).
#[derive(Debug)]
struct DependsOn<'a> {
    package: &'a PackageRange<'a>,
    dependency1: PackageRange<'a>,
    dependency2: Option<PackageRange<'a>>,
}

impl<'a> DependsOn<'a> {
    /// Adds an additional dependency.
    ///
    /// Note this overwrites previous calls to `DependsOn::and`.
    fn and(mut self, package: &'a PubGrubPackage, range: &'a Range<Version>) -> Self {
        self.dependency2 = Some(PackageRange {
            package,
            range,
            kind: PackageRangeKind::Dependency,
            formatter: self.package.formatter,
        });
        self
    }
}

impl std::fmt::Display for DependsOn<'_> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", Padded::new("", self.package, " "))?;
        if self.package.plural() {
            write!(f, "depend on ")?;
        } else {
            write!(f, "depends on ")?;
        }

        match self.dependency2 {
            Some(ref dependency2) => write!(
                f,
                "{}and{}",
                Padded::new("", &self.dependency1, " "),
                Padded::new(" ", &dependency2, "")
            )?,
            None => write!(f, "{}", self.dependency1)?,
        }

        Ok(())
    }
}

/// Inserts the given padding on the left and right sides of the content if
/// the content does not start and end with whitespace respectively.
#[derive(Debug)]
struct Padded<'a, T: std::fmt::Display> {
    left: &'a str,
    content: &'a T,
    right: &'a str,
}

impl<'a, T: std::fmt::Display> Padded<'a, T> {
    fn new(left: &'a str, content: &'a T, right: &'a str) -> Self {
        Padded {
            left,
            content,
            right,
        }
    }
}

impl<'a> Padded<'a, String> {
    fn from_string(left: &'a str, content: &'a String, right: &'a str) -> Self {
        Padded {
            left,
            content,
            right,
        }
    }
}

impl<T: std::fmt::Display> std::fmt::Display for Padded<'_, T> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let mut result = String::new();
        let content = self.content.to_string();

        if let Some(char) = content.chars().next() {
            if !char.is_whitespace() {
                result.push_str(self.left);
            }
        }

        result.push_str(&content);

        if let Some(char) = content.chars().last() {
            if !char.is_whitespace() {
                result.push_str(self.right);
            }
        }

        write!(f, "{result}")
    }
}
