/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use std::collections::HashMap;
use std::collections::HashSet;
use std::hash::Hash;
use std::hash::Hasher;
use std::sync::Arc;

use async_recursion::async_recursion;
use async_trait::async_trait;
use buck2_build_api::configure_targets::get_compatible_targets;
use buck2_common::dice::file_ops::HasFileOps;
use buck2_common::file_ops::FileOps;
use buck2_common::file_ops::PathMetadata;
use buck2_common::file_ops::PathMetadataOrRedirection;
use buck2_common::result::SharedResult;
use buck2_common::result::ToSharedResultExt;
use buck2_core::cells::cell_path::CellPath;
use buck2_core::cells::cell_path::CellPathRef;
use buck2_core::package::PackageLabel;
use buck2_core::target::label::TargetLabel;
use buck2_node::nodes::configured::ConfiguredTargetNode;
use buck2_node::nodes::unconfigured::TargetNode;
use buck2_query::query::environment::ConfiguredOrUnconfiguredTargetLabel;
use buck2_query::query::environment::QueryTarget;
use buck2_query::query::syntax::simple::eval::set::TargetSet;
use buck2_query::query::traversal::async_depth_first_postorder_traversal;
use buck2_query::query::traversal::AsyncNodeLookup;
use buck2_query::query::traversal::AsyncTraversalDelegate;
use buck2_query::query::traversal::ChildVisitor;
use dice::DiceComputations;
use dice::DiceTransaction;
use dupe::Dupe;
use futures::future::join_all;
use futures::future::Shared;
use futures::join;
use futures::stream::FuturesUnordered;
use futures::FutureExt;
use futures::StreamExt;
use more_futures::spawn::spawn_cancellable;
use more_futures::spawn::DropCancelFuture;
use os_str_bytes::OsStrBytes;
use siphasher::sip128::Hasher128;
use siphasher::sip128::SipHasher24;

#[derive(Clone, Dupe, derive_more::Display)]
#[display(fmt = "{:032x}", _0)]
pub struct BuckTargetHash(pub u128);

trait BuckTargetHasher: Hasher + Send + 'static {
    fn finish_u128(&mut self) -> BuckTargetHash;
}

/// siphash24 is used as the "fast" hash. There are faster hash algorithms out there,
/// but many (xxhash, ahash, etc) are poorly documented and having deterministic behavior
/// across architectures is rarely guaranteed. We see about a 20-25% improvement relative
/// to blake3 and so there's likely little opportunity remaining for a faster hash function
/// to capture anyway.
impl BuckTargetHasher for siphasher::sip128::SipHasher24 {
    fn finish_u128(&mut self) -> BuckTargetHash {
        BuckTargetHash(self.finish128().as_u128())
    }
}

/// We use blake3 as our "strong" hash.
struct Blake3Adapter(blake3::Hasher);

impl Hasher for Blake3Adapter {
    fn finish(&self) -> u64 {
        unimplemented!()
    }

    fn write(&mut self, bytes: &[u8]) {
        self.0.update(bytes);
    }
}

impl BuckTargetHasher for Blake3Adapter {
    fn finish_u128(&mut self) -> BuckTargetHash {
        let hash = blake3::Hasher::finalize(&self.0);
        let bytes = hash.as_bytes();
        BuckTargetHash(u128::from_le_bytes(bytes[16..].try_into().unwrap()))
    }
}

pub enum TargetHashesFileMode {
    /// The following files have changed in some way (don't do any IO)
    PathsOnly(HashSet<CellPath>),
    /// Use IO operations to find the paths and their contents
    PathsAndContents,
    /// Don't hash any files
    None,
}

#[async_trait]
trait FileHasher: Send + Sync {
    /// Obtain information about a path in some manner.
    async fn hash_path(&self, path: &CellPath) -> anyhow::Result<Vec<u8>>;
}

struct PathsOnlyFileHasher {
    pseudo_changed_paths: HashSet<CellPath>,
}

#[async_trait]
impl FileHasher for PathsOnlyFileHasher {
    async fn hash_path(&self, path: &CellPath) -> anyhow::Result<Vec<u8>> {
        if self.pseudo_changed_paths.contains(path) {
            Ok(vec![0u8])
        } else {
            Ok(Vec::new())
        }
    }
}

struct PathsAndContentsHasher {
    dice: DiceTransaction,
}

#[async_trait]
impl FileHasher for PathsAndContentsHasher {
    async fn hash_path(&self, cell_path: &CellPath) -> anyhow::Result<Vec<u8>> {
        #[async_recursion]
        async fn hash_item(
            file_ops: &dyn FileOps,
            cell_path: CellPathRef<'async_recursion>,
            res: &mut Vec<u8>,
        ) -> anyhow::Result<()> {
            let info = file_ops.read_path_metadata(cell_path.dupe()).await?;
            // Important that the different branches can never clash, so add a prefix byte to them
            match PathMetadataOrRedirection::from(info) {
                PathMetadataOrRedirection::PathMetadata(meta) => match meta {
                    PathMetadata::File(m) => {
                        let digest = m.digest.raw_digest().as_bytes();
                        res.reserve(1 + digest.len());
                        // We ignore `digest.size` as the SHA1 alone is enough to be unique
                        res.push(0u8);
                        res.extend(digest);
                    }
                    PathMetadata::ExternalSymlink(m) => {
                        // We don't want to go to the disk and get the digest of the file in the external symlink.
                        // But we do want to change the details if the target of the symlink changes, so
                        // take the data in the symlink, put it into a buffer, and produce a digest of that
                        let target = m.to_path_buf();
                        let target = target.to_raw_bytes();
                        res.reserve(1 + target.len());
                        res.push(1u8);
                        res.extend(&*target);
                    }
                    PathMetadata::Directory => {
                        res.push(2u8);
                        let files = file_ops.read_dir(cell_path.dupe()).await?.included;
                        res.extend(files.len().to_be_bytes());
                        for x in &*files {
                            let name = x.file_name.as_str();
                            res.extend(name.len().to_be_bytes());
                            res.extend(name.as_bytes());
                            hash_item(file_ops, cell_path.join(&x.file_name).as_ref(), res).await?;
                        }
                    }
                },
                PathMetadataOrRedirection::Redirection(r) => {
                    // TODO (T126181780): This should have a limit on recursion.
                    hash_item(file_ops, r.as_ref().as_ref(), res).await?;
                }
            }
            Ok(())
        }

        let file_ops = self.dice.file_ops();
        let mut res = Vec::new();
        hash_item(&file_ops, cell_path.as_ref(), &mut res).await?;
        Ok(res)
    }
}

/// Types of node that can be target hashed (just configured and unconfigured).
/// This trait is purposely defined here instead of in buck2_node crate
/// so that we can only access the public fields of these nodes.
#[async_trait]
pub trait TargetHashingTargetNode: QueryTarget {
    /// We only hash this node, not its dependencies.
    /// Importantly, we look at the nodes after configuration (for the configured case).
    fn target_hash<H: Hasher>(&self, state: &mut H);

    // Takes in Target Nodes and returns a new set of (un)Configured
    // Target Nodes based on type of hashing specified.
    async fn get_target_nodes(
        dice: &DiceComputations,
        loaded_targets: Vec<(PackageLabel, anyhow::Result<Vec<TargetNode>>)>,
        global_target_platform: Option<TargetLabel>,
    ) -> anyhow::Result<TargetSet<Self>>;
}

#[async_trait]
impl TargetHashingTargetNode for ConfiguredTargetNode {
    fn target_hash<H: Hasher>(&self, state: &mut H) {
        self.target_hash(state)
    }

    async fn get_target_nodes(
        dice: &DiceComputations,
        loaded_targets: Vec<(PackageLabel, anyhow::Result<Vec<TargetNode>>)>,
        global_target_platform: Option<TargetLabel>,
    ) -> anyhow::Result<TargetSet<Self>> {
        get_compatible_targets(dice, loaded_targets.into_iter(), global_target_platform).await
    }
}

#[async_trait]
impl TargetHashingTargetNode for TargetNode {
    fn target_hash<H: Hasher>(&self, state: &mut H) {
        self.target_hash(state)
    }

    async fn get_target_nodes(
        _dice: &DiceComputations,
        loaded_targets: Vec<(PackageLabel, anyhow::Result<Vec<TargetNode>>)>,
        _global_target_platform: Option<TargetLabel>,
    ) -> anyhow::Result<TargetSet<Self>> {
        let mut target_set = TargetSet::new();
        for (_package, result) in loaded_targets {
            target_set.extend(result?);
        }
        Ok(target_set)
    }
}
pub struct TargetHashes {
    // key is an unconfigured target label, but the hash is generated from the configured target label.
    target_mapping: HashMap<TargetLabel, SharedResult<BuckTargetHash>>,
}

#[derive(thiserror::Error, Debug)]
enum TargetHashError {
    #[error(
        "Found a dependency `{0}` of target `{1}` which has not been hashed yet. This may indicate a dependency cycle in the unconfigured graph."
    )]
    DependencyCycle(String, String),
}

impl TargetHashes {
    pub fn get(&self, label: &TargetLabel) -> Option<&SharedResult<BuckTargetHash>> {
        self.target_mapping.get(label)
    }

    async fn compute_recursive_target_hashes<T: TargetHashingTargetNode, L: AsyncNodeLookup<T>>(
        dice: DiceTransaction,
        lookup: L,
        targets: TargetSet<T>,
        file_hasher: Option<Arc<dyn FileHasher>>,
        use_fast_hash: bool,
    ) -> anyhow::Result<Self>
    where
        T::NodeRef: ConfiguredOrUnconfiguredTargetLabel,
    {
        struct Delegate<T: QueryTarget> {
            hashes: HashMap<T::NodeRef, Shared<DropCancelFuture<SharedResult<BuckTargetHash>>>>,
            file_hasher: Option<Arc<dyn FileHasher>>,
            use_fast_hash: bool,
            dice: DiceTransaction,
        }

        #[async_trait]
        impl<T: TargetHashingTargetNode> AsyncTraversalDelegate<T> for Delegate<T> {
            fn visit(&mut self, target: T) -> anyhow::Result<()> {
                // this is postorder, so guaranteed that all deps have futures already.
                let dep_futures: Vec<_> = target
                    .deps()
                    .map(|dep| {
                        self.hashes.get(dep).cloned().ok_or_else(|| {
                            TargetHashError::DependencyCycle(
                                dep.clone().to_string(),
                                target.node_ref().to_string(),
                            )
                        })
                    })
                    .collect::<Result<Vec<_>, TargetHashError>>()?;

                let file_hasher = self.file_hasher.dupe();
                let dice = self.dice.dupe();

                let use_fast_hash = self.use_fast_hash;
                // we spawn off the hash computation since it can't be done in visit directly. Even if it could,
                // this allows us to start the computations for dependents before finishing the computation for a node.
                self.hashes.insert(
                    target.node_ref().clone(),
                    spawn_cancellable(
                        |_| {
                            async move {
                                let mut hasher = TargetHashes::new_hasher(use_fast_hash);
                                TargetHashes::hash_node(&target, &mut *hasher);

                                let mut input_futs = Vec::new();
                                if let Some(file_hasher) = file_hasher {
                                    target.inputs_for_each(|cell_path| {
                                        let file_hasher = file_hasher.dupe();
                                        input_futs.push(async move {
                                            let file_hash = file_hasher.hash_path(&cell_path).await;
                                            (cell_path, file_hash)
                                        });
                                        anyhow::Ok(())
                                    })?;
                                }

                                let (dep_hashes, input_hashes) =
                                    join!(join_all(dep_futures), join_all(input_futs));

                                TargetHashes::hash_deps(dep_hashes, &mut *hasher)?;
                                TargetHashes::hash_files(input_hashes, &mut *hasher)?;

                                Ok(hasher.finish_u128())
                            }
                            .boxed()
                        },
                        &*dice.per_transaction_data().spawner,
                        dice.per_transaction_data(),
                    )
                    .into_drop_cancel()
                    .shared(),
                );

                Ok(())
            }

            async fn for_each_child(
                &mut self,
                target: &T,
                func: &mut dyn ChildVisitor<T>,
            ) -> anyhow::Result<()> {
                for dep in target.deps() {
                    func.visit(dep.clone())?;
                }

                Ok(())
            }
        }

        let mut delegate = Delegate::<T> {
            hashes: HashMap::new(),
            file_hasher,
            use_fast_hash,
            dice,
        };

        async_depth_first_postorder_traversal(&lookup, targets.iter_names(), &mut delegate).await?;

        let mut futures: FuturesUnordered<_> = delegate
            .hashes
            .into_iter()
            .map(|(target, fut)| async move { (target, fut.await) })
            .collect();

        let mut target_mapping: HashMap<TargetLabel, SharedResult<BuckTargetHash>> = HashMap::new();

        // TODO(cjhopman): FuturesOrdered/Unordered interacts poorly with tokio cooperative scheduling
        // (see https://github.com/rust-lang/futures-rs/issues/2053). Clean this up once a good
        // solution there exists.

        while let Some((target, hash)) = tokio::task::unconstrained(futures.next()).await {
            // Only need the hashes of the requested target, not of dependencies.
            if targets.contains(&target) {
                let overwrite = target_mapping.insert(target.unconfigured_label().dupe(), hash);
                assert!(
                    overwrite.is_none(),
                    "Target {} was computed multiple times.",
                    target.unconfigured_label().name().as_str()
                );
            }
        }
        Ok(Self { target_mapping })
    }

    async fn compute_immediate_target_hashes<T: TargetHashingTargetNode>(
        targets: TargetSet<T>,
        file_hasher: Option<Arc<dyn FileHasher>>,
        use_fast_hash: bool,
    ) -> anyhow::Result<Self>
    where
        T::NodeRef: ConfiguredOrUnconfiguredTargetLabel,
    {
        let hashing_futures: Vec<_> = targets
            .into_iter()
            .map(|target| {
                let file_hasher = file_hasher.dupe();
                async move {
                    let hash_result: anyhow::Result<BuckTargetHash> = try {
                        let mut hasher = TargetHashes::new_hasher(use_fast_hash);
                        TargetHashes::hash_node(&target, &mut *hasher);

                        if let Some(file_hasher) = file_hasher {
                            let mut input_futs = Vec::new();
                            target.inputs_for_each(|cell_path| {
                                let file_hasher = file_hasher.dupe();
                                input_futs.push(async move {
                                    let file_hash = file_hasher.hash_path(&cell_path).await;
                                    (cell_path, file_hash)
                                });
                                anyhow::Ok(())
                            })?;

                            let input_hashes = join_all(input_futs).await;
                            TargetHashes::hash_files(input_hashes, &mut *hasher)?;
                        }

                        hasher.finish_u128()
                    };
                    (
                        target.node_ref().unconfigured_label().dupe(),
                        hash_result.shared_error(),
                    )
                }
                .boxed()
                .shared()
            })
            .collect();

        let target_mapping: HashMap<TargetLabel, SharedResult<BuckTargetHash>> =
            join_all(hashing_futures).await.into_iter().collect();
        Ok(Self { target_mapping })
    }

    pub fn compute_immediate_one(node: &TargetNode, use_fast_hash: bool) -> BuckTargetHash {
        let mut hasher = TargetHashes::new_hasher(use_fast_hash);
        TargetHashes::hash_node(node, &mut *hasher);
        hasher.finish_u128()
    }

    pub async fn compute<T: TargetHashingTargetNode, L: AsyncNodeLookup<T>>(
        dice: DiceTransaction,
        lookup: L,
        targets: Vec<(PackageLabel, anyhow::Result<Vec<TargetNode>>)>,
        global_target_platform: Option<TargetLabel>,
        file_hash_mode: TargetHashesFileMode,
        use_fast_hash: bool,
        target_hash_recursive: bool,
    ) -> anyhow::Result<Self>
    where
        T::NodeRef: ConfiguredOrUnconfiguredTargetLabel,
    {
        let targets = T::get_target_nodes(&dice, targets, global_target_platform).await?;
        let file_hasher = Self::new_file_hasher(dice.dupe(), file_hash_mode);
        if target_hash_recursive {
            Self::compute_recursive_target_hashes(dice, lookup, targets, file_hasher, use_fast_hash)
                .await
        } else {
            Self::compute_immediate_target_hashes(targets, file_hasher, use_fast_hash).await
        }
    }

    fn new_file_hasher(
        dice: DiceTransaction,
        file_hash_mode: TargetHashesFileMode,
    ) -> Option<Arc<dyn FileHasher>> {
        match file_hash_mode {
            TargetHashesFileMode::PathsOnly(modified_paths) => {
                Some(Arc::new(PathsOnlyFileHasher {
                    pseudo_changed_paths: modified_paths,
                }))
            }
            TargetHashesFileMode::PathsAndContents => {
                Some(Arc::new(PathsAndContentsHasher { dice: dice.dupe() }))
            }
            TargetHashesFileMode::None => None,
        }
    }

    fn new_hasher(use_fast_hash: bool) -> Box<dyn BuckTargetHasher> {
        if use_fast_hash {
            Box::new(SipHasher24::new())
        } else {
            Box::new(Blake3Adapter(blake3::Hasher::new()))
        }
    }

    fn hash_node<T: TargetHashingTargetNode>(node: &T, mut hasher: &mut dyn BuckTargetHasher) {
        node.target_hash(&mut hasher);
    }

    fn hash_deps(
        dep_hashes: Vec<SharedResult<BuckTargetHash>>,
        hasher: &mut dyn BuckTargetHasher,
    ) -> SharedResult<()> {
        for target_hash in dep_hashes {
            hasher.write_u128(target_hash?.0);
        }
        Ok(())
    }

    fn hash_files(
        file_digests: Vec<(CellPath, anyhow::Result<Vec<u8>>)>,
        mut hasher: &mut dyn BuckTargetHasher,
    ) -> SharedResult<()> {
        for (path, digest) in file_digests {
            path.hash(&mut hasher);
            let digest = digest?;
            hasher.write_usize(digest.len());
            hasher.write(&digest);
        }
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use crate::target_hash::BuckTargetHash;

    #[test]
    fn test_hash_display() {
        assert_eq!(
            "00000000000000000000000000000000",
            BuckTargetHash(0).to_string()
        );
        assert_eq!(
            "ffffffffffffffffffffffffffffffff",
            BuckTargetHash(u128::MAX).to_string()
        );
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use std::fmt::Write;
use std::iter;

use async_trait::async_trait;
use buck2_build_api::configure_targets::load_compatible_patterns;
use buck2_cli_proto::ConfiguredTargetsRequest;
use buck2_cli_proto::ConfiguredTargetsResponse;
use buck2_cli_proto::HasClientContext;
use buck2_core::pattern::pattern_type::TargetPatternExtra;
use buck2_node::load_patterns::MissingTargetBehavior;
use buck2_server_ctx::ctx::ServerCommandContextTrait;
use buck2_server_ctx::partial_result_dispatcher::NoPartialResult;
use buck2_server_ctx::partial_result_dispatcher::PartialResultDispatcher;
use buck2_server_ctx::pattern::parse_patterns_from_cli_args;
use buck2_server_ctx::pattern::target_platform_from_client_context;
use buck2_server_ctx::template::run_server_command;
use buck2_server_ctx::template::ServerCommandTemplate;
use dice::DiceTransaction;

use crate::commands::targets::fmt::print_target_call_stack_after_target;

pub async fn configured_targets_command(
    server_ctx: &dyn ServerCommandContextTrait,
    partial_result_dispatcher: PartialResultDispatcher<NoPartialResult>,
    req: ConfiguredTargetsRequest,
) -> anyhow::Result<ConfiguredTargetsResponse> {
    run_server_command(
        ConfiguredTargetsServerCommand { req },
        server_ctx,
        partial_result_dispatcher,
    )
    .await
}

struct ConfiguredTargetsServerCommand {
    req: ConfiguredTargetsRequest,
}

#[async_trait]
impl ServerCommandTemplate for ConfiguredTargetsServerCommand {
    type StartEvent = buck2_data::ConfiguredTargetsCommandStart;
    type EndEvent = buck2_data::ConfiguredTargetsCommandEnd;
    type Response = ConfiguredTargetsResponse;
    type PartialResult = NoPartialResult;

    fn is_success(&self, response: &ConfiguredTargetsResponse) -> bool {
        let ConfiguredTargetsResponse {
            serialized_targets_output,
        } = response;
        let _ignore = serialized_targets_output;
        true
    }

    async fn command(
        &self,
        server_ctx: &dyn ServerCommandContextTrait,
        _partial_result_dispatcher: PartialResultDispatcher<NoPartialResult>,
        mut ctx: DiceTransaction,
    ) -> anyhow::Result<ConfiguredTargetsResponse> {
        // TODO(nga): this should accept `ConfiguredTargetPatternExtra`. And handle the universe.
        let parsed_patterns = parse_patterns_from_cli_args::<TargetPatternExtra>(
            &mut ctx,
            &self.req.target_patterns,
            server_ctx.working_dir(),
        )
        .await?;

        let client_ctx = self.req.client_context()?;

        let target_call_stacks = client_ctx.target_call_stacks;

        let global_target_platform =
            target_platform_from_client_context(client_ctx, server_ctx, &ctx).await?;

        let skip_missing_targets = MissingTargetBehavior::from_skip(self.req.skip_missing_targets);

        let compatible_targets = load_compatible_patterns(
            &ctx,
            parsed_patterns,
            global_target_platform,
            skip_missing_targets,
        )
        .await?;

        let mut serialized_targets_output = String::new();
        for node in compatible_targets.into_iter() {
            // TODO(nga): we should probably get rid of forward nodes.
            let nodes = iter::once(&node).chain(node.forward_target());

            for node in nodes {
                writeln!(serialized_targets_output, "{}", node.label())?;
                if target_call_stacks {
                    print_target_call_stack_after_target(
                        &mut serialized_targets_output,
                        node.call_stack().as_deref(),
                    );
                }
            }
        }

        Ok(ConfiguredTargetsResponse {
            serialized_targets_output,
        })
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use std::collections::hash_map::DefaultHasher;
use std::collections::HashMap;
use std::hash::Hash;
use std::hash::Hasher;
use std::net::Ipv4Addr;
use std::net::SocketAddr;
use std::net::TcpListener;
use std::process::Stdio;

use anyhow::Context;
use async_trait::async_trait;
use buck2_artifact::artifact::artifact_type::Artifact;
use buck2_build_api::actions::artifact::get_artifact_fs::GetArtifactFs;
use buck2_build_api::analysis::calculation::RuleAnalysisCalculation;
use buck2_build_api::artifact_groups::ArtifactGroup;
use buck2_build_api::build::materialize_artifact_group;
use buck2_build_api::build::MaterializationContext;
use buck2_build_api::context::HasBuildContextData;
use buck2_build_api::interpreter::rule_defs::cmd_args::AbsCommandLineContext;
use buck2_build_api::interpreter::rule_defs::cmd_args::CommandLineArgLike;
use buck2_build_api::interpreter::rule_defs::cmd_args::SimpleCommandLineArtifactVisitor;
use buck2_build_api::interpreter::rule_defs::provider::builtin::install_info::InstallInfoCallable;
use buck2_build_api::interpreter::rule_defs::provider::builtin::run_info::RunInfo;
use buck2_cli_proto::HasClientContext;
use buck2_cli_proto::InstallRequest;
use buck2_cli_proto::InstallResponse;
use buck2_common::client_utils::get_channel_tcp;
use buck2_common::dice::cells::HasCellResolver;
use buck2_common::dice::file_ops::HasFileOps;
use buck2_common::executor_config::PathSeparatorKind;
use buck2_common::file_ops::FileDigest;
use buck2_common::pattern::resolve::resolve_target_patterns;
use buck2_core::directory::DirectoryEntry;
use buck2_core::fs::artifact_path_resolver::ArtifactFs;
use buck2_core::fs::fs_util;
use buck2_core::fs::paths::abs_norm_path::AbsNormPathBuf;
use buck2_core::fs::paths::forward_rel_path::ForwardRelativePathBuf;
use buck2_core::package::PackageLabel;
use buck2_core::pattern::pattern_type::ConfiguredProvidersPatternExtra;
use buck2_core::pattern::pattern_type::ProvidersPatternExtra;
use buck2_core::provider::label::ConfiguredProvidersLabel;
use buck2_core::provider::label::ProvidersName;
use buck2_core::target::name::TargetName;
use buck2_data::InstallEventInfoEnd;
use buck2_data::InstallEventInfoStart;
use buck2_events::dispatch::span_async;
use buck2_execute::artifact::artifact_dyn::ArtifactDyn;
use buck2_execute::artifact::fs::ExecutorFs;
use buck2_execute::artifact_value::ArtifactValue;
use buck2_execute::directory::ActionDirectoryMember;
use buck2_execute::materialize::materializer::HasMaterializer;
use buck2_install_proto::installer_client::InstallerClient;
use buck2_install_proto::FileReadyRequest;
use buck2_install_proto::InstallInfoRequest;
use buck2_install_proto::ShutdownRequest;
use buck2_node::nodes::frontend::TargetGraphCalculation;
use buck2_node::target_calculation::ConfiguredTargetCalculation;
use buck2_server_ctx::ctx::ServerCommandContextTrait;
use buck2_server_ctx::partial_result_dispatcher::NoPartialResult;
use buck2_server_ctx::partial_result_dispatcher::PartialResultDispatcher;
use buck2_server_ctx::pattern::parse_patterns_from_cli_args;
use buck2_server_ctx::pattern::target_platform_from_client_context;
use buck2_server_ctx::template::run_server_command;
use buck2_server_ctx::template::ServerCommandTemplate;
use buck2_util::process::background_command;
use chrono::DateTime;
use chrono::NaiveDateTime;
use chrono::Utc;
use dice::DiceComputations;
use dice::DiceTransaction;
use dupe::Dupe;
use futures::future::try_join;
use futures::future::try_join_all;
use futures::future::FutureExt;
use futures::stream::StreamExt;
use futures::stream::TryStreamExt;
use starlark_map::small_map::SmallMap;
use tokio::sync::mpsc;
use tonic::transport::Channel;

#[derive(Debug, thiserror::Error)]
pub enum InstallError {
    #[error("Target {1}:{0} cannot be installed as it does not expose an InstallInfo provider")]
    NoInstallProvider(TargetName, PackageLabel),

    #[error("Installer target `{0}` doesn't expose RunInfo provider")]
    NoRunInfoProvider(TargetName),

    #[error(
        "Installer failed to process file ready request for `{install_id}`. Artifact: `{artifact}` located at `{path}`. Error message: `{err}`\n. More details can be found at `{installer_log}`"
    )]
    ProcessingFileReadyFailure {
        install_id: String,
        artifact: String,
        path: AbsNormPathBuf,
        err: String,
        installer_log: String,
    },

    #[error("Installer failed for `{install_id}` with `{err}`")]
    InternalInstallerFailure { install_id: String, err: String },

    #[error("Communication with the installer failed with `{err}`")]
    InstallerCommunicationFailure { err: String },

    #[error("Incorrect seconds/nanos argument")]
    NativeDateTime,
}

async fn get_installer_log_directory(
    server_ctx: &dyn ServerCommandContextTrait,
    ctx: &DiceComputations,
) -> anyhow::Result<AbsNormPathBuf> {
    let out_path = ctx.get_buck_out_path().await?;
    let filesystem = server_ctx.project_root();
    let buck_out_path = out_path
        .root()
        .as_forward_relative_path()
        .resolve(filesystem.root());
    let install_log_dir = buck_out_path.join(ForwardRelativePathBuf::unchecked_new(
        "installer".to_owned(),
    ));
    fs_util::create_dir_all(&install_log_dir)?;
    Ok(install_log_dir)
}

pub async fn install_command(
    ctx: &dyn ServerCommandContextTrait,
    partial_result_dispatcher: PartialResultDispatcher<NoPartialResult>,
    req: InstallRequest,
) -> anyhow::Result<InstallResponse> {
    run_server_command(InstallServerCommand { req }, ctx, partial_result_dispatcher).await
}

struct InstallServerCommand {
    req: InstallRequest,
}

#[async_trait]
impl ServerCommandTemplate for InstallServerCommand {
    type StartEvent = buck2_data::InstallCommandStart;
    type EndEvent = buck2_data::InstallCommandEnd;
    type Response = InstallResponse;
    type PartialResult = NoPartialResult;

    fn end_event(&self, _response: &anyhow::Result<Self::Response>) -> Self::EndEvent {
        buck2_data::InstallCommandEnd {
            unresolved_target_patterns: self.req.target_patterns.clone(),
        }
    }

    async fn command(
        &self,
        server_ctx: &dyn ServerCommandContextTrait,
        _partial_result_dispatcher: PartialResultDispatcher<Self::PartialResult>,
        ctx: DiceTransaction,
    ) -> anyhow::Result<Self::Response> {
        install(server_ctx, ctx, &self.req).await
    }

    fn is_success(&self, _response: &Self::Response) -> bool {
        // No response if we failed.
        true
    }
}

async fn install(
    server_ctx: &dyn ServerCommandContextTrait,
    mut ctx: DiceTransaction,
    request: &InstallRequest,
) -> anyhow::Result<InstallResponse> {
    let cwd = server_ctx.working_dir();

    let cell_resolver = ctx.get_cell_resolver().await?;

    let client_ctx = request.client_context()?;
    let global_target_platform =
        target_platform_from_client_context(client_ctx, server_ctx, &ctx).await?;

    let materializations = MaterializationContext::force_materializations();
    let materializations = &materializations; // Don't move this below.

    // Note <TargetName> does not return the providers
    let parsed_patterns = parse_patterns_from_cli_args::<ConfiguredProvidersPatternExtra>(
        &mut ctx,
        &request.target_patterns,
        cwd,
    )
    .await?;
    server_ctx.log_target_pattern(&parsed_patterns);

    ctx.per_transaction_data()
        .get_materializer()
        .log_materializer_state(server_ctx.events());

    let resolved_pattern =
        resolve_target_patterns(&cell_resolver, &parsed_patterns, &ctx.file_ops()).await?;

    let resolved_pattern = resolved_pattern
        .convert_pattern()
        .context("Install with explicit configuration pattern is not supported yet")?;

    let mut installer_to_files_map = HashMap::new();
    for (package, spec) in resolved_pattern.specs {
        let ctx = &ctx;
        let targets: Vec<(TargetName, ProvidersPatternExtra)> = match spec {
            buck2_core::pattern::PackageSpec::Targets(targets) => targets,
            buck2_core::pattern::PackageSpec::All => {
                let interpreter_results = ctx.get_interpreter_results(package.dupe()).await?;
                interpreter_results
                    .targets()
                    .keys()
                    .map(|target| {
                        (
                            target.to_owned(),
                            ProvidersPatternExtra {
                                providers: ProvidersName::Default,
                            },
                        )
                    })
                    .collect()
            }
        };
        for (target_name, providers) in targets {
            let label = providers.into_providers_label(package.dupe(), target_name.as_ref());
            let providers_label = ctx
                .get_configured_provider_label(&label, global_target_platform.dupe().as_ref())
                .await?;
            let frozen_providers = ctx
                .get_providers(&providers_label)
                .await?
                .require_compatible()?;
            let providers = frozen_providers.provider_collection();
            match providers.get_provider(InstallInfoCallable::provider_id_t()) {
                Some(install_info) => {
                    let install_id = format!("{}", providers_label.target());
                    let installer_label = install_info.get_installer()?;
                    installer_to_files_map
                        .entry(installer_label)
                        .or_insert_with(Vec::new)
                        .push((install_id, install_info));
                }
                None => {
                    return Err(InstallError::NoInstallProvider(
                        label.target().name().to_owned(),
                        package.dupe(),
                    )
                    .into());
                }
            };
        }
    }

    let install_log_dir = &get_installer_log_directory(server_ctx, &ctx).await?;

    let mut install_requests = Vec::with_capacity(installer_to_files_map.len());
    for (installer_label, install_info_vector) in &installer_to_files_map {
        let ctx = &ctx;
        let installer_run_args = &request.installer_run_args;

        let mut install_files_vector: Vec<(&String, SmallMap<_, _>)> = Vec::new();
        for (install_id, install_info) in install_info_vector {
            let install_files = install_info.get_files()?;
            install_files_vector.push((install_id, install_files));
        }

        let handle_install_request_future = async move {
            handle_install_request(
                ctx,
                materializations,
                install_log_dir,
                &install_files_vector,
                installer_label,
                installer_run_args,
                request.installer_debug,
            )
            .await
        };
        install_requests.push(handle_install_request_future);
    }

    try_join_all(install_requests)
        .await
        .context("Interaction with installer failed.")?;

    Ok(InstallResponse {})
}

fn get_random_tcp_port() -> anyhow::Result<u16> {
    let bind_address = std::net::Ipv4Addr::LOCALHOST.into();
    let socket_addr = SocketAddr::new(bind_address, 0);
    let tcp_port = TcpListener::bind(socket_addr)?.local_addr()?.port();
    Ok(tcp_port)
}

fn get_timestamp_as_string() -> anyhow::Result<String> {
    let nt = NaiveDateTime::from_timestamp_opt(Utc::now().timestamp(), 0)
        .context(InstallError::NativeDateTime)?;
    let dt: DateTime<Utc> = DateTime::from_utc(nt, Utc);
    Ok(dt.format("%Y%m%d-%H%M%S").to_string())
}

fn calculate_hash<T: Hash>(t: &T) -> u64 {
    let mut s = DefaultHasher::new();
    t.hash(&mut s);
    s.finish()
}

async fn handle_install_request<'a>(
    ctx: &'a DiceComputations,
    materializations: &'a MaterializationContext,
    install_log_dir: &AbsNormPathBuf,
    install_files_slice: &[(&String, SmallMap<&str, Artifact>)],
    installer_label: &ConfiguredProvidersLabel,
    initial_installer_run_args: &[String],
    installer_debug: bool,
) -> anyhow::Result<()> {
    let (files_tx, files_rx) = mpsc::unbounded_channel();
    let build_files = async move {
        build_files(ctx, materializations, install_files_slice, files_tx).await?;
        anyhow::Ok(())
    };
    let build_installer_and_connect = async move {
        // FIXME: The random unused tcp port might be available when get_random_tcp_port() is called,
        // but when the installer tries to bind on it, someone else might bind on it.
        // TODO: choose unused tcp port on installer side.
        // The way communication may happen:
        // 1. buck2 passes a temp file for a tcp port output.
        // 2. installer app choose unused tcp port and writes it into the passed file.
        // 3. buck2 reads tcp port from file and use it to connect to the installer app. (`connect_to_installer` function)
        let tcp_port = get_random_tcp_port()?;

        let installer_log_filename = format!(
            "{}/installer_{}_{}.log",
            install_log_dir,
            get_timestamp_as_string()?,
            calculate_hash(&installer_label.target().name())
        );

        let mut installer_run_args: Vec<String> = initial_installer_run_args.to_vec();

        installer_run_args.extend(vec![
            "--tcp-port".to_owned(),
            tcp_port.to_string(),
            "--log-path".to_owned(),
            installer_log_filename.to_owned(),
        ]);

        build_launch_installer(
            ctx,
            materializations,
            installer_label,
            &installer_run_args,
            installer_debug,
        )
        .await?;

        let client: InstallerClient<Channel> = connect_to_installer(tcp_port).await?;
        let artifact_fs = ctx.get_artifact_fs().await?;

        for (install_id, install_files) in install_files_slice {
            send_install_info(client.clone(), install_id, install_files, &artifact_fs).await?;
        }

        let send_files_result = tokio_stream::wrappers::UnboundedReceiverStream::new(files_rx)
            .map(anyhow::Ok)
            .try_for_each_concurrent(None, |file| {
                send_file(
                    file,
                    &artifact_fs,
                    client.clone(),
                    installer_log_filename.to_owned(),
                )
            })
            .await;
        send_shutdown_command(client.clone()).await?;
        send_files_result.context("Failed to send artifacts to installer")?;
        anyhow::Ok(())
    };
    try_join(build_installer_and_connect, build_files).await?;
    anyhow::Ok(())
}

async fn send_install_info(
    mut client: InstallerClient<Channel>,
    install_id: &str,
    install_files: &SmallMap<&str, Artifact>,
    artifact_fs: &ArtifactFs,
) -> anyhow::Result<()> {
    let mut files_map = HashMap::new();
    for (file_name, artifact) in install_files {
        let artifact_path = &artifact_fs
            .fs()
            .resolve(&artifact.resolve_path(artifact_fs)?);
        files_map
            .entry((*file_name).to_owned())
            .or_insert_with(|| artifact_path.to_string());
    }

    let install_info_request = tonic::Request::new(InstallInfoRequest {
        install_id: install_id.to_owned(),
        files: files_map,
    });

    let response_result = client.install(install_info_request).await;
    let install_info_response = match response_result {
        Ok(r) => r.into_inner(),
        Err(status) => {
            return Err(InstallError::InternalInstallerFailure {
                install_id: install_id.to_owned(),
                err: status.message().to_owned(),
            }
            .into());
        }
    };

    if install_info_response.install_id != install_id {
        send_shutdown_command(client.clone()).await?;
        return Err(anyhow::anyhow!(
            "Received install id: {} doesn't match with the sent one: {}",
            install_info_response.install_id,
            &install_id
        ));
    }

    Ok(())
}

async fn send_shutdown_command(mut client: InstallerClient<Channel>) -> anyhow::Result<()> {
    let response_result = client
        .shutdown_server(tonic::Request::new(ShutdownRequest {}))
        .await;

    return match response_result {
        Ok(_) => Ok(()),
        Err(status) => Err(InstallError::InstallerCommunicationFailure {
            err: status.message().to_owned(),
        }
        .into()),
    };
}

async fn build_launch_installer<'a>(
    ctx: &'a DiceComputations,
    materializations: &'a MaterializationContext,
    providers_label: &ConfiguredProvidersLabel,
    installer_run_args: &[String],
    installer_log_console: bool,
) -> anyhow::Result<()> {
    let frozen_providers = ctx
        .get_providers(providers_label)
        .await?
        .require_compatible()?;

    if let Some(installer_run_info) =
        RunInfo::from_providers(frozen_providers.provider_collection())
    {
        let (inputs, run_args) = {
            let artifact_fs = ctx.get_artifact_fs().await?;
            let mut artifact_visitor = SimpleCommandLineArtifactVisitor::new();
            installer_run_info.visit_artifacts(&mut artifact_visitor)?;
            // Produce arguments for local platform.
            let path_separator = if cfg!(windows) {
                PathSeparatorKind::Windows
            } else {
                PathSeparatorKind::Unix
            };
            let executor_fs = ExecutorFs::new(&artifact_fs, path_separator);
            let mut run_args = Vec::<String>::new();
            let mut ctx = AbsCommandLineContext::new(&executor_fs);
            installer_run_info.add_to_command_line(&mut run_args, &mut ctx)?;
            (artifact_visitor.inputs, run_args)
        };
        // returns IndexMap<ArtifactGroup,ArtifactGroupValues>;
        try_join_all(inputs.into_iter().map(|input| async move {
            materialize_artifact_group(ctx, &input, materializations)
                .await
                .map(|value| (input, value))
        }))
        .await
        .context("Failed to build installer")?;
        background_command(&run_args[0])
            .args(&run_args[1..])
            .args(installer_run_args)
            .stderr(get_stdio(installer_log_console)?)
            .spawn()
            .context("Failed to spawn installer")?;

        Ok(())
    } else {
        Err(InstallError::NoRunInfoProvider(providers_label.target().name().to_owned()).into())
    }
}

fn get_stdio(log_installer_console: bool) -> anyhow::Result<Stdio> {
    if log_installer_console {
        Ok(Stdio::inherit())
    } else {
        Ok(Stdio::null())
    }
}

#[derive(Debug)]
pub struct FileResult {
    install_id: String,
    name: String,
    artifact: Artifact,
    artifact_value: ArtifactValue,
}

async fn build_files(
    ctx: &DiceComputations,
    materializations: &MaterializationContext,
    install_files_slice: &[(&String, SmallMap<&str, Artifact>)],
    tx: mpsc::UnboundedSender<FileResult>,
) -> anyhow::Result<()> {
    let mut file_outputs = Vec::with_capacity(install_files_slice.len());
    for (install_id, file_info) in install_files_slice {
        for (name, artifact) in file_info.into_iter() {
            file_outputs.push((
                install_id,
                name,
                ArtifactGroup::Artifact(artifact.to_owned()),
                tx.clone(),
            ));
        }
    }

    try_join_all(file_outputs.into_iter().map(
        |(install_id, name, artifact, tx_clone)| async move {
            let artifact_values =
                materialize_artifact_group(ctx, &artifact, materializations).await?;
            for (artifact, artifact_value) in artifact_values.iter() {
                let file_result = FileResult {
                    install_id: (*install_id).to_owned(),
                    name: (*name).to_owned(),
                    artifact: artifact.to_owned(),
                    artifact_value: artifact_value.to_owned(),
                };
                tx_clone.send(file_result)?;
            }
            anyhow::Ok(())
        },
    ))
    .await?;
    Ok(())
}

async fn connect_to_installer(tcp_port: u16) -> anyhow::Result<InstallerClient<Channel>> {
    use std::time::Duration;

    use buck2_common::client_utils::retrying;

    // These numbers might need to be configured based on the installer
    let initial_delay = Duration::from_millis(100);
    let max_delay = Duration::from_millis(500);
    let timeout = Duration::from_secs(120);

    span_async(
        buck2_data::ConnectToInstallerStart {
            tcp_port: tcp_port.into(),
        },
        async move {
            let channel = retrying(initial_delay, max_delay, timeout, async || {
                get_channel_tcp(Ipv4Addr::LOCALHOST, tcp_port).await
            })
            .await
            .context("Failed to connect to the installer using TCP")?;

            Ok(InstallerClient::new(channel)
                .max_encoding_message_size(usize::MAX)
                .max_decoding_message_size(usize::MAX))
        }
        .map(|res| (res, buck2_data::ConnectToInstallerEnd {})),
    )
    .await
}

async fn send_file(
    file: FileResult,
    artifact_fs: &ArtifactFs,
    mut client: InstallerClient<Channel>,
    install_log: String,
) -> anyhow::Result<()> {
    let install_id = file.install_id;
    let name = file.name;
    let artifact = file.artifact;

    enum Data<'a> {
        Digest(&'a FileDigest), // NOTE: A misnommer, this is rather BlobDigest.
        Symlink(String),
    }

    let data = match &file.artifact_value.entry() {
        DirectoryEntry::Dir(dir) => Data::Digest(dir.fingerprint().data()),
        DirectoryEntry::Leaf(ActionDirectoryMember::File(file)) => Data::Digest(file.digest.data()),
        DirectoryEntry::Leaf(ActionDirectoryMember::Symlink(symlink)) => {
            // todo(@lebentle) Use for now to unblock exopackage,
            // but should follow symlink and validate the target exists and send that
            Data::Symlink(symlink.target().as_str().to_owned())
        }
        DirectoryEntry::Leaf(ActionDirectoryMember::ExternalSymlink(symlink)) => {
            Data::Symlink(symlink.with_full_target()?.target_str().to_owned())
        }
    };

    let (digest, size, digest_algorithm) = match data {
        Data::Digest(d) => (
            d.raw_digest().to_string(),
            d.size(),
            d.raw_digest().algorithm().to_string(),
        ),
        Data::Symlink(sym) => (format!("re-symlink:{}", sym), 0, "".to_owned()), // Messy :(
    };

    let path = &artifact_fs
        .fs()
        .resolve(&artifact.resolve_path(artifact_fs)?);
    let request = tonic::Request::new(FileReadyRequest {
        install_id: install_id.to_owned(),
        name: name.to_owned(),
        digest,
        digest_algorithm,
        size,
        path: path.to_string(),
    });

    let start = InstallEventInfoStart {
        artifact_name: name.to_owned(),
        file_path: path.to_string(),
    };
    let end = InstallEventInfoEnd {};
    span_async(start, async {
        let mut outcome: anyhow::Result<()> = Ok(());
        let response_result = client.file_ready(request).await;
        let response = match response_result {
            Ok(r) => r.into_inner(),
            Err(status) => {
                return (
                    Err(InstallError::ProcessingFileReadyFailure {
                        install_id: install_id.to_owned(),
                        artifact: name,
                        path: path.to_owned(),
                        err: status.message().to_owned(),
                        installer_log: install_log.to_owned(),
                    }
                    .into()),
                    end,
                );
            }
        };

        if response.install_id != install_id {
            outcome = Err(InstallError::ProcessingFileReadyFailure {
                install_id: install_id.to_owned(),
                artifact: name.to_owned(),
                path: path.to_owned(),
                err: format!(
                    "Received install id: {} doesn't match with the sent one: {}",
                    response.install_id, &install_id
                ),
                installer_log: install_log.to_owned(),
            }
            .into());
        }

        if let Some(error_detail) = response.error_detail {
            outcome = Err(InstallError::ProcessingFileReadyFailure {
                install_id: install_id.to_owned(),
                artifact: name.to_owned(),
                path: path.to_owned(),
                err: error_detail.message,
                installer_log: install_log.to_owned(),
            }
            .into());
        }
        (outcome, end)
    })
    .await?;
    Ok(())
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

pub mod build;
pub mod configured_targets;
pub mod install;
pub mod query;
pub mod targets;
pub mod targets_show_outputs;


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use std::sync::Arc;

use async_trait::async_trait;
use buck2_artifact::artifact::artifact_type::Artifact;
use buck2_build_api::actions::artifact::get_artifact_fs::GetArtifactFs;
use buck2_build_api::analysis::calculation::RuleAnalysisCalculation;
use buck2_cli_proto::targets_show_outputs_response::TargetPaths;
use buck2_cli_proto::HasClientContext;
use buck2_cli_proto::TargetsRequest;
use buck2_cli_proto::TargetsShowOutputsResponse;
use buck2_common::dice::cells::HasCellResolver;
use buck2_common::dice::file_ops::HasFileOps;
use buck2_common::pattern::resolve::resolve_target_patterns;
use buck2_common::pattern::resolve::ResolvedPattern;
use buck2_core::cells::CellResolver;
use buck2_core::package::PackageLabel;
use buck2_core::pattern::pattern_type::ProvidersPatternExtra;
use buck2_core::pattern::PackageSpec;
use buck2_core::pattern::ParsedPattern;
use buck2_core::provider::label::ConfiguredProvidersLabel;
use buck2_core::provider::label::ProvidersLabel;
use buck2_core::target::label::TargetLabel;
use buck2_execute::artifact::artifact_dyn::ArtifactDyn;
use buck2_node::nodes::eval_result::EvaluationResult;
use buck2_node::nodes::frontend::TargetGraphCalculation;
use buck2_node::target_calculation::ConfiguredTargetCalculation;
use buck2_server_ctx::ctx::ServerCommandContextTrait;
use buck2_server_ctx::partial_result_dispatcher::NoPartialResult;
use buck2_server_ctx::partial_result_dispatcher::PartialResultDispatcher;
use buck2_server_ctx::pattern::parse_patterns_from_cli_args;
use buck2_server_ctx::pattern::target_platform_from_client_context;
use buck2_server_ctx::template::run_server_command;
use buck2_server_ctx::template::ServerCommandTemplate;
use dice::DiceComputations;
use dice::DiceTransaction;
use dupe::Dupe;
use futures::future::FutureExt;
use futures::stream::FuturesUnordered;
use gazebo::prelude::VecExt;
use tokio_stream::StreamExt;

struct TargetsArtifacts {
    providers_label: ConfiguredProvidersLabel,
    artifacts: Vec<Artifact>,
}

pub async fn targets_show_outputs_command(
    ctx: &dyn ServerCommandContextTrait,
    partial_result_dispatcher: PartialResultDispatcher<NoPartialResult>,
    req: TargetsRequest,
) -> anyhow::Result<TargetsShowOutputsResponse> {
    run_server_command(
        TargetsShowOutputsServerCommand { req },
        ctx,
        partial_result_dispatcher,
    )
    .await
}

struct TargetsShowOutputsServerCommand {
    req: TargetsRequest,
}

#[async_trait]
impl ServerCommandTemplate for TargetsShowOutputsServerCommand {
    type StartEvent = buck2_data::TargetsCommandStart;
    type EndEvent = buck2_data::TargetsCommandEnd;
    type Response = buck2_cli_proto::TargetsShowOutputsResponse;
    type PartialResult = NoPartialResult;

    async fn command(
        &self,
        server_ctx: &dyn ServerCommandContextTrait,
        _partial_result_dispatcher: PartialResultDispatcher<Self::PartialResult>,
        ctx: DiceTransaction,
    ) -> anyhow::Result<Self::Response> {
        targets_show_outputs(server_ctx, ctx, &self.req).await
    }

    fn is_success(&self, _response: &Self::Response) -> bool {
        // No response if we failed.
        true
    }
}

async fn targets_show_outputs(
    server_ctx: &dyn ServerCommandContextTrait,
    mut ctx: DiceTransaction,
    request: &TargetsRequest,
) -> anyhow::Result<TargetsShowOutputsResponse> {
    let cwd = server_ctx.working_dir();

    let cell_resolver = ctx.get_cell_resolver().await?;

    let client_ctx = request.client_context()?;
    let target_platform = target_platform_from_client_context(client_ctx, server_ctx, &ctx).await?;

    let parsed_patterns = parse_patterns_from_cli_args::<ProvidersPatternExtra>(
        &mut ctx,
        &request.target_patterns,
        cwd,
    )
    .await?;

    let artifact_fs = ctx.get_artifact_fs().await?;

    let mut targets_paths = Vec::new();

    for targets_artifacts in retrieve_targets_artifacts_from_patterns(
        &ctx,
        &target_platform,
        &parsed_patterns,
        &cell_resolver,
    )
    .await?
    {
        let mut paths = Vec::new();
        for artifact in targets_artifacts.artifacts {
            let path = artifact.resolve_path(&artifact_fs)?;
            paths.push(path.to_string());
        }
        targets_paths.push(TargetPaths {
            target: targets_artifacts.providers_label.unconfigured().to_string(),
            paths,
        })
    }

    Ok(TargetsShowOutputsResponse { targets_paths })
}

async fn retrieve_targets_artifacts_from_patterns(
    ctx: &DiceComputations,
    global_target_platform: &Option<TargetLabel>,
    parsed_patterns: &[ParsedPattern<ProvidersPatternExtra>],
    cell_resolver: &CellResolver,
) -> anyhow::Result<Vec<TargetsArtifacts>> {
    let resolved_pattern =
        resolve_target_patterns(cell_resolver, parsed_patterns, &ctx.file_ops()).await?;

    retrieve_artifacts_for_targets(ctx, resolved_pattern, global_target_platform.to_owned()).await
}

async fn retrieve_artifacts_for_targets(
    ctx: &DiceComputations,
    spec: ResolvedPattern<ProvidersPatternExtra>,
    global_target_platform: Option<TargetLabel>,
) -> anyhow::Result<Vec<TargetsArtifacts>> {
    let futs: FuturesUnordered<_> = spec
        .specs
        .into_iter()
        .map(|(package, spec)| {
            let global_target_platform = global_target_platform.dupe();
            async move {
                {
                    let res = ctx.get_interpreter_results(package.dupe()).await?;
                    retrieve_artifacts_for_spec(
                        ctx,
                        package.dupe(),
                        spec,
                        global_target_platform,
                        res,
                    )
                    .await
                }
            }
            .boxed()
        })
        .collect();

    futures::pin_mut!(futs);

    let mut results = Vec::new();
    while let Some(mut targets_artifacts) = futs.try_next().await? {
        results.append(&mut targets_artifacts);
    }

    Ok(results)
}

async fn retrieve_artifacts_for_spec(
    ctx: &DiceComputations,
    package: PackageLabel,
    spec: PackageSpec<ProvidersPatternExtra>,
    global_target_platform: Option<TargetLabel>,
    res: Arc<EvaluationResult>,
) -> anyhow::Result<Vec<TargetsArtifacts>> {
    let available_targets = res.targets();

    let todo_targets: Vec<(ProvidersLabel, Option<TargetLabel>)> = match spec {
        PackageSpec::All => available_targets
            .keys()
            .map(|t| {
                (
                    ProvidersLabel::default_for(TargetLabel::new(package.dupe(), t)),
                    global_target_platform.dupe(),
                )
            })
            .collect(),
        PackageSpec::Targets(targets) => {
            for (target_name, _) in &targets {
                res.resolve_target(target_name)?;
            }
            targets.into_map(|(target_name, providers)| {
                (
                    providers.into_providers_label(package.dupe(), target_name.as_ref()),
                    global_target_platform.dupe(),
                )
            })
        }
    };

    let mut futs: FuturesUnordered<_> = todo_targets
        .into_iter()
        .map(|(providers_label, target_platform)| {
            retrieve_artifacts_for_provider_label(ctx, providers_label, target_platform)
        })
        .collect();

    let mut outputs = Vec::new();
    while let Some(targets_artifacts) = futs.next().await {
        outputs.push(targets_artifacts?);
    }

    Ok(outputs)
}

async fn retrieve_artifacts_for_provider_label(
    ctx: &DiceComputations,
    providers_label: ProvidersLabel,
    target_platform: Option<TargetLabel>,
) -> anyhow::Result<TargetsArtifacts> {
    let providers_label = ctx
        .get_configured_provider_label(&providers_label, target_platform.as_ref())
        .await?;

    let providers = ctx
        .get_providers(&providers_label)
        .await?
        .require_compatible()?;

    let collection = providers.provider_collection();

    let mut artifacts = Vec::new();
    collection
        .default_info()
        .for_each_default_output_artifact_only(&mut |o| {
            artifacts.push(o);
            Ok(())
        })?;

    Ok(TargetsArtifacts {
        providers_label,
        artifacts,
    })
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use std::collections::BTreeMap;
use std::io::BufWriter;
use std::sync::Arc;

use anyhow::Context as _;
use async_trait::async_trait;
use buck2_build_api::actions::artifact::get_artifact_fs::GetArtifactFs;
use buck2_build_api::build;
use buck2_build_api::build::BuildEvent;
use buck2_build_api::build::BuildTargetResult;
use buck2_build_api::build::ConvertMaterializationContext;
use buck2_build_api::build::HasCreateUnhashedSymlinkLock;
use buck2_build_api::build::MaterializationContext;
use buck2_build_api::build::ProvidersToBuild;
use buck2_build_api::query::oneshot::QUERY_FRONTEND;
use buck2_cli_proto::build_request::build_providers::Action as BuildProviderAction;
use buck2_cli_proto::build_request::BuildProviders;
use buck2_cli_proto::build_request::Materializations;
use buck2_cli_proto::HasClientContext;
use buck2_common::dice::cells::HasCellResolver;
use buck2_common::dice::file_ops::HasFileOps;
use buck2_common::legacy_configs::dice::HasLegacyConfigs;
use buck2_common::pattern::resolve::resolve_target_patterns;
use buck2_common::pattern::resolve::ResolvedPattern;
use buck2_core::fs::fs_util;
use buck2_core::pattern::pattern_type::ConfiguredProvidersPatternExtra;
use buck2_core::pattern::pattern_type::ProvidersPatternExtra;
use buck2_core::pattern::PackageSpec;
use buck2_core::pattern::ParsedPattern;
use buck2_core::provider::label::ConfiguredProvidersLabel;
use buck2_core::provider::label::ProvidersLabel;
use buck2_core::provider::label::ProvidersName;
use buck2_core::target::label::TargetLabel;
use buck2_events::dispatch::console_message;
use buck2_events::dispatch::span_async;
use buck2_execute::materialize::materializer::HasMaterializer;
use buck2_node::configured_universe::CqueryUniverse;
use buck2_node::load_patterns::MissingTargetBehavior;
use buck2_node::nodes::eval_result::EvaluationResult;
use buck2_node::nodes::frontend::TargetGraphCalculation;
use buck2_node::nodes::unconfigured::TargetNode;
use buck2_node::target_calculation::ConfiguredTargetCalculation;
use buck2_server_ctx::ctx::ServerCommandContextTrait;
use buck2_server_ctx::partial_result_dispatcher::NoPartialResult;
use buck2_server_ctx::partial_result_dispatcher::PartialResultDispatcher;
use buck2_server_ctx::pattern::parse_patterns_from_cli_args;
use buck2_server_ctx::pattern::target_platform_from_client_context;
use buck2_server_ctx::template::run_server_command;
use buck2_server_ctx::template::ServerCommandTemplate;
use dice::DiceComputations;
use dice::DiceTransaction;
use dupe::Dupe;
use futures::future::FutureExt;
use futures::future::TryFutureExt;
use futures::stream::futures_unordered::FuturesUnordered;
use futures::stream::Stream;
use futures::stream::StreamExt;
use itertools::Itertools;

use crate::commands::build::results::build_report::BuildReportCollector;
use crate::commands::build::results::providers::ProvidersPrinter;
use crate::commands::build::results::result_report::ResultReporter;
use crate::commands::build::results::result_report::ResultReporterOptions;
use crate::commands::build::results::BuildOwner;
use crate::commands::build::results::BuildResultCollector;
use crate::commands::build::unhashed_outputs::create_unhashed_outputs;

mod results;
mod unhashed_outputs;

pub async fn build_command(
    ctx: &dyn ServerCommandContextTrait,
    partial_result_dispatcher: PartialResultDispatcher<NoPartialResult>,
    req: buck2_cli_proto::BuildRequest,
) -> anyhow::Result<buck2_cli_proto::BuildResponse> {
    run_server_command(BuildServerCommand { req }, ctx, partial_result_dispatcher).await
}

struct BuildServerCommand {
    req: buck2_cli_proto::BuildRequest,
}

#[async_trait]
impl ServerCommandTemplate for BuildServerCommand {
    type StartEvent = buck2_data::BuildCommandStart;
    type EndEvent = buck2_data::BuildCommandEnd;
    type Response = buck2_cli_proto::BuildResponse;
    type PartialResult = NoPartialResult;

    fn end_event(&self, _response: &anyhow::Result<Self::Response>) -> Self::EndEvent {
        buck2_data::BuildCommandEnd {
            unresolved_target_patterns: self.req.target_patterns.clone(),
        }
    }

    async fn command(
        &self,
        server_ctx: &dyn ServerCommandContextTrait,
        _partial_result_dispatcher: PartialResultDispatcher<Self::PartialResult>,
        ctx: DiceTransaction,
    ) -> anyhow::Result<Self::Response> {
        build(server_ctx, ctx, &self.req).await
    }

    fn is_success(&self, response: &Self::Response) -> bool {
        response.error_messages.is_empty()
    }
}

enum TargetResolutionConfig {
    /// Resolve using target platform.
    Default(Option<TargetLabel>),
    /// Resolve in the universe.
    Universe(CqueryUniverse),
}

async fn build(
    server_ctx: &dyn ServerCommandContextTrait,
    mut ctx: DiceTransaction,
    request: &buck2_cli_proto::BuildRequest,
) -> anyhow::Result<buck2_cli_proto::BuildResponse> {
    // TODO(nmj): Move build report printing logic out of here.
    let fs = server_ctx.project_root();
    let cwd = server_ctx.working_dir();

    let build_opts = request
        .build_opts
        .as_ref()
        .expect("should have build options");

    let cell_resolver = ctx.get_cell_resolver().await?;

    let client_ctx = request.client_context()?;
    let global_target_platform =
        target_platform_from_client_context(client_ctx, server_ctx, &ctx).await?;

    let should_create_unhashed_links = ctx
        .parse_legacy_config_property(cell_resolver.root_cell(), "buck2", "create_unhashed_links")
        .await?;

    let parsed_patterns: Vec<ParsedPattern<ConfiguredProvidersPatternExtra>> =
        parse_patterns_from_cli_args(&mut ctx, &request.target_patterns, cwd).await?;
    server_ctx.log_target_pattern(&parsed_patterns);

    ctx.per_transaction_data()
        .get_materializer()
        .log_materializer_state(server_ctx.events());

    let resolved_pattern: ResolvedPattern<ConfiguredProvidersPatternExtra> =
        resolve_target_patterns(&cell_resolver, &parsed_patterns, &ctx.file_ops()).await?;

    let target_resolution_config: TargetResolutionConfig = if request.target_universe.is_empty() {
        TargetResolutionConfig::Default(global_target_platform)
    } else {
        TargetResolutionConfig::Universe(
            QUERY_FRONTEND
                .get()?
                .universe_from_literals(&ctx, cwd, &request.target_universe, global_target_platform)
                .await?,
        )
    };

    let artifact_fs = ctx.get_artifact_fs().await?;
    let build_providers = Arc::new(request.build_providers.clone().unwrap());
    let response_options = request.response_options.clone().unwrap_or_default();

    let mut result_collector = ResultReporter::new(
        &artifact_fs,
        ResultReporterOptions {
            return_outputs: response_options.return_outputs,
            return_default_other_outputs: response_options.return_default_other_outputs,
        },
    );

    let mut build_report_collector = if build_opts.unstable_print_build_report {
        Some(BuildReportCollector::new(
            server_ctx.events().trace_id(),
            &artifact_fs,
            server_ctx.project_root(),
            ctx.parse_legacy_config_property(
                cell_resolver.root_cell(),
                "build_report",
                "print_unconfigured_section",
            )
            .await?
            .unwrap_or(true),
            ctx.parse_legacy_config_property(
                cell_resolver.root_cell(),
                "build_report",
                "unstable_include_other_outputs",
            )
            .await?
            .unwrap_or(false),
        ))
    } else {
        None
    };

    let mut providers_printer = if request.unstable_print_providers {
        Some(ProvidersPrinter)
    } else {
        None
    };

    let mut result_collectors = vec![
        Some(&mut result_collector as &mut dyn BuildResultCollector),
        build_report_collector
            .as_mut()
            .map(|v| v as &mut dyn BuildResultCollector),
        providers_printer
            .as_mut()
            .map(|v| v as &mut dyn BuildResultCollector),
    ]
    .into_iter()
    .flatten()
    .collect::<Vec<&mut dyn BuildResultCollector>>();

    let final_artifact_materializations =
        Materializations::from_i32(request.final_artifact_materializations)
            .with_context(|| "Invalid final_artifact_materializations")
            .unwrap();
    let materialization_context =
        ConvertMaterializationContext::from(final_artifact_materializations);

    let mut provider_artifacts = Vec::new();
    for (k, v) in build_targets(
        &ctx,
        resolved_pattern,
        target_resolution_config,
        build_providers,
        &materialization_context,
        build_opts.fail_fast,
        MissingTargetBehavior::from_skip(build_opts.skip_missing_targets),
        build_opts.skip_incompatible_targets,
    )
    .await?
    {
        result_collectors.collect_result(&BuildOwner::Target(&k), &v);
        let mut outputs = v.outputs.into_iter().filter_map(|output| match output {
            Ok(output) => Some(output),
            _ => None,
        });
        provider_artifacts.extend(&mut outputs);
    }

    if should_create_unhashed_links.unwrap_or(false) {
        span_async(buck2_data::CreateOutputSymlinksStart {}, async {
            let lock = ctx
                .per_transaction_data()
                .get_create_unhashed_symlink_lock();
            let _guard = lock.lock().await;
            let res = create_unhashed_outputs(provider_artifacts, &artifact_fs, fs);

            let created = match res.as_ref() {
                Ok(n) => *n,
                Err(..) => 0,
            };
            (res, buck2_data::CreateOutputSymlinksEnd { created })
        })
        .await?;
    }

    let mut serialized_build_report = None;
    if let Some(build_report_collector) = build_report_collector {
        let report = build_report_collector.into_report();
        if !build_opts.unstable_build_report_filename.is_empty() {
            let file = fs_util::create_file(
                fs.resolve(cwd)
                    .as_abs_path()
                    .join(&build_opts.unstable_build_report_filename),
            )
            .context("Error writing build report")?;
            let mut file = BufWriter::new(file);
            serde_json::to_writer_pretty(&mut file, &report)?
        } else {
            serialized_build_report = Some(serde_json::to_string(&report)?);
        };
    }

    // TODO(nmj): The BuildResult / BuildResponse will eventually return all of the
    //            data back to the CLI client, and all build report generation will happen there.
    //            For now, we're going to be a little hacky to remove some stdout printing that
    //            used to exist here.
    let (build_targets, error_messages) = match result_collector.results() {
        Ok(targets) => (targets, Vec::new()),
        Err(errors) => {
            let error_strings = errors
                .errors
                .iter()
                .map(|e| format!("{:#}", e))
                .unique()
                .collect();
            (vec![], error_strings)
        }
    };

    let project_root = server_ctx.project_root().to_string();

    Ok(buck2_cli_proto::BuildResponse {
        build_targets,
        project_root,
        serialized_build_report: serialized_build_report.unwrap_or_default(),
        error_messages,
    })
}

async fn build_targets(
    ctx: &DiceComputations,
    spec: ResolvedPattern<ConfiguredProvidersPatternExtra>,
    target_resolution_config: TargetResolutionConfig,
    build_providers: Arc<BuildProviders>,
    materialization_context: &MaterializationContext,
    fail_fast: bool,
    missing_target_behavior: MissingTargetBehavior,
    skip_incompatible_targets: bool,
) -> anyhow::Result<BTreeMap<ConfiguredProvidersLabel, BuildTargetResult>> {
    let stream = match target_resolution_config {
        TargetResolutionConfig::Default(global_target_platform) => {
            let spec = spec.convert_pattern().context(
                "Cannot build with explicit configurations when universe is not specified",
            )?;
            build_targets_with_global_target_platform(
                ctx,
                spec,
                global_target_platform,
                build_providers,
                materialization_context,
                missing_target_behavior,
                skip_incompatible_targets,
            )
            .left_stream()
        }
        TargetResolutionConfig::Universe(universe) => build_targets_in_universe(
            ctx,
            spec,
            universe,
            build_providers,
            materialization_context,
        )
        .right_stream(),
    };

    // We omit skipped targets here.
    let res = BuildTargetResult::collect_stream(stream, fail_fast)
        .await?
        .into_iter()
        .filter_map(|(k, v)| Some((k, v?)))
        .collect();

    Ok(res)
}

fn build_targets_in_universe<'a>(
    ctx: &'a DiceComputations,
    spec: ResolvedPattern<ConfiguredProvidersPatternExtra>,
    universe: CqueryUniverse,
    build_providers: Arc<BuildProviders>,
    materialization_context: &'a MaterializationContext,
) -> impl Stream<Item = anyhow::Result<BuildEvent>> + Unpin + 'a {
    let providers_to_build = build_providers_to_providers_to_build(&build_providers);
    let provider_labels = universe.get_provider_labels(&spec);
    provider_labels
        .into_iter()
        .map(|p| {
            let materialization_context = materialization_context.dupe();
            let providers_to_build = providers_to_build.clone();
            async move {
                let res = build::build_configured_label(
                    ctx,
                    &materialization_context,
                    p,
                    &providers_to_build,
                    false,
                )
                .await;

                match res {
                    Ok(stream) => stream.map(Ok).left_stream(),
                    Err(e) => futures::stream::once(futures::future::ready(Err(e))).right_stream(),
                }
            }
            .boxed()
        })
        .collect::<FuturesUnordered<_>>()
        .flatten_unordered(None)
}

fn build_targets_with_global_target_platform<'a>(
    ctx: &'a DiceComputations,
    spec: ResolvedPattern<ProvidersPatternExtra>,
    global_target_platform: Option<TargetLabel>,
    build_providers: Arc<BuildProviders>,
    materialization_context: &'a MaterializationContext,
    missing_target_behavior: MissingTargetBehavior,
    skip_incompatible_targets: bool,
) -> impl Stream<Item = anyhow::Result<BuildEvent>> + Unpin + 'a {
    spec.specs
        .into_iter()
        .map(|(package, spec)| {
            let build_providers = build_providers.dupe();
            let global_target_platform = global_target_platform.dupe();
            async move {
                let res = ctx.get_interpreter_results(package.dupe()).await?;
                anyhow::Ok(build_targets_for_spec(
                    ctx,
                    spec,
                    global_target_platform,
                    res,
                    build_providers,
                    materialization_context,
                    missing_target_behavior,
                    skip_incompatible_targets,
                ))
            }
        })
        .collect::<FuturesUnordered<_>>()
        .map(|res| match res {
            Ok(stream) => stream.left_stream(),
            Err(e) => futures::stream::once(futures::future::ready(Err(e))).right_stream(),
        })
        .flatten_unordered(None)
}

struct TargetBuildSpec {
    target: TargetNode,
    providers: ProvidersName,
    global_target_platform: Option<TargetLabel>,
    // Indicates whether this target was explicitly requested or not. If it's the result
    // of something like `//foo/...` we can skip it (for example if it's incompatible with
    // the target platform).
    skippable: bool,
}

fn build_providers_to_providers_to_build(build_providers: &BuildProviders) -> ProvidersToBuild {
    let mut providers_to_build = ProvidersToBuild::default();

    if build_providers.default_info != BuildProviderAction::Skip as i32 {
        providers_to_build.default = true;
        providers_to_build.default_other = true;
    }

    if build_providers.test_info != BuildProviderAction::Skip as i32 {
        providers_to_build.tests = true;
    }

    if build_providers.run_info != BuildProviderAction::Skip as i32 {
        providers_to_build.run = true;
    }

    providers_to_build
}

fn build_targets_for_spec<'a>(
    ctx: &'a DiceComputations,
    spec: PackageSpec<ProvidersPatternExtra>,
    global_target_platform: Option<TargetLabel>,
    res: Arc<EvaluationResult>,
    build_providers: Arc<BuildProviders>,
    materialization_context: &'a MaterializationContext,
    missing_target_behavior: MissingTargetBehavior,
    skip_incompatible_targets: bool,
) -> impl Stream<Item = anyhow::Result<BuildEvent>> + Unpin + 'a {
    async move {
        let skippable = match spec {
            PackageSpec::Targets(..) => skip_incompatible_targets,
            PackageSpec::All => true,
        };

        let (targets, missing) = res.apply_spec(spec);
        if let Some(missing) = missing {
            match missing_target_behavior {
                MissingTargetBehavior::Fail => {
                    return Err(missing.into_error());
                }
                MissingTargetBehavior::Warn => {
                    // TODO: This should be reported in the build report eventually.
                    console_message(missing.missing_targets_warning());
                }
            }
        }

        let todo_targets: Vec<TargetBuildSpec> = targets
            .into_iter()
            .map(|((_target_name, extra), target)| TargetBuildSpec {
                target,
                providers: extra.providers,
                global_target_platform: global_target_platform.dupe(),
                skippable,
            })
            .collect();

        let providers_to_build = build_providers_to_providers_to_build(&build_providers);

        let stream = todo_targets
            .into_iter()
            .map(|build_spec| {
                let materialization_context = materialization_context.dupe();
                let providers_to_build = providers_to_build.clone();
                async move {
                    build_target(
                        ctx,
                        build_spec,
                        &providers_to_build,
                        &materialization_context,
                    )
                    .await
                }
                .boxed()
            })
            .collect::<FuturesUnordered<_>>()
            .flatten_unordered(None);

        anyhow::Ok(stream)
    }
    .boxed()
    .try_flatten_stream()
}

async fn build_target<'a>(
    ctx: &'a DiceComputations,
    spec: TargetBuildSpec,
    providers_to_build: &ProvidersToBuild,
    materialization_context: &MaterializationContext,
) -> impl Stream<Item = anyhow::Result<BuildEvent>> + 'a {
    let res = async {
        let providers_label = ctx
            .get_configured_provider_label(
                &ProvidersLabel::new(spec.target.label().dupe(), spec.providers),
                spec.global_target_platform.as_ref(),
            )
            .await?;

        build::build_configured_label(
            ctx,
            materialization_context,
            providers_label,
            providers_to_build,
            spec.skippable,
        )
        .await
    }
    .await;

    match res {
        Ok(stream) => stream.map(Ok).left_stream(),
        Err(e) => futures::stream::once(futures::future::ready(Err(e))).right_stream(),
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

//! Processing and reporting the the results of the build

use buck2_build_api::build::BuildTargetResult;
use buck2_core::provider::label::ConfiguredProvidersLabel;

pub(crate) enum BuildOwner<'a> {
    Target(&'a ConfiguredProvidersLabel),
}

/// Collects the results of the build and processes it
pub(crate) trait BuildResultCollector: Send {
    fn collect_result(&mut self, label: &BuildOwner, result: &BuildTargetResult);
}

impl BuildResultCollector for Vec<&mut dyn BuildResultCollector> {
    fn collect_result(&mut self, label: &BuildOwner, result: &BuildTargetResult) {
        for collector in self {
            collector.collect_result(label, result);
        }
    }
}

pub mod result_report {
    use buck2_build_api::build::BuildProviderType;
    use buck2_build_api::build::BuildTargetResult;
    use buck2_build_api::build::ProviderArtifacts;
    use buck2_cli_proto::build_target::build_output::BuildOutputProviders;
    use buck2_cli_proto::build_target::BuildOutput;
    use buck2_cli_proto::BuildTarget;
    use buck2_common::result::SharedError;
    use buck2_core::fs::artifact_path_resolver::ArtifactFs;
    use buck2_execute::artifact::artifact_dyn::ArtifactDyn;
    use dupe::Dupe;
    use starlark_map::small_map::SmallMap;

    use crate::commands::build::results::BuildOwner;
    use crate::commands::build::results::BuildResultCollector;

    /// Simple container for multiple [`SharedError`]s
    pub(crate) struct SharedErrors {
        pub errors: Vec<SharedError>,
    }

    #[derive(Copy, Clone, Dupe)]
    pub(crate) struct ResultReporterOptions {
        pub(crate) return_outputs: bool,
        pub(crate) return_default_other_outputs: bool,
    }

    pub(crate) struct ResultReporter<'a> {
        artifact_fs: &'a ArtifactFs,
        options: ResultReporterOptions,
        results: Result<Vec<BuildTarget>, SharedErrors>,
    }

    impl<'a> ResultReporter<'a> {
        pub(crate) fn new(artifact_fs: &'a ArtifactFs, options: ResultReporterOptions) -> Self {
            Self {
                artifact_fs,
                options,
                results: Ok(Vec::new()),
            }
        }

        pub(crate) fn results(self) -> Result<Vec<BuildTarget>, SharedErrors> {
            self.results
        }
    }

    impl<'a> BuildResultCollector for ResultReporter<'a> {
        fn collect_result(&mut self, label: &BuildOwner, result: &BuildTargetResult) {
            let outputs = result
                .outputs
                .iter()
                .filter_map(|output| match output {
                    Ok(output) => Some(output),
                    Err(e) => {
                        match self.results.as_mut() {
                            Ok(..) => {
                                self.results = Err(SharedErrors {
                                    errors: vec![e.dupe()],
                                });
                            }
                            Err(errs) => errs.errors.push(e.dupe()),
                        };
                        None
                    }
                })
                .collect::<Vec<_>>();

            if let Ok(r) = &mut self.results {
                let artifacts = if self.options.return_outputs {
                    // NOTE: We use an SmallMap here to preserve the order the rule author wrote, all
                    // the while avoiding duplicates.
                    let mut artifacts = SmallMap::new();

                    for output in outputs {
                        let ProviderArtifacts {
                            values,
                            provider_type,
                        } = output;

                        if !self.options.return_default_other_outputs
                            && matches!(provider_type, BuildProviderType::DefaultOther)
                        {
                            continue;
                        }

                        for (artifact, _value) in values.iter() {
                            let entry =
                                artifacts
                                    .entry(artifact)
                                    .or_insert_with(|| BuildOutputProviders {
                                        default_info: false,
                                        run_info: false,
                                        other: false,
                                        test_info: false,
                                    });

                            match provider_type {
                                BuildProviderType::Default => {
                                    entry.default_info = true;
                                }
                                BuildProviderType::DefaultOther => {
                                    entry.other = true;
                                }
                                BuildProviderType::Run => {
                                    entry.run_info = true;
                                }
                                BuildProviderType::Test => {
                                    entry.test_info = true;
                                }
                            }
                        }
                    }

                    let artifact_fs = &self.artifact_fs;

                    artifacts
                        .into_iter()
                        .map(|(a, providers)| BuildOutput {
                            path: a.resolve_path(artifact_fs).unwrap().to_string(),
                            providers: Some(providers),
                        })
                        .collect()
                } else {
                    Vec::new()
                };

                let (target, configuration) = match label {
                    BuildOwner::Target(t) => (t.unconfigured().to_string(), t.cfg().to_string()),
                };

                r.push(BuildTarget {
                    target,
                    configuration,
                    run_args: result.run_args.clone().unwrap_or_default(),
                    outputs: artifacts,
                })
            };
        }
    }
}

pub mod build_report {
    use std::collections::HashMap;

    use buck2_build_api::build::BuildProviderType;
    use buck2_core::configuration::data::ConfigurationData;
    use buck2_core::fs::artifact_path_resolver::ArtifactFs;
    use buck2_core::fs::paths::abs_norm_path::AbsNormPathBuf;
    use buck2_core::fs::project::ProjectRoot;
    use buck2_core::fs::project_rel_path::ProjectRelativePathBuf;
    use buck2_core::provider::label::NonDefaultProvidersName;
    use buck2_core::provider::label::ProvidersName;
    use buck2_core::target::label::TargetLabel;
    use buck2_execute::artifact::artifact_dyn::ArtifactDyn;
    use buck2_wrapper_common::invocation_id::TraceId;
    use derivative::Derivative;
    use dupe::Dupe;
    use itertools::Itertools;
    use serde::Serialize;
    use starlark_map::small_set::SmallSet;

    use crate::commands::build::results::BuildOwner;
    use crate::commands::build::results::BuildResultCollector;
    use crate::commands::build::BuildTargetResult;

    #[derive(Debug, Serialize)]
    #[allow(clippy::upper_case_acronyms)] // We care about how they serialise
    enum BuildOutcome {
        SUCCESS,
        FAIL,
        #[allow(dead_code)] // Part of the spec, but not yet used
        CANCELED,
    }

    impl Default for BuildOutcome {
        fn default() -> Self {
            Self::SUCCESS
        }
    }

    #[derive(Debug, Serialize)]
    pub(crate) struct BuildReport {
        trace_id: TraceId,
        success: bool,
        results: HashMap<EntryLabel, ConfiguredBuildReportEntry>,
        failures: HashMap<EntryLabel, ProjectRelativePathBuf>,
        project_root: AbsNormPathBuf,
        truncated: bool,
    }

    #[derive(Default, Debug, Serialize)]
    pub(crate) struct BuildReportEntry {
        /// whether this particular target was successful
        success: BuildOutcome,
        /// a map of each subtarget of the current target (outputted as a `|` delimited list) to
        /// the default exposed output of the subtarget
        outputs: HashMap<String, Vec<ProjectRelativePathBuf>>,
        /// a map of each subtarget of the current target (outputted as a `|` delimited list) to
        /// the hidden, implicitly built outputs of the subtarget. There are multiple outputs
        /// per subtarget
        other_outputs: HashMap<String, Vec<ProjectRelativePathBuf>>,
    }

    #[derive(Debug, Serialize)]
    pub(crate) struct ConfiguredBuildReportEntry {
        #[serde(flatten)]
        #[serde(skip_serializing_if = "Option::is_none")]
        compatible: Option<BuildReportEntry>,

        /// the configured entry
        configured: HashMap<ConfigurationData, BuildReportEntry>,
    }

    #[derive(Derivative, Serialize, Eq, PartialEq, Hash)]
    #[derivative(Debug)]
    #[serde(untagged)]
    enum EntryLabel {
        #[derivative(Debug = "transparent")]
        Target(TargetLabel),
    }

    pub(crate) struct BuildReportCollector<'a> {
        trace_id: &'a TraceId,
        artifact_fs: &'a ArtifactFs,
        build_report_results: HashMap<EntryLabel, ConfiguredBuildReportEntry>,
        overall_success: bool,
        project_root: &'a ProjectRoot,
        include_unconfigured_section: bool,
        include_other_outputs: bool,
    }

    impl<'a> BuildReportCollector<'a> {
        pub(crate) fn new(
            trace_id: &'a TraceId,
            artifact_fs: &'a ArtifactFs,
            project_root: &'a ProjectRoot,
            include_unconfigured_section: bool,
            include_other_outputs: bool,
        ) -> Self {
            Self {
                trace_id,
                artifact_fs,
                build_report_results: HashMap::new(),
                overall_success: true,
                project_root,
                include_unconfigured_section,
                include_other_outputs,
            }
        }

        pub(crate) fn into_report(self) -> BuildReport {
            BuildReport {
                trace_id: self.trace_id.dupe(),
                success: self.overall_success,
                results: self.build_report_results,
                failures: HashMap::new(),
                project_root: self.project_root.root().to_owned(),
                // In buck1 we may truncate build report for a large number of targets.
                // Setting this to false since we don't currently truncate buck2's build report.
                truncated: false,
            }
        }
    }

    impl<'a> BuildResultCollector for BuildReportCollector<'a> {
        fn collect_result(&mut self, label: &BuildOwner, result: &BuildTargetResult) {
            let (default_outs, other_outs, success) = {
                let mut default_outs = SmallSet::new();
                let mut other_outs = SmallSet::new();
                let mut success = true;

                result.outputs.iter().for_each(|res| {
                    match res {
                        Ok(artifacts) => {
                            let mut is_default = false;
                            let mut is_other = false;

                            match artifacts.provider_type {
                                BuildProviderType::Default => {
                                    // as long as we have requested it as a default info, it should  be
                                    // considered a default output whether or not it also appears as an other
                                    // non-main output
                                    is_default = true;
                                }
                                BuildProviderType::DefaultOther
                                | BuildProviderType::Run
                                | BuildProviderType::Test => {
                                    // as long as the output isn't the default, we add it to other outputs.
                                    // This means that the same artifact may appear twice if its part of the
                                    // default AND the other outputs, but this is intended as it accurately
                                    // describes the type of the artifact
                                    is_other = true;
                                }
                            }

                            for (artifact, _value) in artifacts.values.iter() {
                                if is_default {
                                    default_outs
                                        .insert(artifact.resolve_path(self.artifact_fs).unwrap());
                                }

                                if is_other && self.include_other_outputs {
                                    other_outs
                                        .insert(artifact.resolve_path(self.artifact_fs).unwrap());
                                }
                            }
                        }
                        Err(..) => success = false,
                    }
                });

                (default_outs, other_outs, success)
            };

            let report_results = self
                .build_report_results
                .entry(match label {
                    BuildOwner::Target(t) => EntryLabel::Target(t.unconfigured().target().dupe()),
                })
                .or_insert_with(|| ConfiguredBuildReportEntry {
                    compatible: if self.include_unconfigured_section {
                        Some(BuildReportEntry::default())
                    } else {
                        None
                    },
                    configured: HashMap::new(),
                });

            let unconfigured_report = &mut report_results.compatible;
            let configured_report = report_results
                .configured
                .entry(match label {
                    BuildOwner::Target(t) => t.cfg().dupe(),
                })
                .or_insert_with(BuildReportEntry::default);
            if !default_outs.is_empty() {
                if let Some(report) = unconfigured_report {
                    report.outputs.insert(
                        report_providers_name(label),
                        default_outs.iter().cloned().collect(),
                    );
                }

                configured_report.outputs.insert(
                    report_providers_name(label),
                    default_outs.into_iter().collect(),
                );
            }
            if !other_outs.is_empty() {
                if let Some(report) = unconfigured_report {
                    report.other_outputs.insert(
                        report_providers_name(label),
                        other_outs.iter().cloned().collect(),
                    );
                }

                configured_report.other_outputs.insert(
                    report_providers_name(label),
                    other_outs.into_iter().collect(),
                );
            }

            if !success {
                if let Some(report) = unconfigured_report {
                    report.success = BuildOutcome::FAIL;
                }
                configured_report.success = BuildOutcome::FAIL;
                self.overall_success = false;
            }
        }
    }

    fn report_providers_name(label: &BuildOwner) -> String {
        match label {
            BuildOwner::Target(t) => match t.name() {
                ProvidersName::Default => "DEFAULT".to_owned(),
                ProvidersName::NonDefault(box NonDefaultProvidersName::Named(names)) => {
                    names.iter().join("|")
                }
                ProvidersName::NonDefault(box NonDefaultProvidersName::UnrecognizedFlavor(f)) => {
                    format!("#{}", f)
                }
            },
        }
    }
}

pub mod providers {
    use crate::commands::build::results::BuildOwner;
    use crate::commands::build::results::BuildResultCollector;
    use crate::commands::build::BuildTargetResult;

    pub(crate) struct ProvidersPrinter;

    impl BuildResultCollector for ProvidersPrinter {
        fn collect_result(&mut self, _label: &BuildOwner, result: &BuildTargetResult) {
            // TODO: should we print the label here?
            let providers = result.providers.provider_collection();
            for x in providers.provider_ids() {
                let p = providers.get_provider_raw(x).unwrap();
                eprintln!("    {} = {}", x.name(), p);
            }
        }
    }
}