use std::hash::BuildHasherDefault;

use petgraph::{
    graph::{Graph, NodeIndex},
    Directed,
};
use rustc_hash::{FxHashMap, FxHashSet};

use distribution_types::{
    Dist, DistributionMetadata, Name, ResolutionDiagnostic, VersionId, VersionOrUrlRef,
};
use pep440_rs::{Version, VersionSpecifier};
use pep508_rs::{MarkerEnvironment, MarkerTree};
use pypi_types::{ParsedUrlError, Yanked};
use uv_git::GitResolver;
use uv_normalize::{ExtraName, PackageName};

use crate::preferences::Preferences;
use crate::pubgrub::{PubGrubDistribution, PubGrubPackageInner};
use crate::redirect::url_to_precise;
use crate::resolution::AnnotatedDist;
use crate::resolver::Resolution;
use crate::{InMemoryIndex, Manifest, MetadataResponse, ResolveError, VersionsResponse};

/// A complete resolution graph in which every node represents a pinned package and every edge
/// represents a dependency between two pinned packages.
#[derive(Debug)]
pub struct ResolutionGraph {
    /// The underlying graph.
    pub(crate) petgraph: Graph<AnnotatedDist, Version, Directed>,
    /// Any diagnostics that were encountered while building the graph.
    pub(crate) diagnostics: Vec<ResolutionDiagnostic>,
}

impl ResolutionGraph {
    /// Create a new graph from the resolved PubGrub state.
    #[allow(clippy::too_many_arguments)]
    pub(crate) fn from_state(
        index: &InMemoryIndex,
        preferences: &Preferences,
        git: &GitResolver,
        resolution: Resolution,
    ) -> anyhow::Result<Self, ResolveError> {
        // Collect all marker expressions from relevant pubgrub packages.
        let mut markers: FxHashMap<(&PackageName, &Version, &Option<ExtraName>), MarkerTree> =
            FxHashMap::default();
        for (package, versions) in &resolution.packages {
            if let PubGrubPackageInner::Package {
                name,
                marker: Some(marker),
                extra,
                ..
            } = &**package
            {
                for version in versions {
                    markers
                        .entry((name, version, extra))
                        .or_insert_with(|| MarkerTree::Or(vec![]))
                        .or(marker.clone());
                }
            }
        }

        // Add every package to the graph.
        let mut petgraph: Graph<AnnotatedDist, Version, Directed> =
            Graph::with_capacity(resolution.packages.len(), resolution.packages.len());
        let mut inverse: FxHashMap<(&PackageName, &Version, &Option<ExtraName>), NodeIndex<u32>> =
            FxHashMap::with_capacity_and_hasher(
                resolution.packages.len(),
                BuildHasherDefault::default(),
            );
        let mut diagnostics = Vec::new();

        for (package, versions) in &resolution.packages {
            for version in versions {
                match &**package {
                    PubGrubPackageInner::Package {
                        name,
                        extra,
                        marker: None,
                        url: None,
                    } => {
                        // Create the distribution.
                        let dist = resolution
                            .pins
                            .get(name, version)
                            .expect("Every package should be pinned")
                            .clone();

                        // Track yanks for any registry distributions.
                        match dist.yanked() {
                            None | Some(Yanked::Bool(false)) => {}
                            Some(Yanked::Bool(true)) => {
                                diagnostics.push(ResolutionDiagnostic::YankedVersion {
                                    dist: dist.clone(),
                                    reason: None,
                                });
                            }
                            Some(Yanked::Reason(reason)) => {
                                diagnostics.push(ResolutionDiagnostic::YankedVersion {
                                    dist: dist.clone(),
                                    reason: Some(reason.clone()),
                                });
                            }
                        }

                        // Extract the hashes, preserving those that were already present in the
                        // lockfile if necessary.
                        let hashes = if let Some(digests) = preferences
                            .match_hashes(name, version)
                            .filter(|digests| !digests.is_empty())
                        {
                            digests.to_vec()
                        } else if let Some(versions_response) = index.packages().get(name) {
                            if let VersionsResponse::Found(ref version_maps) = *versions_response {
                                version_maps
                                    .iter()
                                    .find_map(|version_map| version_map.hashes(version))
                                    .map(|mut digests| {
                                        digests.sort_unstable();
                                        digests
                                    })
                                    .unwrap_or_default()
                            } else {
                                vec![]
                            }
                        } else {
                            vec![]
                        };

                        // Extract the metadata.
                        let metadata = {
                            let dist = PubGrubDistribution::from_registry(name, version);

                            let response = index
                                .distributions()
                                .get(&dist.version_id())
                                .unwrap_or_else(|| {
                                    panic!(
                                        "Every package should have metadata: {:?}",
                                        dist.version_id()
                                    )
                                });

                            let MetadataResponse::Found(archive) = &*response else {
                                panic!(
                                    "Every package should have metadata: {:?}",
                                    dist.version_id()
                                )
                            };

                            archive.metadata.clone()
                        };

                        // Validate the extra.
                        if let Some(extra) = extra {
                            if !metadata.provides_extras.contains(extra) {
                                diagnostics.push(ResolutionDiagnostic::MissingExtra {
                                    dist: dist.clone(),
                                    extra: extra.clone(),
                                });
                            }
                        }
                        // Extract the markers.
                        let marker = markers.get(&(name, version, extra)).cloned();

                        // Add the distribution to the graph.
                        let index = petgraph.add_node(AnnotatedDist {
                            dist,
                            extra: extra.clone(),
                            marker,
                            hashes,
                            metadata,
                        });
                        inverse.insert((name, version, extra), index);
                    }

                    PubGrubPackageInner::Package {
                        name,
                        extra,
                        marker: None,
                        url: Some(url),
                    } => {
                        // Create the distribution.
                        let dist = Dist::from_url(name.clone(), url_to_precise(url.clone(), git))?;

                        // Extract the hashes, preserving those that were already present in the
                        // lockfile if necessary.
                        let hashes = if let Some(digests) = preferences
                            .match_hashes(name, version)
                            .filter(|digests| !digests.is_empty())
                        {
                            digests.to_vec()
                        } else if let Some(metadata_response) =
                            index.distributions().get(&dist.version_id())
                        {
                            if let MetadataResponse::Found(ref archive) = *metadata_response {
                                let mut digests = archive.hashes.clone();
                                digests.sort_unstable();
                                digests
                            } else {
                                vec![]
                            }
                        } else {
                            vec![]
                        };

                        // Extract the metadata.
                        let metadata = {
                            let dist = PubGrubDistribution::from_url(name, url);

                            let response = index
                                .distributions()
                                .get(&dist.version_id())
                                .unwrap_or_else(|| {
                                    panic!(
                                        "Every package should have metadata: {:?}",
                                        dist.version_id()
                                    )
                                });

                            let MetadataResponse::Found(archive) = &*response else {
                                panic!(
                                    "Every package should have metadata: {:?}",
                                    dist.version_id()
                                )
                            };

                            archive.metadata.clone()
                        };

                        // Validate the extra.
                        if let Some(extra) = extra {
                            if !metadata.provides_extras.contains(extra) {
                                diagnostics.push(ResolutionDiagnostic::MissingExtra {
                                    dist: dist.clone().into(),
                                    extra: extra.clone(),
                                });
                            }
                        }
                        // Extract the markers.
                        let marker = markers.get(&(name, version, extra)).cloned();

                        // Add the distribution to the graph.
                        let index = petgraph.add_node(AnnotatedDist {
                            dist: dist.into(),
                            extra: extra.clone(),
                            marker,
                            hashes,
                            metadata,
                        });
                        inverse.insert((name, version, extra), index);
                    }

                    _ => {}
                };
            }
        }

        // Add every edge to the graph.
        for (names, version_set) in resolution.dependencies {
            for versions in version_set {
                let from_index =
                    inverse[&(&names.from, &versions.from_version, &versions.from_extra)];
                let to_index = inverse[&(&names.to, &versions.to_version, &versions.to_extra)];
                petgraph.update_edge(from_index, to_index, versions.to_version.clone());
            }
        }

        Ok(Self {
            petgraph,
            diagnostics,
        })
    }

    /// Return the number of distinct packages in the graph.
    pub fn len(&self) -> usize {
        self.petgraph
            .node_indices()
            .map(|index| &self.petgraph[index])
            .filter(|dist| dist.extra.is_none())
            .count()
    }

    /// Return `true` if there are no packages in the graph.
    pub fn is_empty(&self) -> bool {
        self.len() == 0
    }

    /// Returns `true` if the graph contains the given package.
    pub fn contains(&self, name: &PackageName) -> bool {
        self.petgraph
            .node_indices()
            .any(|index| self.petgraph[index].name() == name)
    }

    /// Return the [`ResolutionDiagnostic`]s that were encountered while building the graph.
    pub fn diagnostics(&self) -> &[ResolutionDiagnostic] {
        &self.diagnostics
    }

    /// Return the marker tree specific to this resolution.
    ///
    /// This accepts a manifest, in-memory-index and marker environment. All
    /// of which should be the same values given to the resolver that produced
    /// this graph.
    ///
    /// The marker tree returned corresponds to an expression that, when true,
    /// this resolution is guaranteed to be correct. Note though that it's
    /// possible for resolution to be correct even if the returned marker
    /// expression is false.
    ///
    /// For example, if the root package has a dependency `foo; sys_platform ==
    /// "macos"` and resolution was performed on Linux, then the marker tree
    /// returned will contain a `sys_platform == "linux"` expression. This
    /// means that whenever the marker expression evaluates to true (i.e., the
    /// current platform is Linux), then the resolution here is correct. But
    /// it is possible that the resolution is also correct on other platforms
    /// that aren't macOS, such as Windows. (It is unclear at time of writing
    /// whether this is fundamentally impossible to compute, or just impossible
    /// to compute in some cases.)
    pub fn marker_tree(
        &self,
        manifest: &Manifest,
        index: &InMemoryIndex,
        marker_env: &MarkerEnvironment,
    ) -> anyhow::Result<pep508_rs::MarkerTree, Box<ParsedUrlError>> {
        use pep508_rs::{
            MarkerExpression, MarkerOperator, MarkerTree, MarkerValueString, MarkerValueVersion,
        };

        /// A subset of the possible marker values.
        ///
        /// We only track the marker parameters that are referenced in a marker
        /// expression. We'll use references to the parameter later to generate
        /// values based on the current marker environment.
        #[derive(Debug, Eq, Hash, PartialEq)]
        enum MarkerParam {
            Version(MarkerValueVersion),
            String(MarkerValueString),
        }

        /// Add all marker parameters from the given tree to the given set.
        fn add_marker_params_from_tree(marker_tree: &MarkerTree, set: &mut FxHashSet<MarkerParam>) {
            match marker_tree {
                MarkerTree::Expression(
                    MarkerExpression::Version { key, .. }
                    | MarkerExpression::VersionInverted { key, .. },
                ) => {
                    set.insert(MarkerParam::Version(key.clone()));
                }
                MarkerTree::Expression(
                    MarkerExpression::String { key, .. }
                    | MarkerExpression::StringInverted { key, .. },
                ) => {
                    set.insert(MarkerParam::String(key.clone()));
                }
                MarkerTree::And(ref exprs) | MarkerTree::Or(ref exprs) => {
                    for expr in exprs {
                        add_marker_params_from_tree(expr, set);
                    }
                }
                // We specifically don't care about these for the
                // purposes of generating a marker string for a lock
                // file. Quoted strings are marker values given by the
                // user. We don't track those here, since we're only
                // interested in which markers are used.
                MarkerTree::Expression(
                    MarkerExpression::Extra { .. } | MarkerExpression::Arbitrary { .. },
                ) => {}
            }
        }

        let mut seen_marker_values = FxHashSet::default();
        for i in self.petgraph.node_indices() {
            let dist = &self.petgraph[i];
            let version_id = match dist.version_or_url() {
                VersionOrUrlRef::Version(version) => {
                    VersionId::from_registry(dist.name().clone(), version.clone())
                }
                VersionOrUrlRef::Url(verbatim_url) => VersionId::from_url(verbatim_url.raw()),
            };
            let res = index
                .distributions()
                .get(&version_id)
                .expect("every package in resolution graph has metadata");
            let MetadataResponse::Found(archive, ..) = &*res else {
                panic!(
                    "Every package should have metadata: {:?}",
                    dist.version_id()
                )
            };
            for req in manifest.apply(archive.metadata.requires_dist.iter()) {
                let Some(ref marker_tree) = req.marker else {
                    continue;
                };
                add_marker_params_from_tree(marker_tree, &mut seen_marker_values);
            }
        }

        // Ensure that we consider markers from direct dependencies.
        for direct_req in manifest.apply(manifest.requirements.iter()) {
            let Some(ref marker_tree) = direct_req.marker else {
                continue;
            };
            add_marker_params_from_tree(marker_tree, &mut seen_marker_values);
        }

        // Generate the final marker expression as a conjunction of
        // strict equality terms.
        let mut conjuncts = vec![];
        for marker_param in seen_marker_values {
            let expr = match marker_param {
                MarkerParam::Version(value_version) => {
                    let from_env = marker_env.get_version(&value_version);
                    MarkerExpression::Version {
                        key: value_version,
                        specifier: VersionSpecifier::equals_version(from_env.clone()),
                    }
                }
                MarkerParam::String(value_string) => {
                    let from_env = marker_env.get_string(&value_string);
                    MarkerExpression::String {
                        key: value_string,
                        operator: MarkerOperator::Equal,
                        value: from_env.to_string(),
                    }
                }
            };
            conjuncts.push(MarkerTree::Expression(expr));
        }
        Ok(MarkerTree::And(conjuncts))
    }
}

impl From<ResolutionGraph> for distribution_types::Resolution {
    fn from(graph: ResolutionGraph) -> Self {
        Self::new(
            graph
                .petgraph
                .node_indices()
                .map(|node| {
                    (
                        graph.petgraph[node].name().clone(),
                        graph.petgraph[node].dist.clone(),
                    )
                })
                .collect(),
            graph.diagnostics,
        )
    }
}
