//! Convert annotated metadata into a renderable context

pub mod crate_context;
mod platforms;

use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::{Path, PathBuf};

use anyhow::Result;
use serde::{Deserialize, Serialize};

use crate::config::CrateId;
use crate::context::crate_context::{CrateContext, CrateDependency, Rule};
use crate::context::platforms::resolve_cfg_platforms;
use crate::lockfile::Digest;
use crate::metadata::Annotations;
use crate::utils::starlark::{Select, SelectList};

pub use self::crate_context::*;

/// A struct containing information about a Cargo dependency graph in an easily to consume
/// format for rendering reproducible Bazel targets.
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Clone)]
pub struct Context {
    /// The collective checksum of all inputs to the context
    pub checksum: Option<Digest>,

    /// The collection of all crates that make up the dependency graph
    pub crates: BTreeMap<CrateId, CrateContext>,

    /// A subset of only crates with binary targets
    pub binary_crates: BTreeSet<CrateId>,

    /// A subset of workspace members mapping to their workspace
    /// path relative to the workspace root
    pub workspace_members: BTreeMap<CrateId, String>,

    /// A mapping of `cfg` flags to platform triples supporting the configuration
    pub conditions: BTreeMap<String, BTreeSet<String>>,
}

impl Context {
    pub fn try_from_path<T: AsRef<Path>>(path: T) -> Result<Self> {
        let data = fs::read_to_string(path.as_ref())?;
        Ok(serde_json::from_str(&data)?)
    }

    pub fn new(annotations: Annotations) -> Result<Self> {
        // Build a map of crate contexts
        let crates: BTreeMap<CrateId, CrateContext> = annotations
            .metadata
            .crates
            .iter()
            // Convert the crate annotations into more renderable contexts
            .map(|(_, annotation)| {
                let context = CrateContext::new(
                    annotation,
                    &annotations.metadata.packages,
                    &annotations.lockfile.crates,
                    &annotations.pairred_extras,
                    annotations.config.generate_build_scripts,
                );
                let id = CrateId::new(context.name.clone(), context.version.clone());
                (id, context)
            })
            .collect();

        // Filter for any crate that contains a binary
        let binary_crates: BTreeSet<CrateId> = crates
            .iter()
            .filter(|(_, ctx)| ctx.targets.iter().any(|t| matches!(t, Rule::Binary(..))))
            // Only consider remote repositories (so non-workspace members).
            .filter(|(_, ctx)| ctx.repository.is_some())
            .map(|(id, _)| id.clone())
            .collect();

        // Given a list of all conditional dependencies, build a set of platform
        // triples which satsify the conditions.
        let conditions = resolve_cfg_platforms(
            crates.values().collect(),
            &annotations.config.supported_platform_triples,
        )?;

        // Generate a list of all workspace members
        let workspace_members = annotations
            .metadata
            .workspace_members
            .iter()
            .filter_map(|id| {
                let pkg = &annotations.metadata.packages[id];
                let package_path_id = match Self::get_package_path_id(
                    pkg,
                    &annotations.metadata.workspace_root,
                    &annotations.metadata.workspace_metadata.workspace_prefix,
                    &annotations.metadata.workspace_metadata.package_prefixes,
                ) {
                    Ok(id) => id,
                    Err(e) => return Some(Err(e)),
                };
                let crate_id = CrateId::new(pkg.name.clone(), pkg.version.to_string());

                // Crates that have repository information are not considered workspace members.
                // The assumpion is that they are "extra workspace members".
                match crates[&crate_id].repository {
                    Some(_) => None,
                    None => Some(Ok((crate_id, package_path_id))),
                }
            })
            .collect::<Result<BTreeMap<CrateId, String>>>()?;

        Ok(Self {
            checksum: None,
            crates,
            binary_crates,
            workspace_members,
            conditions,
        })
    }

    // A helper function for locating the unique path in a workspace to a workspace member
    fn get_package_path_id(
        package: &cargo_metadata::Package,
        workspace_root: &Path,
        workspace_prefix: &Option<String>,
        package_prefixes: &BTreeMap<String, String>,
    ) -> Result<String> {
        // Locate the package's manifest directory
        let manifest_dir = package
            .manifest_path
            .parent()
            .expect("Every manifest should have a parent")
            .as_std_path();

        // Compare it with the root of the workspace
        let package_path_diff = pathdiff::diff_paths(manifest_dir, workspace_root)
            .expect("Every workspace member's manifest is a child of the workspace root");

        // Ensure the package paths are adjusted in the macros according to the splicing results
        let package_path = match package_prefixes.get(&package.name) {
            // Any package prefix should be absolute and therefore always applied
            Some(prefix) => PathBuf::from(prefix).join(package_path_diff),
            // If no package prefix is present, attempt to apply the workspace prefix
            // since workspace members would not have shown up with their own label
            None => match workspace_prefix {
                Some(prefix) => PathBuf::from(prefix).join(package_path_diff),
                None => package_path_diff,
            },
        };

        // Sanitize the path for increased consistency
        let package_path_id = package_path
            .display()
            .to_string()
            .replace('\\', "/")
            .trim_matches('/')
            .to_owned();

        Ok(package_path_id)
    }

    /// Filter a crate's dependencies to only ones with aliases
    pub fn crate_aliases(
        &self,
        crate_id: &CrateId,
        build: bool,
        include_dev: bool,
    ) -> SelectList<&CrateDependency> {
        let ctx = &self.crates[crate_id];
        let mut set = SelectList::default();

        // Return a set of aliases for build dependencies
        // vs normal dependencies when requested.
        if build {
            // Note that there may not be build dependencies so no dependencies
            // will be gathered in this case
            if let Some(attrs) = &ctx.build_script_attrs {
                let collection: Vec<(Option<String>, &CrateDependency)> = attrs
                    .deps
                    .configurations()
                    .into_iter()
                    .flat_map(move |conf| {
                        attrs
                            .deps
                            .get_iter(conf)
                            .expect("Iterating over known keys should never panic")
                            .filter(|dep| dep.alias.is_some())
                            .map(move |dep| (conf.cloned(), dep))
                    })
                    .chain(attrs.proc_macro_deps.configurations().into_iter().flat_map(
                        move |conf| {
                            attrs
                                .proc_macro_deps
                                .get_iter(conf)
                                .expect("Iterating over known keys should never panic")
                                .filter(|dep| dep.alias.is_some())
                                .map(move |dep| (conf.cloned(), dep))
                        },
                    ))
                    .collect();

                for (config, dep) in collection {
                    set.insert(dep, config);
                }
            }
        } else {
            let attrs = &ctx.common_attrs;
            let mut collection: Vec<(Option<String>, &CrateDependency)> =
                attrs
                    .deps
                    .configurations()
                    .into_iter()
                    .flat_map(move |conf| {
                        attrs
                            .deps
                            .get_iter(conf)
                            .expect("Iterating over known keys should never panic")
                            .filter(|dep| dep.alias.is_some())
                            .map(move |dep| (conf.cloned(), dep))
                    })
                    .chain(attrs.proc_macro_deps.configurations().into_iter().flat_map(
                        move |conf| {
                            attrs
                                .proc_macro_deps
                                .get_iter(conf)
                                .expect("Iterating over known keys should never panic")
                                .filter(|dep| dep.alias.is_some())
                                .map(move |dep| (conf.cloned(), dep))
                        },
                    ))
                    .collect();

            // Optionally include dev dependencies
            if include_dev {
                collection = collection
                    .into_iter()
                    .chain(
                        attrs
                            .deps_dev
                            .configurations()
                            .into_iter()
                            .flat_map(move |conf| {
                                attrs
                                    .deps_dev
                                    .get_iter(conf)
                                    .expect("Iterating over known keys should never panic")
                                    .filter(|dep| dep.alias.is_some())
                                    .map(move |dep| (conf.cloned(), dep))
                            }),
                    )
                    .chain(
                        attrs
                            .proc_macro_deps_dev
                            .configurations()
                            .into_iter()
                            .flat_map(move |conf| {
                                attrs
                                    .proc_macro_deps_dev
                                    .get_iter(conf)
                                    .expect("Iterating over known keys should never panic")
                                    .filter(|dep| dep.alias.is_some())
                                    .map(move |dep| (conf.cloned(), dep))
                            }),
                    )
                    .collect();
            }

            for (config, dep) in collection {
                set.insert(dep, config);
            }
        }

        set
    }

    /// Create a set of all direct dependencies of workspace member crates and map them to
    /// optional alternative names that allow them to be uniquely identified. This typically
    /// results in a mapping of ([CrateId], [None]) where [None] defaults to using the crate
    /// name. The next most common would be using ([CrateId], `Some(alias)`) as some projects
    /// may use aliases in Cargo as a way to differentiate different versions of the same dep.
    pub fn flat_workspace_member_deps(&self) -> BTreeMap<CrateId, Option<String>> {
        let workspace_member_dependencies: BTreeSet<CrateDependency> = self
            .workspace_members
            .iter()
            .map(|(id, _)| &self.crates[id])
            .flat_map(|ctx| {
                // Build an interator of all dependency CrateIds.
                // TODO: This expansion is horribly verbose and should be refactored but closures
                // were not playing nice when I tried it.
                ctx.common_attrs
                    .deps
                    .configurations()
                    .into_iter()
                    .flat_map(move |conf| {
                        ctx.common_attrs
                            .deps
                            .get_iter(conf)
                            .expect("Lookup should be guaranteed")
                    })
                    .chain(
                        ctx.common_attrs
                            .deps_dev
                            .configurations()
                            .into_iter()
                            .flat_map(move |conf| {
                                ctx.common_attrs
                                    .deps_dev
                                    .get_iter(conf)
                                    .expect("Lookup should be guaranteed")
                            }),
                    )
                    .chain(
                        ctx.common_attrs
                            .proc_macro_deps
                            .configurations()
                            .into_iter()
                            .flat_map(move |conf| {
                                ctx.common_attrs
                                    .proc_macro_deps
                                    .get_iter(conf)
                                    .expect("Lookup should be guaranteed")
                            }),
                    )
                    .chain(
                        ctx.common_attrs
                            .proc_macro_deps_dev
                            .configurations()
                            .into_iter()
                            .flat_map(move |conf| {
                                ctx.common_attrs
                                    .proc_macro_deps_dev
                                    .get_iter(conf)
                                    .expect("Lookup should be guaranteed")
                            }),
                    )
            })
            .cloned()
            .collect();

        // Search for any duplicate workspace member definitions
        let duplicate_deps: Vec<CrateDependency> = workspace_member_dependencies
            .iter()
            .filter(|dep| {
                workspace_member_dependencies
                    .iter()
                    .filter(|check| dep.id.name == check.id.name)
                    .count()
                    > 1
            })
            .cloned()
            .collect();

        workspace_member_dependencies
            .into_iter()
            .map(|dep| {
                if duplicate_deps.contains(&dep) {
                    if let Some(alias) = &dep.alias {
                        // Check for any duplicate aliases
                        let aliases = duplicate_deps
                            .iter()
                            .filter(|dupe| dupe.id.name == dep.id.name)
                            .filter(|dupe| dupe.alias.is_some())
                            .filter(|dupe| dupe.alias == dep.alias);

                        // If there are multiple aliased crates with the same name, the name is updated to
                        // be `{alias}-{version}` to differentiate them.
                        if aliases.count() >= 2 {
                            let rename = format!("{}-{}", &alias, &dep.id.version);
                            (dep.id, Some(rename))
                        } else {
                            (dep.id, Some(alias.clone()))
                        }
                    } else {
                        // Check for all duplicates that match the current dependency and have no alias
                        let unaliased = duplicate_deps
                            .iter()
                            .filter(|dupe| dupe.id.name == dep.id.name)
                            .filter(|dupe| dupe.alias.is_none());

                        // If there are multiple unaliased crates with the same name, the name is updated to
                        // be `{name}-{version}` to differentiate them.
                        if unaliased.count() >= 2 {
                            let rename = format!("{}-{}", &dep.id.name, &dep.id.version);
                            (dep.id, Some(rename))
                        } else {
                            (dep.id, None)
                        }
                    }
                } else {
                    (dep.id, dep.alias)
                }
            })
            .collect()
    }

    /// Produce a list of binary dependencies with optional aliases which prevent duplicate
    /// targets from being generated.
    pub fn flat_binary_deps(&self) -> BTreeMap<CrateId, Option<String>> {
        // Check for any duplicate binary crate names. If one exists provide an alias to differentiate them
        self.binary_crates
            .iter()
            .map(|crate_id| {
                let dupe_count = self
                    .binary_crates
                    .iter()
                    .filter(|id| crate_id.name == id.name)
                    .count();
                // For targets that appear twice (which can happen if one crate aliases a binary dependency)
                if dupe_count >= 2 {
                    let rename = format!("{}-{}", crate_id.name, crate_id.version);
                    (crate_id.clone(), Some(rename))
                } else {
                    (crate_id.clone(), None)
                }
            })
            .collect()
    }
}

#[cfg(test)]
mod test {
    use super::*;

    use crate::config::Config;

    fn mock_context_common() -> Context {
        let annotations = Annotations::new(
            crate::test::metadata::common(),
            crate::test::lockfile::common(),
            Config::default(),
        )
        .unwrap();

        Context::new(annotations).unwrap()
    }

    fn mock_context_aliases() -> Context {
        let annotations = Annotations::new(
            crate::test::metadata::alias(),
            crate::test::lockfile::alias(),
            Config::default(),
        )
        .unwrap();

        Context::new(annotations).unwrap()
    }

    #[test]
    fn flat_workspace_member_deps() {
        let context = mock_context_common();
        let workspace_member_deps = context.flat_workspace_member_deps();

        assert_eq!(
            workspace_member_deps,
            BTreeMap::from([
                (
                    CrateId::new("bitflags".to_owned(), "1.3.2".to_owned()),
                    None
                ),
                (CrateId::new("cfg-if".to_owned(), "1.0.0".to_owned()), None),
            ])
        );
    }

    #[test]
    fn flat_workspace_member_deps_with_alises() {
        let context = mock_context_aliases();
        let workspace_member_deps = context.flat_workspace_member_deps();

        assert_eq!(
            workspace_member_deps,
            BTreeMap::from([
                (
                    CrateId {
                        name: "log".to_owned(),
                        version: "0.3.9".to_owned(),
                    },
                    Some("pinned_log".to_owned())
                ),
                (
                    CrateId {
                        name: "log".to_owned(),
                        version: "0.4.14".to_owned(),
                    },
                    None
                ),
                (
                    CrateId {
                        name: "names".to_owned(),
                        version: "0.11.1-dev".to_owned(),
                    },
                    Some("pinned_names".to_owned())
                ),
                (
                    CrateId {
                        name: "names".to_owned(),
                        version: "0.12.0".to_owned(),
                    },
                    None
                ),
                (
                    CrateId {
                        name: "value-bag".to_owned(),
                        version: "1.0.0-alpha.7".to_owned(),
                    },
                    None
                ),
            ])
        );
    }
}
