use std::borrow::Cow;
use std::cmp::Ordering;
use std::collections::{BTreeMap, BTreeSet};
use std::fmt::Write;
use std::ops::Bound;

use indexmap::IndexSet;
use itertools::Itertools;
use jiff::Timestamp;
use owo_colors::OwoColorize;
use pubgrub::{DerivationTree, Derived, External, Map, ReportFormatter, Term};
use reqwest::StatusCode;
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, VersionSpecifier, VersionSpecifiers};
use uv_pep508::{MarkerEnvironment, MarkerExpression, MarkerTree, MarkerValueVersion};
use uv_platform_tags::{AbiTag, IncompatibleTag, LanguageTag, PlatformTag, Tags};

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

type ReportDerived = Derived<PubGrubPackage, Range<Version>, UnavailableReason>;

#[derive(Debug)]
pub(crate) struct PubGrubReportFormatter<'a> {
    /// See [`crate::error::NoSolutionError::included_versions`].
    pub(crate) included_versions: &'a FxHashMap<PackageName, BTreeSet<Version>>,

    /// See [`crate::error::NoSolutionError::available_versions`].
    pub(crate) available_versions: &'a FxHashMap<PackageName, BTreeSet<Version>>,

    /// The Python requirement for the resolution.
    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>,
}

/// Render a PubGrub report without recursive tree traversal.
///
/// This preserves the output and shared-node reference behavior of
/// [`pubgrub::DefaultStringReporter`], whose recursive entry point is private.
pub(crate) fn report(
    derivation_tree: &ErrorTree,
    formatter: &PubGrubReportFormatter<'_>,
) -> String {
    match derivation_tree {
        DerivationTree::External(external) => formatter.format_external(external),
        DerivationTree::Derived(derived) => {
            let mut reporter = IterativeReporter::default();
            reporter.build(derived, formatter);
            reporter.lines.join("\n")
        }
    }
}

/// Accumulates the report state used by [`report`].
#[derive(Default)]
struct IterativeReporter {
    ref_count: usize,
    shared_with_ref: Map<usize, usize>,
    lines: Vec<String>,
}

impl IterativeReporter {
    fn build(&mut self, root: &ReportDerived, formatter: &PubGrubReportFormatter<'_>) {
        enum Task<'a> {
            Build(&'a ReportDerived),
            Finish(Option<usize>),
            AfterFirstDerived {
                current: &'a ReportDerived,
                first: &'a ReportDerived,
                second: &'a ReportDerived,
            },
            Emit(String),
        }

        let mut tasks = vec![Task::Build(root)];
        while let Some(task) = tasks.pop() {
            match task {
                Task::Build(current) => {
                    tasks.push(Task::Finish(current.shared_id));
                    match (current.cause1.as_ref(), current.cause2.as_ref()) {
                        (
                            DerivationTree::External(external1),
                            DerivationTree::External(external2),
                        ) => {
                            self.lines.push(formatter.explain_both_external(
                                external1,
                                external2,
                                &current.terms,
                            ));
                        }
                        (DerivationTree::Derived(derived), DerivationTree::External(external))
                        | (DerivationTree::External(external), DerivationTree::Derived(derived)) => {
                            if let Some(ref_id) = self.line_ref_of(derived.shared_id) {
                                self.lines.push(formatter.explain_ref_and_external(
                                    ref_id,
                                    derived,
                                    external,
                                    &current.terms,
                                ));
                            } else {
                                match (derived.cause1.as_ref(), derived.cause2.as_ref()) {
                                    (
                                        DerivationTree::Derived(prior_derived),
                                        DerivationTree::External(prior_external),
                                    )
                                    | (
                                        DerivationTree::External(prior_external),
                                        DerivationTree::Derived(prior_derived),
                                    ) => {
                                        tasks.push(Task::Emit(
                                            formatter.and_explain_prior_and_external(
                                                prior_external,
                                                external,
                                                &current.terms,
                                            ),
                                        ));
                                        tasks.push(Task::Build(prior_derived));
                                    }
                                    _ => {
                                        tasks.push(Task::Emit(
                                            formatter
                                                .and_explain_external(external, &current.terms),
                                        ));
                                        tasks.push(Task::Build(derived));
                                    }
                                }
                            }
                        }
                        (DerivationTree::Derived(derived1), DerivationTree::Derived(derived2)) => {
                            match (
                                self.line_ref_of(derived1.shared_id),
                                self.line_ref_of(derived2.shared_id),
                            ) {
                                (Some(ref1), Some(ref2)) => {
                                    self.lines.push(formatter.explain_both_ref(
                                        ref1,
                                        derived1,
                                        ref2,
                                        derived2,
                                        &current.terms,
                                    ));
                                }
                                (Some(ref1), None) => {
                                    tasks.push(Task::Emit(formatter.and_explain_ref(
                                        ref1,
                                        derived1,
                                        &current.terms,
                                    )));
                                    tasks.push(Task::Build(derived2));
                                }
                                (None, Some(ref2)) => {
                                    tasks.push(Task::Emit(formatter.and_explain_ref(
                                        ref2,
                                        derived2,
                                        &current.terms,
                                    )));
                                    tasks.push(Task::Build(derived1));
                                }
                                (None, None) => {
                                    tasks.push(Task::AfterFirstDerived {
                                        current,
                                        first: derived1,
                                        second: derived2,
                                    });
                                    tasks.push(Task::Build(derived1));
                                }
                            }
                        }
                    }
                }
                Task::Finish(shared_id) => {
                    if let Some(shared_id) = shared_id
                        && !self.shared_with_ref.contains_key(&shared_id)
                    {
                        self.add_line_ref();
                        self.shared_with_ref.insert(shared_id, self.ref_count);
                    }
                }
                Task::AfterFirstDerived {
                    current,
                    first,
                    second,
                } => {
                    if first.shared_id.is_some() {
                        self.lines.push(String::new());
                        tasks.push(Task::Build(current));
                    } else {
                        let ref_id = self.add_line_ref();
                        self.lines.push(String::new());
                        tasks.push(Task::Emit(formatter.and_explain_ref(
                            ref_id,
                            first,
                            &current.terms,
                        )));
                        tasks.push(Task::Build(second));
                    }
                }
                Task::Emit(line) => self.lines.push(line),
            }
        }
    }

    fn add_line_ref(&mut self) -> usize {
        self.ref_count += 1;
        if let Some(line) = self.lines.last_mut() {
            let _ = write!(line, " ({})", self.ref_count);
        }
        self.ref_count
    }

    fn line_ref_of(&self, shared_id: Option<usize>) -> Option<usize> {
        shared_id.and_then(|id| self.shared_with_ref.get(&id).copied())
    }
}

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 included 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(included_versions) = package.name().and_then(|name| self.included_versions.get(name)) {
                            update_availability_range(&complement, included_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 UnavailableReason::Version(UnavailableVersion::UnsatisfiableDependency(
                    requirement,
                )) = reason
                    && let Some(root) = self.format_root_requires(package)
                {
                    return format!("{root} {requirement}");
                }

                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(" ", &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(" ", &message, " "), context)
                            } else {
                                format!("{}{}", range, padded(" ", &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 dependency_set.is_empty()
                    && let Some(root) = self.format_root(package)
                {
                    return format!("{root} for {dependency} cannot be satisfied");
                }

                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_key(|&(package, _)| package);
        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("", &external, ", "),
            padded("", &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("", &derived1_terms, " "),
            ref_id2,
            padded("", &derived2_terms, ", "),
            padded("", &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("", &derived_terms, " "),
            padded("", &external, ", "),
            padded("", &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("", &external, ", "),
            padded("", &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("", &derived, ", "),
            padded("", &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("", &external, ", "),
            padded("", &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<Cow<'static, str>> {
        if self.is_workspace() {
            if matches!(&**package, PubGrubPackageInner::Root(_)) {
                if self.is_single_project_workspace() {
                    return Some(Cow::Borrowed("your project requires"));
                }
                return Some(Cow::Borrowed("your workspace requires"));
            }
        }
        match &**package {
            PubGrubPackageInner::Root(Some(name)) => Some(Cow::Owned(format!("{name} depends on"))),
            PubGrubPackageInner::Root(None) => Some(Cow::Borrowed("you require")),
            _ => 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<&'static str> {
        if self.is_workspace() {
            if matches!(&**package, PubGrubPackageInner::Root(_)) {
                if self.is_single_project_workspace() {
                    return Some("your project's requirements");
                }
                return Some("your workspace's requirements");
            }
        }
        match &**package {
            PubGrubPackageInner::Root(_) => Some("your requirements"),
            _ => 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<Cow<'static, str>> {
        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(Cow::Borrowed("your project"))
                } else {
                    Some(Cow::Owned(format!("{package}")))
                }
            }
            PubGrubPackageInner::Extra { name, .. } if self.workspace_members.contains(name) => {
                Some(Cow::Owned(format!("{package}")))
            }
            PubGrubPackageInner::Group { name, .. } if self.workspace_members.contains(name) => {
                Some(Cow::Owned(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, package_set2, dependency2, dependency_set2),
            ) if package1 == package2 && package_set1 == package_set2 => {
                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("", &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("", &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,
        current_environment: &MarkerEnvironment,
        tags: Option<&Tags>,
        workspace_members: &BTreeSet<PackageName>,
        options: &Options,
        inherited_exclude_newer_ranges: &FxHashMap<PackageName, Range<Version>>,
        output_hints: &mut IndexSet<PubGrubHint>,
    ) {
        // Check for disjoint target hints (only applicable to universal resolution).
        if let Some(markers) = env.fork_markers() {
            // TODO(konsti): This is a crude approximation to telling the user the difference
            // between their Python version and the relevant Python version range from the marker.
            let current_python_version = current_environment.python_version().version.clone();
            let current_python_marker = MarkerTree::expression(MarkerExpression::Version {
                key: MarkerValueVersion::PythonVersion,
                specifier: VersionSpecifier::equals_version(current_python_version.clone()),
            });
            if markers.is_disjoint(current_python_marker) {
                output_hints.insert(PubGrubHint::DisjointPythonVersion {
                    python_version: current_python_version,
                });
            } else if !markers.evaluate(current_environment, &[]) {
                output_hints.insert(PubGrubHint::DisjointEnvironment);
            }
        }

        let requested_ranges = requested_ranges(derivation_tree);

        let mut pending = vec![(derivation_tree, inherited_exclude_newer_ranges.clone())];
        while let Some((derivation_tree, inherited_exclude_newer_ranges)) = pending.pop() {
            match derivation_tree {
                DerivationTree::External(External::Custom(package, set, reason)) => {
                    if matches!(
                        reason,
                        UnavailableReason::Version(UnavailableVersion::UnsatisfiableDependency(_))
                    ) {
                        continue;
                    }

                    if let Some(name) = package.name_no_root() {
                        // Check for no versions due to pre-release options.
                        if !fork_urls.contains_key(name) {
                            self.prerelease_hint(
                                name,
                                set,
                                requested_ranges.get(name).map(Vec::as_slice),
                                selector,
                                env,
                                options,
                                output_hints,
                            );
                        }

                        // Check for no versions due to no `--find-links` flat index.
                        Self::index_hints(
                            name,
                            set,
                            self.included_versions.get(name),
                            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 !fork_urls.contains_key(name) {
                            self.prerelease_hint(
                                name,
                                set,
                                requested_ranges.get(name).map(Vec::as_slice),
                                selector,
                                env,
                                options,
                                output_hints,
                            );
                        }

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

                        let exclude_newer = if let Some(index) = fork_indexes.get(name) {
                            options
                                .exclude_newer
                                .exclude_newer_package_for_index_with_source(
                                    name,
                                    index_locations.exclude_newer_for(index.url()),
                                )
                        } else {
                            options
                                .exclude_newer
                                .exclude_newer_package(name)
                                .map(|exclude_newer| {
                                    let source = if options.exclude_newer.package.contains_key(name)
                                    {
                                        EffectiveExcludeNewerSource::Package
                                    } else {
                                        EffectiveExcludeNewerSource::Global
                                    };
                                    (exclude_newer, source)
                                })
                        };

                        if let Some((exclude_newer, source)) = exclude_newer {
                            // Check if there are no included versions in the requested
                            // range, but there are still available versions in that range
                            // (i.e., they were filtered out by `exclude-newer`).
                            let no_included_in_set = self
                                .included_versions
                                .get(name)
                                .is_none_or(|versions| !versions.iter().any(|v| set.contains(v)));
                            let available_has_versions_in_set = self
                                .available_versions
                                .get(name)
                                .is_some_and(|versions| versions.iter().any(|v| set.contains(v)));
                            if no_included_in_set && available_has_versions_in_set {
                                let version_hint_set =
                                    inherited_exclude_newer_ranges.get(name).map_or_else(
                                        || set.clone(),
                                        |exclude_newer_range| set.union(exclude_newer_range),
                                    );
                                let matching_version = self.exclude_newer_version_hint(
                                    name,
                                    &version_hint_set,
                                    index,
                                    fork_indexes,
                                );
                                output_hints.insert(PubGrubHint::ExcludeNewer {
                                    package: name.clone(),
                                    source,
                                    exclude_newer,
                                    matching_version,
                                });
                            }
                        }
                    }
                }
                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) => {
                    let cause1_exclude_newer_ranges =
                        Self::subtree_exclude_newer_ranges(&derived.cause1);
                    let cause2_exclude_newer_ranges =
                        Self::subtree_exclude_newer_ranges(&derived.cause2);

                    let mut cause1_inherited_exclude_newer_ranges =
                        inherited_exclude_newer_ranges.clone();
                    for (name, range) in &cause2_exclude_newer_ranges {
                        cause1_inherited_exclude_newer_ranges
                            .entry(name.clone())
                            .and_modify(|existing| *existing = existing.union(range))
                            .or_insert_with(|| range.clone());
                    }

                    let mut cause2_inherited_exclude_newer_ranges = inherited_exclude_newer_ranges;
                    for (name, range) in &cause1_exclude_newer_ranges {
                        cause2_inherited_exclude_newer_ranges
                            .entry(name.clone())
                            .and_modify(|existing| *existing = existing.union(range))
                            .or_insert_with(|| range.clone());
                    }

                    pending.push((&derived.cause2, cause2_inherited_exclude_newer_ranges));
                    pending.push((&derived.cause1, cause1_inherited_exclude_newer_ranges));
                }
            }
        }
    }

    /// Collect the version ranges in `derivation_tree` that were excluded solely by
    /// `exclude-newer`, grouped by package name.
    fn subtree_exclude_newer_ranges(
        derivation_tree: &ErrorTree,
    ) -> FxHashMap<PackageName, Range<Version>> {
        let mut exclude_newer_ranges: FxHashMap<PackageName, Range<Version>> = FxHashMap::default();
        let mut trees = vec![derivation_tree];
        while let Some(derivation_tree) = trees.pop() {
            match derivation_tree {
                DerivationTree::External(External::Custom(package, versions, reason)) => {
                    if matches!(
                        reason,
                        UnavailableReason::Version(UnavailableVersion::IncompatibleDist(
                            IncompatibleDist::Wheel(IncompatibleWheel::ExcludeNewer(_))
                                | IncompatibleDist::Source(IncompatibleSource::ExcludeNewer(_))
                        ))
                    ) {
                        if let Some(name) = package.name() {
                            exclude_newer_ranges
                                .entry(name.clone())
                                .and_modify(|set| *set = set.union(versions))
                                .or_insert_with(|| versions.clone());
                        }
                    }
                }
                DerivationTree::External(_) => {}
                DerivationTree::Derived(derived) => {
                    trees.push(&derived.cause2);
                    trees.push(&derived.cause1);
                }
            }
        }
        exclude_newer_ranges
    }

    /// Return the latest version in `set` that is available for resolver error reporting,
    /// along with the earliest known publish date for that version.
    fn exclude_newer_version_hint(
        &self,
        name: &PackageName,
        set: &Range<Version>,
        index: &InMemoryIndex,
        fork_indexes: &ForkIndexes,
    ) -> Option<ExcludeNewerVersionDetail> {
        let version = self.available_versions.get(name).and_then(|versions| {
            versions
                .iter()
                .rfind(|version| set.contains(version))
                .cloned()
        })?;

        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 publish_date = version_maps
            .iter()
            .filter_map(|version_map| {
                version_map.get(&version).and_then(|prioritized| {
                    prioritized
                        .files()
                        .filter_map(|file| file.upload_time_utc_ms)
                        .min()
                })
            })
            .min()
            .and_then(|upload_time| {
                Some(
                    jiff::Timestamp::from_millisecond(upload_time)
                        .ok()?
                        .to_string(),
                )
            });

        Some(ExcludeNewerVersionDetail {
            version,
            publish_date,
            singleton: set.as_singleton().is_some(),
        })
    }

    /// 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::FreethreadedAbi
            | 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>,
        listed: Option<&BTreeSet<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::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::Network(status)) => {
                hints.insert(PubGrubHint::InvalidPackageNetwork {
                    package: name.clone(),
                    status: *status,
                });
            }
            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(),
                            });
                        }
                        MetadataUnavailable::Network(status) => {
                            hints.insert(PubGrubHint::InvalidVersionNetwork {
                                package: name.clone(),
                                version: version.clone(),
                                status: *status,
                            });
                        }
                    }
                    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 when the index listed no version at all. This is an
            // unusual but valid case in which a package returns a 200 response, but without any
            // versions or distributions for the package. A package that listed versions and had
            // none of them work is the case the hint exists for, and its set covers them all.
            if listed.is_some_and(|listed| !listed.is_empty()) {
                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(),
                    any_successful_response: available_indexes
                        .values()
                        .any(|indexes| indexes.contains(&index.url)),
                });
            }
        }
    }

    /// Generate a [`PubGrubHint`] for a package whose pre-releases were not considered.
    ///
    /// A pre-release marker is only visible in `requested`, the ranges the package was requested
    /// with. The bounds of the derived `set` land on whichever versions the registry lists next,
    /// pre-release or not, since the resolver widens version sets across the gaps between the
    /// known versions of a package.
    fn prerelease_hint(
        &self,
        name: &PackageName,
        set: &Range<Version>,
        requested: Option<&[&Range<Version>]>,
        selector: &CandidateSelector,
        env: &ResolverEnvironment,
        options: &Options,
        hints: &mut IndexSet<PubGrubHint>,
    ) {
        if selector.prerelease_strategy().selection(name, env) != PrereleaseSelection::Disallow {
            return;
        }

        let prerelease_request = requested
            .unwrap_or_default()
            .iter()
            .copied()
            .find(|range| requests_prerelease(range));

        if let Some(range) = prerelease_request {
            // A pre-release marker appeared in the version requirements.
            match options.flexibility {
                Flexibility::Configurable => {
                    hints.insert(PubGrubHint::PrereleaseRequested {
                        name: name.clone(),
                        range: range.clone(),
                        package_override: options.prerelease.package.contains_key(name),
                    });
                }
                Flexibility::Fixed => {
                    hints.insert(PubGrubHint::BuildPrereleaseRequested {
                        name: name.clone(),
                        range: range.clone(),
                    });
                }
            }
        } else if let Some(version) = self.included_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.
            match options.flexibility {
                Flexibility::Configurable => {
                    hints.insert(PubGrubHint::PrereleaseAvailable {
                        package: name.clone(),
                        version: version.clone(),
                        package_override: options.prerelease.package.contains_key(name),
                    });
                }
                Flexibility::Fixed => {
                    hints.insert(PubGrubHint::BuildPrereleaseAvailable {
                        package: name.clone(),
                        version: version.clone(),
                    });
                }
            }
        }
    }
}

/// Collect the version ranges each package was requested with anywhere in the derivation tree.
///
/// The depended-on side of a dependency incompatibility is the range as written in the
/// requirement, unlike the sets the resolver derives from it.
fn requested_ranges(derivation_tree: &ErrorTree) -> FxHashMap<&PackageName, Vec<&Range<Version>>> {
    let mut requested: FxHashMap<&PackageName, Vec<&Range<Version>>> = FxHashMap::default();
    let mut pending = vec![derivation_tree];
    while let Some(derivation_tree) = pending.pop() {
        match derivation_tree {
            DerivationTree::External(External::FromDependencyOf(_, _, dependency, versions)) => {
                if let Some(name) = dependency.name_no_root() {
                    requested.entry(name).or_default().push(versions);
                }
            }
            DerivationTree::External(_) => {}
            DerivationTree::Derived(derived) => {
                pending.push(&derived.cause1);
                pending.push(&derived.cause2);
            }
        }
    }
    requested
}

/// Return `true` if a requested range includes a pre-release version explicitly.
fn requests_prerelease(range: &Range<Version>) -> bool {
    range.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() && !is_compatible_release_upper_bound(version)
            }
            Bound::Unbounded => false,
        };
        if is_pre2 {
            return true;
        }

        false
    })
}

/// Return `true` for the excluded `.dev0` upper bounds used to desugar compatible releases.
///
/// For example, `~=3.6` becomes `>=3.6,<4.dev0`. The `<4.dev0` boundary preserves PEP 440's
/// ordering semantics, but it does not mean the user requested pre-releases.
fn is_compatible_release_upper_bound(version: &Version) -> bool {
    version.dev() == Some(0) && !version.is_pre() && !version.is_post() && !version.is_local()
}

#[derive(Debug, Clone)]
pub struct ExcludeNewerVersionDetail {
    version: Version,
    publish_date: Option<String>,
    singleton: bool,
}

#[derive(Debug, Clone)]
pub 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,
        // excluded from `PartialEq` and `Hash`
        package_override: bool,
    },
    /// The resolver runs with fixed options (e.g., for build environments) and requires explicit
    /// pre-release opt-in for a package that only has pre-releases available.
    BuildPrereleaseAvailable {
        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>,
        // excluded from `PartialEq` and `Hash`
        package_override: bool,
    },
    /// A requirement included a pre-release marker, but the resolver runs with fixed options
    /// (e.g., for build environments) and cannot enable pre-releases automatically.
    BuildPrereleaseRequested {
        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,
    },
    /// The package metadata could not be fetched due to a network error.
    InvalidPackageNetwork {
        package: PackageName,
        // excluded from `PartialEq` and `Hash`
        status: StatusCode,
    },
    /// 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 package metadata could not be fetched due to a network error.
    InvalidVersionNetwork {
        package: PackageName,
        // excluded from `PartialEq` and `Hash`
        version: Version,
        // excluded from `PartialEq` and `Hash`
        status: StatusCode,
    },
    /// 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,
        // excluded from `PartialEq` and `Hash`
        any_successful_response: bool,
    },
    /// 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>,
    },
    /// Versions of a package were excluded by `exclude-newer`.
    ExcludeNewer {
        package: PackageName,
        source: EffectiveExcludeNewerSource,
        // excluded from `PartialEq` and `Hash`
        exclude_newer: Timestamp,
        // excluded from `PartialEq` and `Hash`
        matching_version: Option<ExcludeNewerVersionDetail>,
    },
    /// The resolution failed for a Python version that is different from the current Python version.
    DisjointPythonVersion {
        // excluded from `PartialEq` and `Hash`
        python_version: Version,
    },
    /// The resolution failed for an environment that is different from the current environment.
    DisjointEnvironment,
}

/// 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,
    },
    BuildPrereleaseAvailable {
        package: PackageName,
    },
    PrereleaseRequested {
        package: PackageName,
    },
    BuildPrereleaseRequested {
        package: PackageName,
    },
    NoIndex,
    Offline,
    InvalidPackageMetadata {
        package: PackageName,
    },
    InvalidPackageStructure {
        package: PackageName,
    },
    InvalidPackageNetwork {
        package: PackageName,
    },
    InvalidVersionMetadata {
        package: PackageName,
    },
    InconsistentVersionMetadata {
        package: PackageName,
    },
    InvalidVersionStructure {
        package: PackageName,
    },
    InvalidVersionNetwork {
        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,
    },
    ExcludeNewer {
        package: PackageName,
        source: EffectiveExcludeNewerSource,
    },
    DisjointPythonVersion,
    DisjointEnvironment,
}

impl From<PubGrubHint> for PubGrubHintCore {
    #[inline]
    fn from(hint: PubGrubHint) -> Self {
        match hint {
            PubGrubHint::PrereleaseAvailable { package, .. } => {
                Self::PrereleaseAvailable { package }
            }
            PubGrubHint::BuildPrereleaseAvailable { package, .. } => {
                Self::BuildPrereleaseAvailable { package }
            }
            PubGrubHint::PrereleaseRequested { name: package, .. } => {
                Self::PrereleaseRequested { package }
            }
            PubGrubHint::BuildPrereleaseRequested { name: package, .. } => {
                Self::BuildPrereleaseRequested { package }
            }
            PubGrubHint::NoIndex => Self::NoIndex,
            PubGrubHint::Offline => Self::Offline,
            PubGrubHint::InvalidPackageMetadata { package, .. } => {
                Self::InvalidPackageMetadata { package }
            }
            PubGrubHint::InvalidPackageStructure { package, .. } => {
                Self::InvalidPackageStructure { package }
            }
            PubGrubHint::InvalidPackageNetwork { package, .. } => {
                Self::InvalidPackageNetwork { package }
            }
            PubGrubHint::InvalidVersionMetadata { package, .. } => {
                Self::InvalidVersionMetadata { package }
            }
            PubGrubHint::InconsistentVersionMetadata { package, .. } => {
                Self::InconsistentVersionMetadata { package }
            }
            PubGrubHint::InvalidVersionStructure { package, .. } => {
                Self::InvalidVersionStructure { package }
            }
            PubGrubHint::InvalidVersionNetwork { package, .. } => {
                Self::InvalidVersionNetwork { 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 },
            PubGrubHint::ExcludeNewer {
                package, source, ..
            } => Self::ExcludeNewer { package, source },
            PubGrubHint::DisjointPythonVersion { .. } => Self::DisjointPythonVersion,
            PubGrubHint::DisjointEnvironment => Self::DisjointEnvironment,
        }
    }
}

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,
                package_override,
            } => {
                let argument = if *package_override {
                    format!("--prerelease-package {package}=allow")
                } else {
                    "--prerelease=allow".to_string()
                };
                write!(
                    f,
                    "Pre-releases are available for `{}` in the requested range (e.g., {}), but pre-releases weren't enabled (try: `{}`)",
                    package.cyan(),
                    version.cyan(),
                    argument.green(),
                )
            }
            Self::BuildPrereleaseAvailable { package, version } => {
                let spec = format!("{package}>={version}");
                write!(
                    f,
                    "Only pre-releases of `{}` (e.g., {}) match these build requirements, and build environments can't enable pre-releases automatically. Add `{}` to `build-system.requires`, `[tool.uv.extra-build-dependencies]`, or supply it via `uv build --build-constraint`.",
                    package.cyan(),
                    version.cyan(),
                    spec.cyan(),
                )
            }
            Self::PrereleaseRequested {
                name,
                range,
                package_override,
            } => {
                let argument = if *package_override {
                    format!("--prerelease-package {name}=allow")
                } else {
                    "--prerelease=allow".to_string()
                };
                write!(
                    f,
                    "`{}` was requested with a pre-release marker (e.g., {}), but pre-releases weren't enabled (try: `{}`)",
                    name.cyan(),
                    PackageRange::compatibility(&PubGrubPackage::base(name.clone()), range, None)
                        .cyan(),
                    argument.green(),
                )
            }
            Self::BuildPrereleaseRequested { name, range } => {
                write!(
                    f,
                    "`{}` was requested with a pre-release marker (e.g., {}), but build environments can't opt into pre-releases automatically.  Add `{}` to `build-system.requires`, `[tool.uv.extra-build-dependencies]`, or supply it via `uv build --build-constraint`.",
                    name.cyan(),
                    PackageRange::compatibility(&PubGrubPackage::base(name.clone()), range, None)
                        .cyan(),
                    PackageRange::compatibility(&PubGrubPackage::base(name.clone()), range, None)
                        .cyan(),
                )
            }
            Self::NoIndex => {
                write!(
                    f,
                    "Packages were unavailable because index lookups were disabled and no additional package locations were provided (try: `{}`)",
                    "--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.",
                )
            }
            Self::InvalidPackageMetadata { package, reason } => {
                write!(
                    f,
                    "Metadata for `{}` could not be parsed.\n{}",
                    package.cyan(),
                    textwrap::indent(reason.to_string().as_str(), "  ")
                )
            }
            Self::InvalidPackageStructure { package, reason } => {
                write!(
                    f,
                    "The structure of `{}` was invalid\n{}",
                    package.cyan(),
                    textwrap::indent(reason.to_string().as_str(), "  ")
                )
            }
            Self::InvalidPackageNetwork { package, status } => {
                write!(
                    f,
                    "Metadata for `{}` could not be fetched; the server returned: `{}`",
                    package.cyan(),
                    format!("{status}").red(),
                )
            }
            Self::InvalidVersionMetadata {
                package,
                version,
                reason,
            } => {
                write!(
                    f,
                    "Metadata for `{}` ({}) could not be parsed:\n{}",
                    package.cyan(),
                    format!("v{version}").cyan(),
                    textwrap::indent(reason, "  ")
                )
            }
            Self::InvalidVersionStructure {
                package,
                version,
                reason,
            } => {
                write!(
                    f,
                    "The structure of `{}` ({}) was invalid:\n{}",
                    package.cyan(),
                    format!("v{version}").cyan(),
                    textwrap::indent(reason, "  ")
                )
            }
            Self::InvalidVersionNetwork {
                package,
                version,
                status,
            } => {
                write!(
                    f,
                    "Metadata for `{}` ({}) could not be fetched; the server returned: `{}`",
                    package.cyan(),
                    format!("v{version}").cyan(),
                    format!("{status}").red(),
                )
            }
            Self::InconsistentVersionMetadata {
                package,
                version,
                reason,
            } => {
                write!(
                    f,
                    "Metadata for `{}` ({}) was inconsistent:\n{}",
                    package.cyan(),
                    format!("v{version}").cyan(),
                    textwrap::indent(reason, "  ")
                )
            }
            Self::RequiresPython {
                source: PythonRequirementSource::RequiresPython,
                requires_python,
                name,
                package_set,
                package_requires_python,
            } => {
                let package = PubGrubPackage::base(name.clone());
                let package_range = PackageRange::compatibility(&package, package_set, None);
                let supports = if package_range.plural() {
                    "support"
                } else {
                    "supports"
                };
                write!(
                    f,
                    "The `requires-python` value ({}) includes Python versions that are not supported by your dependencies (e.g., {} only {} {}). Consider using a more restrictive `requires-python` value (like {}).",
                    requires_python.cyan(),
                    package_range.cyan(),
                    supports,
                    package_requires_python.cyan(),
                    package_requires_python.cyan(),
                )
            }
            Self::RequiresPython {
                source: PythonRequirementSource::PythonVersion,
                requires_python,
                name,
                package_set,
                package_requires_python,
            } => {
                let package = PubGrubPackage::base(name.clone());
                let package_range = PackageRange::compatibility(&package, package_set, None);
                let supports = if package_range.plural() {
                    "support"
                } else {
                    "supports"
                };
                write!(
                    f,
                    "The `--python-version` value ({}) includes Python versions that are not supported by your dependencies (e.g., {} only {} {}). Consider using a higher `--python-version` value.",
                    requires_python.cyan(),
                    package_range.cyan(),
                    supports,
                    package_requires_python.cyan(),
                )
            }
            Self::RequiresPython {
                source: PythonRequirementSource::Interpreter,
                requires_python: _,
                name,
                package_set,
                package_requires_python,
            } => {
                let package = PubGrubPackage::base(name.clone());
                let package_range = PackageRange::compatibility(&package, package_set, None);
                let supports = if package_range.plural() {
                    "support"
                } else {
                    "supports"
                };
                write!(
                    f,
                    "The Python interpreter uses a Python version that is not supported by your dependencies (e.g., {} only {} {}). Consider passing a `--python-version` value to raise the minimum supported version.",
                    package_range.cyan(),
                    supports,
                    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.",
                    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}.",
                    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.",
                    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.",
                    name.cyan(),
                    found_index.without_credentials().cyan(),
                    PackageRange::compatibility(&PubGrubPackage::base(name.clone()), 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 ({})",
                    index.without_credentials().cyan(),
                    "401 Unauthorized".red(),
                )
            }
            Self::ForbiddenIndex {
                index,
                any_successful_response,
            } => {
                if *any_successful_response {
                    write!(
                        f,
                        "An index ({}) returned a {} error, but uv received a successful response from another request to the index. If the failing package is not present on this index, consider adding `ignore-error-codes = [403]` to the index's `[[tool.uv.index]]` entry to continue searching across indexes.",
                        index.without_credentials().cyan(),
                        "403 Forbidden".red(),
                    )
                } else {
                    write!(
                        f,
                        "An index ({}) returned a {} error. Check that the index URL is correct and the credentials are valid.",
                        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}",
                    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}",
                    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}: {}",
                        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}: {}",
                        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}: {}",
                        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}: {}",
                        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}: {}",
                    package.cyan(),
                    format!("v{version}").cyan(),
                    tags.iter()
                        .map(|tag| format!("`{}`", tag.cyan()))
                        .join(", "),
                )
            }
            Self::ExcludeNewer {
                package,
                source,
                exclude_newer,
                matching_version,
            } => {
                let latest = match matching_version {
                    Some(ExcludeNewerVersionDetail {
                        version,
                        publish_date: Some(publish_date),
                        singleton: true,
                    }) => format!(
                        " The requested version, {}, was published at {}.",
                        format!("v{version}").cyan(),
                        publish_date.cyan()
                    ),
                    Some(ExcludeNewerVersionDetail {
                        version: _,
                        publish_date: None,
                        singleton: true,
                    }) => String::new(),
                    Some(ExcludeNewerVersionDetail {
                        version,
                        publish_date: Some(publish_date),
                        singleton: false,
                    }) => format!(
                        " The latest version satisfying the requirement is {}, published at {}.",
                        format!("v{version}").cyan(),
                        publish_date.cyan()
                    ),
                    Some(ExcludeNewerVersionDetail {
                        version,
                        publish_date: None,
                        singleton: false,
                    }) => format!(
                        " The latest version satisfying the requirement is {}.",
                        format!("v{version}").cyan()
                    ),
                    None => String::new(),
                };
                match source {
                    EffectiveExcludeNewerSource::Package => write!(
                        f,
                        "`{}` was filtered by `{}` to only include packages uploaded \
                        before {}.{latest} Consider removing the setting or updating it to a later date.",
                        package.cyan(),
                        "exclude-newer-package".green(),
                        exclude_newer.cyan(),
                    ),
                    EffectiveExcludeNewerSource::Global => write!(
                        f,
                        "`{}` was filtered by `{}` to only include packages uploaded \
                        before {}.{latest} Consider using `{}` to override the cutoff for this package.",
                        package.cyan(),
                        "exclude-newer".green(),
                        exclude_newer.cyan(),
                        "exclude-newer-package".green(),
                    ),
                    EffectiveExcludeNewerSource::Index => write!(
                        f,
                        "`{}` was filtered by the index-specific `{}` setting to only include \
                        packages uploaded before {}.{latest} Consider updating that index's cutoff, setting \
                        it to `false`, or using `{}` to override the cutoff for this package.",
                        package.cyan(),
                        "exclude-newer".green(),
                        exclude_newer.cyan(),
                        "exclude-newer-package".green(),
                    ),
                }
            }
            Self::DisjointPythonVersion { python_version } => {
                write!(
                    f,
                    "While the active Python version is {}, \
                    the resolution failed for other Python versions supported by your \
                    project. Consider limiting your project's supported Python versions \
                    using `requires-python`.",
                    python_version.cyan(),
                )
            }
            Self::DisjointEnvironment => {
                write!(
                    f,
                    "The resolution failed for an environment that is not the current one, \
                    consider limiting the environments with `tool.uv.environments`.",
                )
            }
        }
    }
}

/// 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> {
    /// Whether a (normalized) version is contained in a set of versions.
    ///
    /// Unfortunately, we need to normalize the version because when we extract it from the range it
    /// may have `min` or `max` set but the values in `available_versions` will never have `min` or
    /// `max`.
    fn version_contained_in(version: &Version, versions: &BTreeSet<Version>) -> bool {
        if versions.contains(version) {
            return true;
        }

        // It's a little unfortunate we perform a clone here and throw away the value, but the
        // performance implications during an error report seem negligible and it makes the
        // calling code simpler.
        let version = version.clone().with_min(None).with_max(None);
        versions.contains(&version)
    }

    // 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(),
        };

    range
        .iter()
        .filter_map(|(lower, upper)| {
            let segment_range = Range::from_range_bounds((lower.cloned(), upper.cloned()));

            // 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) {
                return None;
            }

            // 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) {
                    return Some((Bound::Unbounded, Bound::Included(last_available.clone())));
                } else if matches!(upper, Bound::Unbounded) {
                    return Some((Bound::Included(first_available.clone()), Bound::Unbounded));
                }
                return Some((
                    Bound::Included(first_available.clone()),
                    Bound::Included(last_available.clone()),
                ));
            }

            // 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 !version_contained_in(version, available_versions) => {
                    Bound::Excluded(version.clone())
                }
                _ => lower.cloned(),
            };
            let upper = match upper {
                Bound::Included(version) if !version_contained_in(version, available_versions) => {
                    Bound::Excluded(version.clone())
                }
                _ => upper.cloned(),
            };

            Some((lower, upper))
        })
        .collect()
}

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("", 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("", &self.dependency1, " "),
                padded(" ", &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.
fn padded<'a, T: std::fmt::Display + ?Sized>(
    left: &'a str,
    content: &'a T,
    right: &'a str,
) -> impl std::fmt::Display + 'a {
    std::fmt::from_fn(move |f| {
        let content = content.to_string();

        if let Some(char) = content.chars().next() {
            if !char.is_whitespace() {
                f.write_str(left)?;
            }
        }

        f.write_str(&content)?;

        if let Some(char) = content.chars().last() {
            if !char.is_whitespace() {
                f.write_str(right)?;
            }
        }

        Ok(())
    })
}

#[cfg(test)]
mod tests {
    use pubgrub::{DefaultStringReporter, Reporter};
    use uv_distribution_types::RequiresPython;
    use uv_pep508::{MarkerEnvironment, MarkerEnvironmentBuilder};

    use super::*;

    fn derived(
        cause1: DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
        cause2: DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
        shared_id: Option<usize>,
    ) -> DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason> {
        DerivationTree::Derived(Derived {
            terms: Map::default(),
            shared_id,
            cause1: cause1.into(),
            cause2: cause2.into(),
        })
    }

    struct FormatterFixture {
        included_versions: FxHashMap<PackageName, BTreeSet<Version>>,
        available_versions: FxHashMap<PackageName, BTreeSet<Version>>,
        python_requirement: PythonRequirement,
        workspace_members: BTreeSet<PackageName>,
    }

    impl FormatterFixture {
        fn new() -> Self {
            let marker_environment = MarkerEnvironment::try_from(MarkerEnvironmentBuilder {
                implementation_name: "cpython",
                implementation_version: "3.12.0",
                os_name: "posix",
                platform_machine: "x86_64",
                platform_python_implementation: "CPython",
                platform_release: "",
                platform_system: "Linux",
                platform_version: "",
                python_full_version: "3.12.0",
                python_version: "3.12",
                sys_platform: "linux",
            })
            .expect("valid marker environment");
            Self {
                included_versions: FxHashMap::default(),
                available_versions: FxHashMap::default(),
                python_requirement: PythonRequirement::from_marker_environment(
                    &marker_environment,
                    RequiresPython::greater_than_equal_version(&Version::new([3_u64, 12])),
                ),
                workspace_members: BTreeSet::default(),
            }
        }

        fn formatter(&self) -> PubGrubReportFormatter<'_> {
            PubGrubReportFormatter {
                included_versions: &self.included_versions,
                available_versions: &self.available_versions,
                python_requirement: &self.python_requirement,
                workspace_members: &self.workspace_members,
                tags: None,
            }
        }
    }

    #[test]
    fn iterative_reporter_matches_pubgrub_for_shared_nodes() {
        let fixture = FormatterFixture::new();
        let formatter = fixture.formatter();
        let package = PubGrubPackage::from(PubGrubPackageInner::Root(None));
        let external1 =
            DerivationTree::External(External::NotRoot(package.clone(), Version::new([1_u64])));
        let external2 =
            DerivationTree::External(External::NotRoot(package.clone(), Version::new([2_u64])));
        let external3 = DerivationTree::External(External::NotRoot(package, Version::new([3_u64])));
        let shared = derived(external1.clone(), external2.clone(), Some(1));
        let unshared = derived(external2.clone(), external3.clone(), None);

        let trees = [
            derived(shared.clone(), external3.clone(), None),
            derived(external3.clone(), shared.clone(), None),
            derived(shared.clone(), unshared, None),
            derived(shared.clone(), shared, None),
        ];

        for tree in trees {
            assert_eq!(
                report(&tree, &formatter),
                DefaultStringReporter::report_with_formatter(&tree, &formatter)
            );
        }
    }

    #[test]
    fn formats_deep_derivation_tree_without_recursion() -> std::io::Result<()> {
        let thread = std::thread::Builder::new()
            .stack_size(256 * 1024)
            .spawn(|| {
                let fixture = FormatterFixture::new();
                let formatter = fixture.formatter();
                let package = PubGrubPackage::from(PubGrubPackageInner::Root(None));
                let leaf =
                    DerivationTree::External(External::NotRoot(package, Version::new([1_u64])));
                let mut tree = leaf.clone();
                for _ in 0..100_000 {
                    tree = derived(tree, leaf.clone(), None);
                }
                let _report = report(&tree, &formatter);
                crate::error::drop_derivation_tree(tree);
            })?;

        assert!(thread.join().is_ok());
        Ok(())
    }
}
