/*
 * 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::sync::Arc;

use buck2_analysis::analysis::calculation::get_dep_analysis;
use buck2_analysis::analysis::calculation::resolve_queries;
use buck2_analysis::analysis::env::get_dep;
use buck2_analysis::analysis::env::get_deps_from_analysis_results;
use buck2_analysis::analysis::env::resolve_query;
use buck2_analysis::analysis::env::resolve_unkeyed_placeholder;
use buck2_analysis::attrs::resolve::ctx::AnalysisQueryResult;
use buck2_analysis::attrs::resolve::ctx::AttrResolutionContext;
use buck2_build_api::interpreter::rule_defs::cmd_args::value::FrozenCommandLineArg;
use buck2_build_api::interpreter::rule_defs::provider::collection::FrozenProviderCollectionValue;
use buck2_common::result::SharedResult;
use buck2_common::result::ToSharedResultExt;
use buck2_core::provider::label::ConfiguredProvidersLabel;
use buck2_core::target::label::ConfiguredTargetLabel;
use buck2_node::configuration::execution::ExecutionPlatformResolution;
use buck2_node::nodes::configured::ConfiguredTargetNode;
use once_cell::sync::OnceCell;
use starlark::environment::Module;

use crate::bxl::starlark_defs::context::BxlContext;

// Contains a `module` that things must live on, and various `FrozenProviderCollectionValue`s
// that are NOT tied to that module. Must claim ownership of them via `add_reference` before returning them.
pub struct LazyAttrResolutionContext<'v> {
    pub module: &'v Module,
    pub configured_node: &'v ConfiguredTargetNode,
    pub ctx: &'v BxlContext<'v>,
    pub dep_analysis_results:
        OnceCell<anyhow::Result<HashMap<&'v ConfiguredTargetLabel, FrozenProviderCollectionValue>>>,
    pub query_results: OnceCell<anyhow::Result<HashMap<String, Arc<AnalysisQueryResult>>>>,
}

impl<'v> LazyAttrResolutionContext<'v> {
    pub(crate) fn dep_analysis_results(
        &self,
    ) -> &anyhow::Result<HashMap<&'v ConfiguredTargetLabel, FrozenProviderCollectionValue>> {
        self.dep_analysis_results.get_or_init(|| {
            get_deps_from_analysis_results(
                self.ctx
                    .async_ctx
                    .via_dice(|dice_ctx| get_dep_analysis(self.configured_node, dice_ctx))?,
            )
        })
    }

    pub(crate) fn query_results(
        &self,
    ) -> &anyhow::Result<HashMap<String, Arc<AnalysisQueryResult>>> {
        self.query_results.get_or_init(|| {
            self.ctx
                .async_ctx
                .via_dice(|dice_ctx| resolve_queries(dice_ctx, self.configured_node))
        })
    }
}

impl<'v> AttrResolutionContext<'v> for LazyAttrResolutionContext<'v> {
    fn starlark_module(&self) -> &'v Module {
        self.module
    }

    fn get_dep(
        &self,
        target: &ConfiguredProvidersLabel,
    ) -> anyhow::Result<FrozenProviderCollectionValue> {
        match self.dep_analysis_results() {
            Ok(deps) => get_dep(deps, target, self.module),
            Err(e) => Err(anyhow::anyhow!("Error getting deps from analysis: `{}`", e)),
        }
    }

    fn resolve_unkeyed_placeholder(
        &self,
        name: &str,
    ) -> anyhow::Result<Option<FrozenCommandLineArg>> {
        match self.dep_analysis_results() {
            Ok(deps) => Ok(resolve_unkeyed_placeholder(deps, name, self.module)),
            Err(e) => Err(anyhow::anyhow!(
                "Error resolving unkeyed placeholder: `{}`",
                e
            )),
        }
    }

    fn resolve_query(&self, query: &str) -> SharedResult<Arc<AnalysisQueryResult>> {
        match self.query_results() {
            Ok(res) => resolve_query(res, query, self.module),
            Err(e) => Err(anyhow::anyhow!("Error resolving query: `{}`", e)).shared_error(),
        }
    }

    fn execution_platform_resolution(&self) -> &ExecutionPlatformResolution {
        self.configured_node.execution_platform_resolution()
    }
}


/*
 * 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;
use std::fmt::Display;
use std::fmt::Formatter;

use allocative::Allocative;
use anyhow::Context;
use buck2_core::package::PackageLabel;
use buck2_node::attrs::coerced_attr::CoercedAttr;
use buck2_node::attrs::display::AttrDisplayWithContext;
use buck2_node::attrs::fmt_context::AttrFmtContext;
use buck2_node::attrs::inspect_options::AttrInspectOptions;
use buck2_node::attrs::serialize::AttrSerializeWithContext;
use derive_more::From;
use dupe::Dupe;
use serde::Serialize;
use starlark::__derive_refs::serde::Serializer;
use starlark::any::ProvidesStaticType;
use starlark::coerce::Coerce;
use starlark::starlark_complex_value;
use starlark::starlark_simple_value;
use starlark::values::starlark_value;
use starlark::values::Freeze;
use starlark::values::Heap;
use starlark::values::NoSerialize;
use starlark::values::StarlarkValue;
use starlark::values::Trace;
use starlark::values::Value;
use starlark::values::ValueLike;
use starlark::StarlarkDocs;

use crate::bxl::starlark_defs::nodes::unconfigured::StarlarkTargetNode;

#[derive(
    Debug,
    Clone,
    Coerce,
    Trace,
    Freeze,
    ProvidesStaticType,
    NoSerialize,
    Allocative
)]
#[repr(C)]
pub struct StarlarkTargetNodeCoercedAttributesGen<V> {
    pub(super) inner: V,
}

impl<V: Display> Display for StarlarkTargetNodeCoercedAttributesGen<V> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "Traversal({})", self.inner)
    }
}

starlark_complex_value!(pub StarlarkTargetNodeCoercedAttributes);

#[starlark_value(type = "starlark_attributes")]
impl<'v, V: ValueLike<'v> + 'v> StarlarkValue<'v> for StarlarkTargetNodeCoercedAttributesGen<V>
where
    Self: ProvidesStaticType<'v>,
{
    fn iterate_collect(&self, heap: &'v Heap) -> anyhow::Result<Vec<Value<'v>>> {
        let starlark_target_node = self
            .inner
            .downcast_ref::<StarlarkTargetNode>()
            .context("invalid inner")?;
        let target_node = &starlark_target_node.0;
        Ok(target_node
            .attrs(AttrInspectOptions::All)
            .map(|a| {
                heap.alloc((
                    a.name,
                    StarlarkCoercedAttr(a.value.clone(), target_node.label().pkg()),
                ))
            })
            .collect())
    }
}

#[derive(Debug, ProvidesStaticType, From, Allocative, StarlarkDocs)]
#[starlark_docs(directory = "bxl")]
pub struct StarlarkCoercedAttr(pub CoercedAttr, pub PackageLabel);

starlark_simple_value!(StarlarkCoercedAttr);

impl Display for StarlarkCoercedAttr {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        self.0.fmt(
            &AttrFmtContext {
                package: Some(self.1.dupe()),
            },
            f,
        )
    }
}

impl Serialize for StarlarkCoercedAttr {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        self.0.serialize_with_ctx(
            &AttrFmtContext {
                package: Some(self.1.dupe()),
            },
            serializer,
        )
    }
}

/// Coerced attr from an unconfigured target node.
#[starlark_value(type = "coerced_attr")]
impl<'v> StarlarkValue<'v> for StarlarkCoercedAttr {}


/*
 * 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::fs::File;
use std::io;
use std::io::BufRead;
use std::path::Path;
use std::process::Command;
use std::str;

use anyhow::Context as _;
use buck2_client_ctx::immediate_config::ImmediateConfigContext;
use buck2_core::fs::fs_util;
use buck2_core::fs::paths::abs_norm_path::AbsNormPathBuf;
use buck2_util::process::background_command;
use termwiz::istty::IsTty;
use thiserror::Error;

#[derive(Error, Debug)]
enum ArgExpansionError {
    #[error("Missing flag file path after --flagfile argument")]
    MissingFlagFilePath,
    #[error("Unable to read flag file at `{path}`")]
    MissingFlagFileOnDisk { source: anyhow::Error, path: String },
    #[error("Unable to read line in flag file `{path}`")]
    FlagFileReadError { source: anyhow::Error, path: String },
    #[error("Python mode file `{path}` output is not UTF-8")]
    PythonOutputNotUtf8 { path: String },
    #[error("No flag file path after @ symbol in argfile argument")]
    MissingFlagFilePathInArgfile,
    #[error("Python argfile at `{path}` exited with non-zero status, stderr: {err:?}")]
    PythonExecutableFailed { path: String, err: String },
    #[error("Python argfile command ({cmd:?}) execution failed")]
    PythonExecutionFailed { source: io::Error, cmd: Command },
    #[error("Unable to read line from stdin")]
    StdinReadError { source: anyhow::Error },
}

/// Log that a relative flag file was not found in CWD, but was found, and used, from the cell root
///
/// This prints directly to stderr (sometimes in color). This should be safe, because flagfile
/// expansion runs *very* early in the CLI process lifetime.
pub fn log_relative_path_from_cell_root(requested_path: &str) -> anyhow::Result<()> {
    let (prefix, reset) = if io::stderr().is_tty() {
        ("\x1b[33m", "\x1b[0m")
    } else {
        ("WARNING: ", "")
    };
    buck2_client_ctx::eprintln!(
        "{}`@{}` was specified, but not found. Using file at `//{}`.",
        prefix,
        requested_path,
        requested_path
    )?;
    buck2_client_ctx::eprintln!(
        "This behavior is being deprecated. Please use `@//{}` instead{}",
        requested_path,
        reset
    )?;
    Ok(())
}

#[derive(Clone)]
enum ArgFile {
    PythonExecutable(AbsNormPathBuf, Option<String>),
    Path(AbsNormPathBuf),
    Stdin,
}

// Expands any argfiles passed as command line parameters. There are
// two ways to do: `@argfile` or `--flagfile PATH`.
//
// Caveats:
//  - `--` and `--flagfile` cannot be values of other options
//  - `--flagfile=X` is _not_ supported, you need to pass
//    `--flagfile X` instead.
//  - `--flagfil` is _not_ supported.
//
// TODO: This function should also return tracking information, so
//       that we know where args come from. This would be useful
//       in cases where the argfiles contain `--config` flags.
//
// TODO: It does _not_ support executable argfiles (e.g., Python)
//       which are supported by Buck v1. See `BuckArgsMethods` in
//       Buck v1 for reference.
pub fn expand_argfiles_with_context(
    args: Vec<String>,
    context: &mut ImmediateConfigContext,
) -> anyhow::Result<Vec<String>> {
    let mut expanded_args = Vec::new();
    let mut arg_iterator = args.into_iter();

    while let Some(next_arg) = arg_iterator.next() {
        match next_arg.as_str() {
            "--" => {
                expanded_args.push(next_arg);
                expanded_args.extend(arg_iterator);
                break;
            }
            "--flagfile" => {
                let flagfile = match arg_iterator.next() {
                    Some(val) => val,
                    None => return Err(anyhow::anyhow!(ArgExpansionError::MissingFlagFilePath)),
                };
                // TODO: We want to detect cyclic inclusion
                let expanded_flagfile_args = resolve_and_expand_argfile(&flagfile, context)?;
                expanded_args.extend(expanded_flagfile_args);
            }
            next_arg if next_arg.starts_with('@') => {
                let flagfile = next_arg.strip_prefix('@').unwrap();
                if flagfile.is_empty() {
                    return Err(anyhow::anyhow!(
                        ArgExpansionError::MissingFlagFilePathInArgfile
                    ));
                }
                // TODO: We want to detect cyclic inclusion
                let expanded_flagfile_args = resolve_and_expand_argfile(flagfile, context)?;
                expanded_args.extend(expanded_flagfile_args);
            }
            _ => expanded_args.push(next_arg),
        }
    }

    Ok(expanded_args)
}

// Resolves a path argument to an absolute path, reads the flag file and expands
// it into a list of arguments.
fn resolve_and_expand_argfile(
    path: &str,
    context: &mut ImmediateConfigContext,
) -> anyhow::Result<Vec<String>> {
    let flagfile = resolve_flagfile(path, context)
        .with_context(|| format!("Error resolving flagfile `{}`", path))?;
    let flagfile_lines = expand_argfile_contents(&flagfile)?;
    expand_argfiles_with_context(flagfile_lines, context)
}

fn expand_argfile_contents(flagfile: &ArgFile) -> anyhow::Result<Vec<String>> {
    match flagfile {
        ArgFile::Path(path) => {
            let mut lines = Vec::new();
            let file =
                File::open(path).map_err(|source| ArgExpansionError::MissingFlagFileOnDisk {
                    source: source.into(),
                    path: path.to_string_lossy().into_owned(),
                })?;
            let reader = io::BufReader::new(file);
            for line_result in reader.lines() {
                let line = line_result.map_err(|source| ArgExpansionError::FlagFileReadError {
                    source: source.into(),
                    path: path.to_string_lossy().into_owned(),
                })?;
                if line.is_empty() {
                    continue;
                }
                lines.push(line);
            }
            Ok(lines)
        }
        ArgFile::PythonExecutable(path, flag) => {
            let mut cmd = background_command("python3");
            cmd.env("BUCK2_ARG_FILE", "1");
            cmd.arg(path.as_os_str());
            if let Some(flag) = flag.as_deref() {
                cmd.args(["--flavors", flag]);
            }
            let cmd_out = cmd
                .output()
                .map_err(|source| ArgExpansionError::PythonExecutionFailed { cmd, source })?;
            if cmd_out.status.success() {
                Ok(str::from_utf8(&cmd_out.stdout)
                    .map_err(|_| ArgExpansionError::PythonOutputNotUtf8 {
                        path: path.to_string_lossy().into_owned(),
                    })?
                    .lines()
                    .filter(|line| !line.is_empty())
                    .map(|s| s.to_owned())
                    .collect::<Vec<String>>())
            } else {
                Err(anyhow::anyhow!(ArgExpansionError::PythonExecutableFailed {
                    path: path.to_string_lossy().into_owned(),
                    err: String::from_utf8_lossy(&cmd_out.stderr).to_string(),
                }))
            }
        }
        ArgFile::Stdin => io::stdin()
            .lock()
            .lines()
            .filter_map(|line| match line {
                Ok(x) if x.is_empty() => None,
                Ok(x) => Some(Ok(x)),
                Err(err) => Some(Err(ArgExpansionError::StdinReadError {
                    source: err.into(),
                }
                .into())),
            })
            .collect(),
    }
}

// Resolves a path argument to an absolute path, so that it can be read.
fn resolve_flagfile(path: &str, context: &mut ImmediateConfigContext) -> anyhow::Result<ArgFile> {
    if path == "-" {
        return Ok(ArgFile::Stdin);
    }

    let (path_part, flag) = match path.split_once('#') {
        Some((pypath, pyflag)) => (pypath, Some(pyflag)),
        None => (path, None),
    };

    let resolved_path = if let Some(cell_resolved_path) = context.resolve_cell_path_arg(path_part) {
        cell_resolved_path.context("Error resolving cell path")?
    } else {
        let p = Path::new(path_part);
        if !p.is_absolute() {
            match fs_util::canonicalize(p) {
                Ok(abs_path) => Ok(abs_path),
                Err(original_error) => {
                    let cell_relative_path = context.resolve_cell_path("", path_part)?;
                    // If the relative path does not exist relative to the cwd,
                    // attempt to make it relative to the cell root. If *that*
                    // doesn't exist, just report the original error back, and
                    // don't tip users off that they can use relative-to-cell paths.
                    // We want to deprecate that.
                    match fs_util::canonicalize(cell_relative_path) {
                        Ok(abs_path) => {
                            log_relative_path_from_cell_root(path_part)?;
                            Ok(abs_path)
                        }
                        Err(_) => Err(ArgExpansionError::MissingFlagFileOnDisk {
                            source: original_error,
                            path: p.to_string_lossy().into_owned(),
                        }),
                    }
                }
            }?
        } else {
            AbsNormPathBuf::try_from(p.to_owned())?
        }
    };

    context.push_trace(&resolved_path);
    if path_part.ends_with(".py") {
        Ok(ArgFile::PythonExecutable(
            resolved_path,
            flag.map(ToOwned::to_owned),
        ))
    } else {
        Ok(ArgFile::Path(resolved_path))
    }
}

#[cfg(test)]
mod tests {
    use buck2_core::fs::fs_util;
    use buck2_core::fs::paths::abs_norm_path::AbsNormPathBuf;
    use buck2_core::fs::paths::abs_path::AbsPath;

    use crate::args::expand_argfile_contents;
    use crate::args::ArgFile;

    #[test]
    fn test_expand_argfile_content() {
        let tempdir = tempfile::tempdir().unwrap();
        let root = AbsPath::new(tempdir.path()).unwrap();
        let mode_file = root.join("mode-file");
        // Test skips empty lines.
        fs_util::write(&mode_file, "a\n\nb\n").unwrap();
        let lines = expand_argfile_contents(&ArgFile::Path(
            AbsNormPathBuf::from(mode_file.to_string_lossy().into_owned()).unwrap(),
        ))
        .unwrap();
        assert_eq!(vec!["a".to_owned(), "b".to_owned()], lines);
    }
}


/*
 * 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.
 */

#![feature(async_closure)]
#![feature(try_blocks)]
#![feature(try_trait_v2)]
#![feature(exit_status_error)]

pub mod args;
pub mod commands;


/*
 * 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::borrow::Cow;
use std::collections::HashMap;
use std::io;
use std::io::Write;
use std::path::Path;

use anyhow::Context;
use async_trait::async_trait;
use buck2_cli_proto::build_request::build_providers;
use buck2_cli_proto::build_request::BuildProviders;
use buck2_cli_proto::build_request::ResponseOptions;
use buck2_cli_proto::build_target::BuildOutput;
use buck2_cli_proto::BuildRequest;
use buck2_cli_proto::BuildTarget;
use buck2_client_ctx::client_ctx::ClientCommandContext;
use buck2_client_ctx::command_outcome::CommandOutcome;
use buck2_client_ctx::common::CommonBuildConfigurationOptions;
use buck2_client_ctx::common::CommonBuildOptions;
use buck2_client_ctx::common::CommonCommandOptions;
use buck2_client_ctx::common::CommonConsoleOptions;
use buck2_client_ctx::common::CommonDaemonCommandOptions;
use buck2_client_ctx::daemon::client::BuckdClientConnector;
use buck2_client_ctx::daemon::client::NoPartialResultHandler;
use buck2_client_ctx::exit_result::ExitResult;
use buck2_client_ctx::exit_result::FailureExitCode;
use buck2_client_ctx::final_console::FinalConsole;
use buck2_client_ctx::output_destination_arg::OutputDestinationArg;
use buck2_client_ctx::streaming::StreamingCommand;
use buck2_core::fs::async_fs_util;
use buck2_core::fs::paths::abs_norm_path::AbsNormPathBuf;
use buck2_core::fs::paths::forward_rel_path::ForwardRelativePath;
use buck2_core::fs::project::ProjectRoot;
use buck2_core::fs::working_dir::WorkingDir;
use dupe::Dupe;
use futures::TryStreamExt;
use gazebo::prelude::*;
use multimap::MultiMap;
use serde::Serialize;

#[derive(Debug, clap::Parser)]
#[clap(name = "build", about = "Build the specified targets")]
pub struct BuildCommand {
    #[clap(flatten)]
    common_opts: CommonCommandOptions,

    #[clap(flatten)]
    build_opts: CommonBuildOptions,

    #[clap(
        long,
        use_delimiter = true,
        help = "Comma separated list of targets at which to root the queryable universe.
                This is useful since targets can exist in multiple configurations."
    )]
    target_universe: Vec<String>,

    #[clap(long = "providers", help = "Print the providers of each target")]
    print_providers: bool,

    /// This option does nothing. It is here to keep compatibility with Buck1 and ci
    #[clap(long = "deep")]
    #[allow(unused)] // for v1 compat
    deep: bool,

    #[clap(
        long = "show-output",
        help = "Print the path to the output for each of the built rules relative to the cell"
    )]
    show_output: bool,

    #[clap(
        long = "show-full-output",
        help = "Print the absolute path to the output for each of the built rules"
    )]
    show_full_output: bool,

    #[clap(
        long = "show-json-output",
        help = "Print the output paths relative to the cell, in JSON format"
    )]
    show_json_output: bool,

    #[clap(
        long = "show-full-json-output",
        help = "Print the output absolute paths, in JSON format"
    )]
    show_full_json_output: bool,

    #[clap(
        long = "materializations",
        help = "Materialize (or skip) the final artifacts, bypassing buckconfig.",
        ignore_case = true,
        arg_enum
    )]
    materializations: Option<FinalArtifactMaterializations>,

    #[allow(unused)]
    #[clap(
        long,
        group = "default-info",
        help = "Build default info (this is the default)"
    )]
    build_default_info: bool,

    #[clap(
        long,
        group = "default-info",
        help = "Do not build default info (this is not the default)"
    )]
    skip_default_info: bool,

    #[allow(unused)]
    #[clap(
        long,
        group = "run-info",
        help = "Build runtime dependencies (this is the default)"
    )]
    build_run_info: bool,

    #[clap(
        long,
        group = "run-info",
        help = "Do not build runtime dependencies (this is not the default)"
    )]
    skip_run_info: bool,

    #[clap(
        long,
        group = "test-info",
        alias = "build-test-dependencies",
        help = "Build tests (this is not the default)"
    )]
    build_test_info: bool,

    #[allow(unused)]
    #[clap(
        long,
        group = "test-info",
        help = "Do not build tests (this is the default)"
    )]
    skip_test_info: bool,

    #[clap(
        long = "out",
        help = "Copy the output of the built target to this path (`-` to stdout)"
    )]
    output_path: Option<OutputDestinationArg>,

    #[clap(name = "TARGET_PATTERNS", help = "Patterns to build")]
    patterns: Vec<String>,
}

impl BuildCommand {
    fn default_info(&self) -> build_providers::Action {
        if self.skip_default_info {
            return build_providers::Action::Skip;
        }
        build_providers::Action::Build
    }

    fn run_info(&self) -> build_providers::Action {
        if self.skip_run_info {
            return build_providers::Action::Skip;
        }
        build_providers::Action::BuildIfAvailable
    }

    fn test_info(&self) -> build_providers::Action {
        if self.build_test_info {
            return build_providers::Action::BuildIfAvailable;
        }
        build_providers::Action::Skip
    }
}

#[derive(Debug, Clone, Dupe, clap::ArgEnum)]
#[clap(rename_all = "snake_case")]
pub enum FinalArtifactMaterializations {
    All,
    None,
}

pub trait MaterializationsToProto {
    fn to_proto(&self) -> buck2_cli_proto::build_request::Materializations;
}
impl MaterializationsToProto for Option<FinalArtifactMaterializations> {
    fn to_proto(&self) -> buck2_cli_proto::build_request::Materializations {
        match self {
            Some(FinalArtifactMaterializations::All) => {
                buck2_cli_proto::build_request::Materializations::Materialize
            }
            Some(FinalArtifactMaterializations::None) => {
                buck2_cli_proto::build_request::Materializations::Skip
            }
            None => buck2_cli_proto::build_request::Materializations::Default,
        }
    }
}

pub fn print_build_result(console: &FinalConsole, error_messages: &[String]) -> anyhow::Result<()> {
    for error_message in error_messages {
        console.print_error(error_message)?;
    }
    Ok(())
}

#[async_trait]
impl StreamingCommand for BuildCommand {
    const COMMAND_NAME: &'static str = "build";

    async fn exec_impl(
        self,
        buckd: &mut BuckdClientConnector,
        matches: &clap::ArgMatches,
        ctx: &mut ClientCommandContext<'_>,
    ) -> ExitResult {
        let show_default_other_outputs = false;
        let context = ctx.client_context(
            &self.common_opts.config_opts,
            matches,
            ctx.sanitized_argv.argv.clone(),
        )?;

        let result = buckd
            .with_flushing()
            .build(
                BuildRequest {
                    context: Some(context),
                    target_patterns: self
                        .patterns
                        .map(|p| buck2_data::TargetPattern { value: p.clone() }),
                    unstable_print_providers: self.print_providers,
                    build_providers: Some(BuildProviders {
                        default_info: self.default_info() as i32,
                        run_info: self.run_info() as i32,
                        test_info: self.test_info() as i32,
                    }),
                    response_options: Some(ResponseOptions {
                        return_outputs: self.show_output
                            || self.show_full_output
                            || self.show_json_output
                            || self.show_full_json_output
                            || self.output_path.is_some(),
                        return_default_other_outputs: show_default_other_outputs,
                    }),
                    build_opts: Some(self.build_opts.to_proto()),
                    final_artifact_materializations: self.materializations.to_proto() as i32,
                    target_universe: self.target_universe,
                },
                ctx.stdin()
                    .console_interaction_stream(&self.common_opts.console_opts),
                &mut NoPartialResultHandler,
            )
            .await;
        let success = match &result {
            Ok(CommandOutcome::Success(response)) => response.error_messages.is_empty(),
            Ok(CommandOutcome::Failure(_)) => false,
            Err(_) => false,
        };

        let console = self.common_opts.console_opts.final_console();

        if success {
            if self.patterns.is_empty() {
                console.print_warning("NO BUILD TARGET PATTERNS SPECIFIED")?;
            } else {
                console.print_success("BUILD SUCCEEDED")?;
            }
        } else {
            console.print_error("BUILD FAILED")?;
        }

        // Action errors will have already been printed, but any other type
        // of error will be printed below the FAILED line here.
        let response = result??;

        print_build_result(&console, &response.error_messages)?;

        let mut stdout = Vec::new();

        if !response.serialized_build_report.is_empty() {
            stdout.extend(response.serialized_build_report.as_bytes());
            writeln!(&mut stdout)?;
        }

        let res = if success {
            if let Some(stdout) = &self.output_path {
                copy_to_out(
                    &response.build_targets,
                    ctx.paths()?.project_root(),
                    &ctx.working_dir,
                    stdout,
                )
                .await
                .context("Error requesting specific output path for --out")?;
            }

            if self.show_output
                || self.show_full_output
                || self.show_json_output
                || self.show_full_json_output
            {
                print_outputs(
                    &mut stdout,
                    response.build_targets,
                    if self.show_full_output || self.show_full_json_output {
                        Some(response.project_root)
                    } else {
                        None
                    },
                    self.show_json_output || self.show_full_json_output,
                    show_default_other_outputs,
                )?;
            }

            ExitResult::success()
        } else {
            ExitResult::failure()
        };

        res.with_stdout(stdout)
    }

    fn console_opts(&self) -> &CommonConsoleOptions {
        &self.common_opts.console_opts
    }

    fn event_log_opts(&self) -> &CommonDaemonCommandOptions {
        &self.common_opts.event_log_opts
    }

    fn common_opts(&self) -> &CommonBuildConfigurationOptions {
        &self.common_opts.config_opts
    }
}

pub(crate) fn print_outputs(
    mut out: impl Write,
    targets: Vec<BuildTarget>,
    root_path: Option<String>,
    as_json: bool,
    show_all_outputs: bool,
) -> anyhow::Result<()> {
    #[derive(Serialize)]
    #[serde(untagged)]
    enum TargetOutputs {
        AllOutputs(MultiMap<String, String>),
        DefaultOutput(HashMap<String, String>),
    }

    impl TargetOutputs {
        fn all_outputs() -> Self {
            Self::AllOutputs(MultiMap::new())
        }

        fn default_output() -> Self {
            Self::DefaultOutput(HashMap::new())
        }

        fn insert(&mut self, target: String, output: String) {
            match self {
                TargetOutputs::AllOutputs(map) => {
                    map.insert(target, output);
                }
                TargetOutputs::DefaultOutput(map) => {
                    map.insert(target, output);
                }
            }
        }
    }

    let mut output_map = if show_all_outputs {
        TargetOutputs::all_outputs()
    } else {
        TargetOutputs::default_output()
    };
    let mut process_output = |target: &String, output: Option<String>| -> anyhow::Result<()> {
        let output = match output {
            Some(output) => {
                let output = if cfg!(windows) {
                    output.replace('/', "\\")
                } else {
                    output
                };
                match &root_path {
                    Some(root) => Path::new(&root).join(output).to_string_lossy().into_owned(),
                    None => output,
                }
            }
            None => "".to_owned(),
        };
        if as_json {
            output_map.insert(target.clone(), output);
        } else {
            writeln!(&mut out, "{} {}", target, output)?;
        }

        Ok(())
    };

    for build_target in targets {
        // just print the default info for build command
        let outputs = build_target.outputs.into_iter().filter(|output| {
            output
                .providers
                .as_ref()
                .map_or(true, |p| show_all_outputs || (p.default_info && !p.other))
        });

        // only print the unconfigured target for now until we migrate everything to support
        // also printing configurations
        if outputs.clone().count() > 1 && !show_all_outputs {
            // We only print the default outputs when we don't `show_all_outputs`,
            // which shouldn't have more than one output.
            // (although we currently don't yet restrict this, but we should).
            process_output(&build_target.target, None)?;
            continue;
        }
        for output in outputs {
            process_output(&build_target.target, Some(output.path))?;
        }
    }

    if as_json {
        serde_json::to_writer(&mut out, &output_map)?;
        writeln!(&mut out)?;
    }

    Ok(())
}

/// Given a list of targets built by this command, extracts a reasonable default output from the list and writes it
/// to the path given by `out`.
///
/// In order to extract a "reasonable default output", this function will bail if any of the following are true:
///  1. Multiple top-level targets were built, in which case the correct output to write is ambiguous,
///  2. A single top-level target was built, but it produced zero default outputs,
///  3. A single top-level target was built, but it produced more than two default outputs
///
/// Otherwise, we'll extract the single default output from the single top-level target and copy it to the output
/// path. If the given path is a directory then all output files will be copied inside of it.
///
/// As a special case, `--out -` is interpreted as `--out /dev/stdout` and allows multiple output files to be
/// written to it.
async fn copy_to_out(
    targets: &[BuildTarget],
    root_path: &ProjectRoot,
    working_dir: &WorkingDir,
    out: &OutputDestinationArg,
) -> anyhow::Result<()> {
    struct OutputToBeCopied {
        from_path: AbsNormPathBuf,
        is_dir: bool,
    }

    let mut outputs_to_be_copied = Vec::new();
    for target in targets {
        let default_outputs: Vec<&BuildOutput> = target
            .outputs
            .iter()
            .filter(|output| {
                output
                    .providers
                    .as_ref()
                    .map_or(true, |p| p.default_info && !p.other)
            })
            .collect();

        let single_default_output = match default_outputs.len() {
            0 => {
                return Err(anyhow::anyhow!(
                    "target {} produced zero default outputs",
                    target.target
                ));
            }
            1 => &default_outputs[0],
            n => {
                return Err(anyhow::anyhow!(
                    "target {} produced {} outputs, choice of output is ambiguous",
                    target.target,
                    n
                ));
            }
        };

        let output_path = root_path
            .root()
            .join(ForwardRelativePath::new(&single_default_output.path)?);
        let output_meta = tokio::fs::metadata(&output_path)
            .await
            .context("Error inspecting file metadata")?;
        let is_dir = output_meta.is_dir();

        outputs_to_be_copied.push(OutputToBeCopied {
            from_path: output_path,
            is_dir,
        });
    }

    match out {
        OutputDestinationArg::Stream => {
            // Check no output is a directory. We allow outputting any number of
            // files (including 0) to stdout.
            if let Some(dir_i) = outputs_to_be_copied.iter().position(|o| o.is_dir) {
                return Err(anyhow::anyhow!(
                    "target {} produces a default output that is a directory, and cannot be sent to stdout",
                    targets[dir_i].target,
                ));
            }
        }
        OutputDestinationArg::Path(..) => {
            // Check we are outputting exactly 1 target. Okay if directory.
            if outputs_to_be_copied.len() != 1 {
                return Err(anyhow::anyhow!(
                    "build command built multiple top-level targets, choice of output is ambiguous"
                ));
            }
        }
    }

    for to_be_copied in outputs_to_be_copied {
        match out {
            OutputDestinationArg::Stream => {
                let mut file = async_fs_util::open(&to_be_copied.from_path).await?;
                tokio::io::copy(&mut file, &mut tokio::io::stdout())
                    .await
                    .map_err(convert_broken_pipe_error)?;
            }
            OutputDestinationArg::Path(path) => {
                let path = path.resolve(working_dir);
                if to_be_copied.is_dir {
                    copy_directory(&to_be_copied.from_path, &path).await?;
                } else {
                    copy_file(&to_be_copied.from_path, &path).await?;
                }
            }
        }
    }

    Ok(())
}

/// Recursively copies a directory to the output path, rooted at `dst`.
#[async_recursion::async_recursion]
async fn copy_directory(src: &Path, dst: &Path) -> anyhow::Result<()> {
    tokio::fs::create_dir_all(dst).await?;
    let stream = tokio_stream::wrappers::ReadDirStream::new(
        tokio::fs::read_dir(src)
            .await
            .context(format!("reading directory {:?}", src))?,
    )
    .err_into::<anyhow::Error>();
    stream
        .try_for_each(|entry| async move {
            if entry.file_type().await?.is_dir() {
                copy_directory(&entry.path(), &dst.join(entry.file_name()))
                    .await
                    .context(format!("copying subdirectory {:?}", entry.path()))
            } else {
                tokio::fs::copy(&entry.path(), &dst.join(entry.file_name()))
                    .await
                    .context(format!("copying file {:?}", entry.path()))
                    .map(|_| ())
            }
        })
        .await?;

    Ok(())
}

async fn copy_file(src: &Path, dst: &Path) -> anyhow::Result<()> {
    if let Some(parent) = dst.parent() {
        if !parent.exists() {
            return Err(anyhow::anyhow!(
                "Directory `{}` does not exist",
                parent.display()
            ));
        }
    }
    let dest_path = match dst.is_dir() {
        true => Cow::Owned(dst.join(src.file_name().context("Failed getting output name")?)),
        false => Cow::Borrowed(dst),
    };

    // NOTE: We don't do the overwrite since we might be writing to e.g. a pipe here and we can't
    // do an atomic move into it.
    match tokio::fs::copy(src, &dest_path).await {
        Ok(..) => Ok(()),
        Err(e) if e.raw_os_error() == Some(libc::ETXTBSY) => {
            let dir = dest_path.parent().context("Output path has no parent")?;
            let mut tmp_name = dest_path
                .file_name()
                .context("Output path has no file name")?
                .to_owned();
            tmp_name.push(".buck2.tmp");
            let tmp_path = dir.join(tmp_name);
            tokio::fs::copy(src, &tmp_path).await?;
            tokio::fs::rename(&tmp_path, dest_path).await?;
            Ok(())
        }
        Err(e) => Err(convert_broken_pipe_error(e)),
    }
}

fn convert_broken_pipe_error(e: io::Error) -> anyhow::Error {
    if e.kind() == io::ErrorKind::BrokenPipe {
        anyhow::Error::new(FailureExitCode::OutputFileBrokenPipe)
    } else {
        anyhow::Error::new(e).context("Error writing build artifact to --out")
    }
}

#[cfg(test)]
mod test {
    use assert_matches::assert_matches;
    use build_providers::Action;
    use clap::Parser;

    use super::*;

    fn parse(args: &[&str]) -> anyhow::Result<BuildCommand> {
        Ok(BuildCommand::from_iter_safe(
            std::iter::once("program").chain(args.iter().copied()),
        )?)
    }

    #[test]
    fn infos_default() -> anyhow::Result<()> {
        let opts = parse(&[])?;

        assert_eq!(opts.default_info(), Action::Build);
        assert_eq!(opts.run_info(), Action::BuildIfAvailable);
        assert_eq!(opts.test_info(), Action::Skip);

        Ok(())
    }

    #[test]
    fn infos_noop() -> anyhow::Result<()> {
        let opts = parse(&[
            "--skip-test-info",
            "--build-default-info",
            "--build-run-info",
        ])?;

        assert_eq!(opts.default_info(), Action::Build);
        assert_eq!(opts.run_info(), Action::BuildIfAvailable);
        assert_eq!(opts.test_info(), Action::Skip);

        Ok(())
    }

    #[test]
    fn infos_configure() -> anyhow::Result<()> {
        let opts = parse(&["--skip-default-info"])?;
        assert_eq!(opts.default_info(), Action::Skip);

        let opts = parse(&["--skip-run-info"])?;
        assert_eq!(opts.run_info(), Action::Skip);

        let opts = parse(&["--build-test-info"])?;
        assert_eq!(opts.test_info(), Action::BuildIfAvailable);

        // Legacy flag from before we could configure the other options.
        let opts = parse(&["--build-test-dependencies"])?;
        assert_eq!(opts.test_info(), Action::BuildIfAvailable);

        Ok(())
    }

    #[test]
    fn infos_validation() -> anyhow::Result<()> {
        // Test duplicate args
        assert_matches!(
            parse(&["--build-default-info", "--skip-default-info"]),
            Err(..)
        );
        assert_matches!(parse(&["--build-run-info", "--skip-run-info"]), Err(..));
        assert_matches!(parse(&["--build-test-info", "--skip-test-info"]), Err(..));

        // Test args across all groups.
        assert_matches!(
            parse(&[
                "--skip-default-info",
                "--skip-run-info",
                "--build-test-info"
            ]),
            Ok(..)
        );

        Ok(())
    }

    #[cfg(unix)]
    mod unix {
        use assert_matches::assert_matches;
        use tokio::process::Command;

        use super::*;

        #[tokio::test]
        async fn test_copy_file() -> anyhow::Result<()> {
            let dir = tempfile::tempdir()?;
            let out = dir.path().join("sleep");

            let res = Command::new("cp")
                .arg(Path::new("/bin/sleep"))
                .arg(&out)
                .spawn()?
                .wait()
                .await?;

            assert!(res.success());

            let mut proc = Command::new(&out)
                .arg("10000")
                .kill_on_drop(true)
                .spawn()
                .context("Error spawning")?;

            // This will fail if we don't handle ETXTBSY.
            copy_file(Path::new("/bin/sleep"), &out).await?;

            // Check that our sleep didn't end
            assert_matches!(proc.try_wait(), Ok(None));

            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.
 */

use async_trait::async_trait;
use buck2_cli_proto::BxlRequest;
use buck2_client_ctx::client_ctx::ClientCommandContext;
use buck2_client_ctx::command_outcome::CommandOutcome;
use buck2_client_ctx::common::CommonBuildConfigurationOptions;
use buck2_client_ctx::common::CommonBuildOptions;
use buck2_client_ctx::common::CommonCommandOptions;
use buck2_client_ctx::common::CommonConsoleOptions;
use buck2_client_ctx::common::CommonDaemonCommandOptions;
use buck2_client_ctx::daemon::client::BuckdClientConnector;
use buck2_client_ctx::daemon::client::StdoutPartialResultHandler;
use buck2_client_ctx::exit_result::ExitResult;
use buck2_client_ctx::path_arg::PathArg;
use buck2_client_ctx::streaming::StreamingCommand;

use crate::commands::build::print_build_result;
use crate::commands::build::FinalArtifactMaterializations;
use crate::commands::build::MaterializationsToProto;

#[derive(Debug, clap::Parser)]
#[clap(name = "bxl", about = "Run BXL scripts")]
pub struct BxlCommand {
    #[clap(flatten)]
    bxl_opts: BxlCommandOptions,

    #[clap(flatten)]
    common_ops: CommonCommandOptions,
}

#[derive(Debug, clap::Parser)]
pub struct BxlCommandOptions {
    #[clap(flatten)]
    build_opts: CommonBuildOptions,

    #[clap(
        long = "materializations",
        help = "Materialize (or skip) the final artifacts, bypassing buckconfig.",
        ignore_case = true,
        arg_enum
    )]
    materializations: Option<FinalArtifactMaterializations>,

    #[clap(
        name = "BXL label",
        help = "The bxl function to execute as defined by the label of form `<cell>//path/file.bxl:<function>`"
    )]
    pub bxl_label: String,

    #[clap(
        name = "BXL INPUT ARGS",
        help = "Arguments passed to the bxl script",
        raw = true
    )]
    pub bxl_args: Vec<String>,

    /// Write user events to this log file. Both user and internal events are written to main event log.
    /// If this flag is specified, user events are additionally written to user event log.
    /// Log format is JSONL, uncompressed if no known extensions are detected, or you can explicitly specify
    /// the compression via the file extension (ex: `.json-lines.gz` would be gzip compressed, `.json-lines.zst`
    /// would be zstd compressed). Resulting log is is compatible with `buck2 log show-user`.
    #[clap(value_name = "PATH", long = "--user-event-log")]
    pub user_event_log: Option<PathArg>,
}

#[async_trait]
impl StreamingCommand for BxlCommand {
    const COMMAND_NAME: &'static str = "bxl";

    async fn exec_impl(
        self,
        buckd: &mut BuckdClientConnector,
        matches: &clap::ArgMatches,
        ctx: &mut ClientCommandContext<'_>,
    ) -> ExitResult {
        let context = ctx.client_context(
            &self.common_ops.config_opts,
            matches,
            ctx.sanitized_argv.argv.clone(),
        )?;
        let result = buckd
            .with_flushing()
            .bxl(
                BxlRequest {
                    context: Some(context),
                    bxl_label: self.bxl_opts.bxl_label,
                    bxl_args: self.bxl_opts.bxl_args,
                    build_opts: Some(self.bxl_opts.build_opts.to_proto()),
                    final_artifact_materializations: self.bxl_opts.materializations.to_proto()
                        as i32,
                    print_stacktrace: ctx.verbosity.print_success_stderr(),
                },
                ctx.stdin()
                    .console_interaction_stream(&self.common_ops.console_opts),
                &mut StdoutPartialResultHandler,
            )
            .await;
        let success = match &result {
            Ok(CommandOutcome::Success(response)) => response.error_messages.is_empty(),
            _ => false,
        };

        let console = self.common_ops.console_opts.final_console();

        if success {
            console.print_success("BXL SUCCEEDED")?;
        } else {
            console.print_error("BXL FAILED")?;
        }

        // Action errors will have already been printed, but any other type
        // of error will be printed below the FAILED line here.
        let response = result??;

        print_build_result(&console, &response.error_messages)?;

        if !success {
            return ExitResult::failure();
        }

        ExitResult::success()
    }

    fn console_opts(&self) -> &CommonConsoleOptions {
        &self.common_ops.console_opts
    }

    fn event_log_opts(&self) -> &CommonDaemonCommandOptions {
        &self.common_ops.event_log_opts
    }

    fn common_opts(&self) -> &CommonBuildConfigurationOptions {
        &self.common_ops.config_opts
    }

    fn user_event_log(&self) -> &Option<PathArg> {
        &self.bxl_opts.user_event_log
    }
}


/*
 * 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 std::sync::Mutex;
use std::time::Duration;

use anyhow::Context;
use buck2_client_ctx::argv::Argv;
use buck2_client_ctx::argv::SanitizedArgv;
use buck2_client_ctx::client_ctx::ClientCommandContext;
use buck2_client_ctx::common::CommonCommandOptions;
use buck2_client_ctx::daemon::client::BuckdLifecycleLock;
use buck2_client_ctx::exit_result::ExitResult;
use buck2_client_ctx::final_console::FinalConsole;
use buck2_client_ctx::startup_deadline::StartupDeadline;
use buck2_client_ctx::streaming::BuckSubcommand;
use buck2_common::daemon_dir::DaemonDir;
use buck2_core::fs::fs_util;
use buck2_core::fs::paths::abs_norm_path::AbsNormPathBuf;
use buck2_core::fs::paths::abs_path::AbsPath;
use buck2_core::fs::paths::abs_path::AbsPathBuf;
use dupe::Dupe;
use gazebo::prelude::SliceExt;
use humantime;
use threadpool::ThreadPool;
use walkdir::WalkDir;

use crate::commands::clean_stale::parse_clean_stale_args;
use crate::commands::clean_stale::CleanStaleCommand;
use crate::commands::kill::kill_command_impl;

/// Delete generated files and caches.
///
/// The command also kills the buck2 daemon.
#[derive(Debug, clap::Parser)]
pub struct CleanCommand {
    #[clap(flatten)]
    common_opts: CommonCommandOptions,

    #[clap(
        long = "dry-run",
        help = "Performs a dry-run and prints the paths that would be removed."
    )]
    dry_run: bool,

    #[clap(
        long = "stale",
        help = "Delete artifacts from buck-out older than 1 week or older than
the specified duration, without killing the daemon",
        value_name = "DURATION"
    )]
    stale: Option<Option<humantime::Duration>>,

    // Like stale but since a specific timestamp, for testing
    #[clap(long = "keep-since-time", conflicts_with = "stale", hidden = true)]
    keep_since_time: Option<i64>,

    #[clap(long = "tracked-only", requires = "stale")]
    tracked_only: bool,
}

impl CleanCommand {
    pub fn exec(self, matches: &clap::ArgMatches, ctx: ClientCommandContext<'_>) -> ExitResult {
        if let Some(keep_since_arg) = parse_clean_stale_args(self.stale, self.keep_since_time)? {
            let cmd = CleanStaleCommand {
                common_opts: self.common_opts,
                keep_since_arg,
                dry_run: self.dry_run,
                tracked_only: self.tracked_only,
            };
            return cmd.exec(matches, ctx);
        }

        ctx.instant_command("clean", async move |ctx| {
            let buck_out_dir = ctx.paths()?.buck_out_path();
            let daemon_dir = ctx.paths()?.daemon_dir()?;
            let console = &self.common_opts.console_opts.final_console();

            if self.dry_run {
                return clean(buck_out_dir, daemon_dir, console, None).await;
            }

            // Kill the daemon and make sure a new daemon does not spin up while we're performing clean up operations
            // This will ensure we have exclusive access to the directories in question
            let lifecycle_lock = BuckdLifecycleLock::lock_with_timeout(
                daemon_dir.clone(),
                StartupDeadline::duration_from_now(Duration::from_secs(10))?,
            )
            .await
            .with_context(|| "Error locking buckd lifecycle.lock")?;

            kill_command_impl(&lifecycle_lock, "`buck2 clean` was invoked").await?;

            clean(buck_out_dir, daemon_dir, console, Some(&lifecycle_lock)).await
        })
    }

    pub fn sanitize_argv(&self, argv: Argv) -> SanitizedArgv {
        argv.no_need_to_sanitize()
    }
}

async fn clean(
    buck_out_dir: AbsNormPathBuf,
    daemon_dir: DaemonDir,
    console: &FinalConsole,
    // None means "dry run".
    lifecycle_lock: Option<&BuckdLifecycleLock>,
) -> anyhow::Result<()> {
    let mut paths_to_clean = Vec::new();
    // Try to clean EdenFS based buck-out first. For EdenFS based buck-out, "eden rm"
    // is efficient. Notice eden rm will remove the buck-out root directory,
    // but for the native fs, the buck-out root directory is kept.
    if let Some(paths) = try_clean_eden_buck_out(&buck_out_dir, lifecycle_lock.is_none()).await? {
        paths_to_clean = paths;
    } else if buck_out_dir.exists() {
        paths_to_clean =
            collect_paths_to_clean(&buck_out_dir)?.map(|path| path.display().to_string());
        if lifecycle_lock.is_some() {
            tokio::task::spawn_blocking(move || clean_buck_out_with_retry(&buck_out_dir))
                .await?
                .context("Failed to spawn clean")?;
        }
    }

    if daemon_dir.path.exists() {
        paths_to_clean.push(daemon_dir.to_string());
        if let Some(lifecycle_lock) = lifecycle_lock {
            lifecycle_lock.clean_daemon_dir()?;
        }
    }

    for path in paths_to_clean {
        console.print_stderr(&path)?;
    }
    Ok(())
}

fn collect_paths_to_clean(buck_out_path: &AbsNormPathBuf) -> anyhow::Result<Vec<AbsNormPathBuf>> {
    let mut paths_to_clean = vec![];
    let dir = fs_util::read_dir(buck_out_path)?;
    for entry in dir {
        let entry = entry?;
        let path = entry.path();
        paths_to_clean.push(path);
    }

    Ok(paths_to_clean)
}

/// In Windows, we've observed the buck-out clean immediately after killing
/// the daemon can fail with this error: `The process cannot access the
/// file because it is being used by another process.`. To get around this,
/// add a single retry.
fn clean_buck_out_with_retry(path: &AbsNormPathBuf) -> anyhow::Result<()> {
    let mut result = clean_buck_out(path);
    match result {
        Ok(_) => {
            return result;
        }
        Err(e) => {
            tracing::info!(
                "Retrying buck-out clean, first attempted failed with: {:#}",
                e
            );
            result = clean_buck_out(path);
        }
    }
    result
}

fn clean_buck_out(path: &AbsNormPathBuf) -> anyhow::Result<()> {
    let walk = WalkDir::new(path);
    let thread_pool = ThreadPool::new(num_cpus::get());
    let error = Arc::new(Mutex::new(None));
    // collect dir paths to delete them after deleting files in them
    // we need reverse order to make sure the dir is already empty when
    // we delete it, otherwise remove would fail with DirNotEmpty exception
    let mut reverse_dir_paths = Vec::new();
    for dir_entry in walk.into_iter().flatten() {
        if dir_entry.file_type().is_dir() {
            // The walk gives us back absolute paths since we give it absolute paths.
            reverse_dir_paths.push(AbsPathBuf::new(dir_entry.into_path()).unwrap());
        } else {
            let error = error.dupe();
            thread_pool.execute(move || {
                // The wlak gives us back absolute paths since we give it absolute paths.
                let res = AbsPath::new(dir_entry.path()).and_then(fs_util::remove_file);

                match res {
                    Ok(_) => {}
                    Err(e) => {
                        let mut error = error.lock().unwrap();
                        if error.is_none() {
                            *error = Some(e);
                        }
                    }
                }
            })
        }
    }

    thread_pool.join();
    if let Some(e) = error.lock().unwrap().take() {
        return Err(e);
    }

    // first entry is buck-out root dir and we don't want to remove it
    for path in reverse_dir_paths.iter().skip(1).rev() {
        fs_util::remove_dir(path)?;
    }

    Ok(())
}

#[cfg(any(fbcode_build, cargo_internal_build))]
async fn try_clean_eden_buck_out(
    buck_out: &AbsNormPathBuf,
    dryrun: bool,
) -> anyhow::Result<Option<Vec<String>>> {
    use std::process::Stdio;

    use buck2_execute::materialize::eden_api::is_recas_eden_mount;
    use buck2_util::process::async_background_command;

    if !cfg!(unix) {
        return Ok(None);
    }

    if !is_recas_eden_mount(buck_out)? {
        return Ok(None);
    }

    // Run eden rm <buck-out> to rm a mount
    let mut eden_rm_cmd = async_background_command("eden");
    eden_rm_cmd
        .arg("rm")
        .arg("-y") // No promot
        .arg(buck_out.as_os_str())
        .current_dir("/")
        .stdout(Stdio::piped())
        .stderr(Stdio::piped());

    buck2_client_ctx::eprintln!(
        "The following command will be executed: `eden rm -y {}`",
        buck_out
    )?;

    if !dryrun {
        eden_rm_cmd
            .spawn()
            .context("Failed to start to remove EdenFS buck-out mount.")?
            .wait()
            .await
            .context("Failed to remove EdenFS buck-out mount.")?;

        // eden rm might not delete the buck-out completed.
        if buck_out.exists() {
            fs_util::remove_dir(buck_out)?;
        }
    }

    Ok(Some(vec![buck_out.display().to_string()]))
}

#[cfg(not(any(fbcode_build, cargo_internal_build)))]
async fn try_clean_eden_buck_out(
    _buck_out: &AbsNormPathBuf,
    _dryrun: bool,
) -> anyhow::Result<Option<Vec<String>>> {
    Ok(None)
}


/*
 * 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 anyhow::Context;
use async_trait::async_trait;
use buck2_cli_proto::CleanStaleRequest;
use buck2_cli_proto::CleanStaleResponse;
use buck2_client_ctx::client_ctx::ClientCommandContext;
use buck2_client_ctx::common::CommonBuildConfigurationOptions;
use buck2_client_ctx::common::CommonCommandOptions;
use buck2_client_ctx::common::CommonConsoleOptions;
use buck2_client_ctx::common::CommonDaemonCommandOptions;
use buck2_client_ctx::daemon::client::BuckdClientConnector;
use buck2_client_ctx::daemon::client::NoPartialResultHandler;
use buck2_client_ctx::exit_result::ExitResult;
use buck2_client_ctx::streaming::StreamingCommand;
use chrono::DateTime;
use chrono::Duration;
use chrono::TimeZone;
use chrono::Utc;
use humantime;

/// Clean only old artifacts from a running buck daemon without killing the daemon.
///
/// This is a separate command from CleanCommand even though it is invoked with
/// a flag (--stale) on the clean subcommand, which is a bit weird.
/// This is just so that it can be used as a StreamingCommand, which CleanCommand should not be.
pub struct CleanStaleCommand {
    pub(crate) common_opts: CommonCommandOptions,
    pub keep_since_arg: KeepSinceArg,
    pub dry_run: bool,
    pub tracked_only: bool,
}

/// Specifies the maximum age of artifacts to keep
pub enum KeepSinceArg {
    Duration(Duration),
    Time(i64),
}

pub fn parse_clean_stale_args(
    stale: Option<Option<humantime::Duration>>,
    keep_since_time: Option<i64>,
) -> anyhow::Result<Option<KeepSinceArg>> {
    let arg = match (stale, keep_since_time) {
        (Some(Some(human_duration)), None) => {
            let duration = chrono::Duration::from_std(human_duration.into())?;
            Some(KeepSinceArg::Duration(duration))
        }
        (Some(None), None) => Some(KeepSinceArg::Duration(chrono::Duration::weeks(1))),
        (None, Some(time)) => Some(KeepSinceArg::Time(time)),
        (Some(_), Some(_)) => unreachable!("keep-since-time conflicts_with stale"),
        (None, None) => None,
    };
    Ok(arg)
}

fn format_result_stats(stats: buck2_data::CleanStaleStats) -> String {
    let mut output = String::new();
    output += &format!(
        "Found {} stale artifacts ({})\n",
        stats.stale_artifact_count,
        bytesize::to_string(stats.stale_bytes, true),
    );
    output += &format!(
        "Found {} recent artifacts ({})\n",
        stats.retained_artifact_count,
        bytesize::to_string(stats.retained_bytes, true),
    );
    output += &format!(
        "Found {} untracked artifacts ({})\n",
        stats.untracked_artifact_count,
        bytesize::to_string(stats.untracked_bytes, true),
    );
    if stats.cleaned_path_count > 0 || stats.cleaned_bytes > 0 {
        output += &format!(
            "Cleaned {} paths ({} artifacts)\n",
            stats.cleaned_path_count, stats.cleaned_artifact_count,
        );
        output += &format!(
            "{} bytes cleaned ({})\n",
            stats.cleaned_bytes,
            bytesize::to_string(stats.cleaned_bytes, true),
        );
    }
    output
}

#[async_trait]
impl StreamingCommand for CleanStaleCommand {
    const COMMAND_NAME: &'static str = "clean-stale";

    async fn exec_impl(
        self,
        buckd: &mut BuckdClientConnector,
        matches: &clap::ArgMatches,
        ctx: &mut ClientCommandContext<'_>,
    ) -> ExitResult {
        let keep_since_time = match self.keep_since_arg {
            KeepSinceArg::Duration(duration) => {
                let keep_since_time: DateTime<Utc> = Utc::now()
                    .checked_sub_signed(duration)
                    .context("Duration underflow")?;
                buck2_client_ctx::eprintln!(
                    "Cleaning artifacts more than {} old",
                    humantime::format_duration(
                        duration.to_std().context("Error converting duration")?
                    ),
                )?;
                // Round up to next second since timestamp below is rounded down
                // (this way clean --stale=0s immediately after a build deletes the result)
                keep_since_time + chrono::Duration::seconds(1)
            }
            KeepSinceArg::Time(timestamp) => Utc
                .timestamp_opt(timestamp, 0)
                .single()
                .context("Invalid timestamp")?,
        };

        let context = ctx.client_context(
            &self.common_opts.config_opts,
            matches,
            ctx.sanitized_argv.argv.clone(),
        )?;
        let response: CleanStaleResponse = buckd
            .with_flushing()
            .clean_stale(
                CleanStaleRequest {
                    context: Some(context),
                    keep_since_time: keep_since_time.timestamp(),
                    dry_run: self.dry_run,
                    tracked_only: self.tracked_only,
                },
                ctx.stdin()
                    .console_interaction_stream(&self.common_opts.console_opts),
                &mut NoPartialResultHandler,
            )
            .await??;

        if let Some(message) = response.message {
            buck2_client_ctx::eprintln!("{}", message)?;
        }
        if let Some(stats) = response.stats {
            buck2_client_ctx::eprintln!("{}", format_result_stats(stats))?;
        }
        ExitResult::success()
    }

    fn console_opts(&self) -> &CommonConsoleOptions {
        &self.common_opts.console_opts
    }

    fn event_log_opts(&self) -> &CommonDaemonCommandOptions {
        &self.common_opts.event_log_opts
    }

    fn common_opts(&self) -> &CommonBuildConfigurationOptions {
        &self.common_opts.config_opts
    }
}


/*
 * 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 async_trait::async_trait;
use buck2_cli_proto::ConfiguredTargetsRequest;
use buck2_cli_proto::ConfiguredTargetsResponse;
use buck2_client_ctx::client_ctx::ClientCommandContext;
use buck2_client_ctx::common::CommonBuildConfigurationOptions;
use buck2_client_ctx::common::CommonCommandOptions;
use buck2_client_ctx::common::CommonConsoleOptions;
use buck2_client_ctx::common::CommonDaemonCommandOptions;
use buck2_client_ctx::daemon::client::BuckdClientConnector;
use buck2_client_ctx::daemon::client::NoPartialResultHandler;
use buck2_client_ctx::exit_result::ExitResult;
use buck2_client_ctx::streaming::StreamingCommand;
use gazebo::prelude::SliceExt;

/// Resolve target patterns to configured targets.
#[derive(Debug, clap::Parser)]
#[clap(name = "ctargets")]
pub struct ConfiguredTargetsCommand {
    #[clap(flatten)]
    common_opts: CommonCommandOptions,

    /// Skip missing targets from `BUCK` files when non-glob pattern is specified.
    /// This option does not skip missing packages
    /// and does not ignore errors of `BUCK` file evaluation.
    #[clap(long)]
    skip_missing_targets: bool,

    /// Patterns to interpret.
    #[clap(name = "TARGET_PATTERNS")]
    patterns: Vec<String>,
}

#[async_trait]
impl StreamingCommand for ConfiguredTargetsCommand {
    const COMMAND_NAME: &'static str = "ctargets";

    async fn exec_impl(
        self,
        buckd: &mut BuckdClientConnector,
        matches: &clap::ArgMatches,
        ctx: &mut ClientCommandContext<'_>,
    ) -> ExitResult {
        let context = Some(ctx.client_context(
            &self.common_opts.config_opts,
            matches,
            ctx.sanitized_argv.argv.clone(),
        )?);
        let ConfiguredTargetsResponse {
            serialized_targets_output,
        } = buckd
            .with_flushing()
            .ctargets(
                ConfiguredTargetsRequest {
                    context,
                    target_patterns: self.patterns.map(|pat| buck2_data::TargetPattern {
                        value: pat.to_owned(),
                    }),
                    skip_missing_targets: self.skip_missing_targets,
                },
                ctx.stdin()
                    .console_interaction_stream(&self.common_opts.console_opts),
                &mut NoPartialResultHandler,
            )
            .await??;

        buck2_client_ctx::print!("{}", serialized_targets_output)?;

        ExitResult::success()
    }

    fn console_opts(&self) -> &CommonConsoleOptions {
        &self.common_opts.console_opts
    }

    fn event_log_opts(&self) -> &CommonDaemonCommandOptions {
        &self.common_opts.event_log_opts
    }

    fn common_opts(&self) -> &CommonBuildConfigurationOptions {
        &self.common_opts.config_opts
    }
}


/*
 * 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::io::ErrorKind;
use std::io::Write;
use std::process::Command;

use anyhow::Context;
use buck2_client_ctx::argv::Argv;
use buck2_client_ctx::argv::SanitizedArgv;
use buck2_client_ctx::client_ctx::ClientCommandContext;
use buck2_client_ctx::common::CommonConsoleOptions;
use buck2_client_ctx::exit_result::ExitResult;
use buck2_client_ctx::final_console::FinalConsole;
use buck2_client_ctx::path_arg::PathArg;
use buck2_core::fs::fs_util;
use buck2_core::fs::paths::abs_path::AbsPath;

/// Buck2 Init
///
/// This command is intended to be part-tutorial part-convenience
/// for generating buck2 projects. Given a path and optional name
/// (in the case that the folder name is not desirable).
#[derive(Debug, clap::Parser)]
#[clap(name = "install", about = "Initialize a buck2 project")]
pub struct InitCommand {
    #[clap(flatten)]
    console_opts: CommonConsoleOptions,

    /// The path to initialize the project in. The folder does not need to exist.
    #[clap(default_value = ".")]
    path: PathArg,

    /// The name for the project. If not provided will default to the last segment
    /// of the path.
    #[clap(short, long)]
    name: Option<String>,

    /// Don't generate a prelude or a toolchain.
    #[clap(long)]
    no_prelude: bool,

    /// Initialize the project even if the git repo at \[PATH\] has uncommitted changes.
    #[clap(long)]
    allow_dirty: bool,

    // Use git to initialize the project and pull in buck2-prelude as a submodule
    #[clap(long)]
    git: bool,
}

impl InitCommand {
    pub fn exec(self, _matches: &clap::ArgMatches, ctx: ClientCommandContext<'_>) -> ExitResult {
        let console = self.console_opts.final_console();

        match exec_impl(self, ctx, &console) {
            Ok(_) => ExitResult::status(0),
            Err(e) => {
                // include the backtrace with the error output
                // (same behaviour as returning the Error from main)
                console.print_error(&format!("{:?}", e))?;
                ExitResult::status(1)
            }
        }
    }

    pub fn sanitize_argv(&self, argv: Argv) -> SanitizedArgv {
        argv.no_need_to_sanitize()
    }
}

fn exec_impl(
    cmd: InitCommand,
    ctx: ClientCommandContext<'_>,
    console: &FinalConsole,
) -> anyhow::Result<()> {
    let path = cmd.path.resolve(&ctx.working_dir);
    fs_util::create_dir_all(&path)?;
    let absolute = fs_util::canonicalize(&path)?;
    let git = cmd.git;

    if absolute.is_file() {
        return Err(anyhow::anyhow!(
            "Target path {} cannot be an existing file",
            absolute.display()
        ));
    }

    if git {
        let status = match Command::new("git")
            .args(["status", "--porcelain"])
            .current_dir(&absolute)
            .output()
        {
            Err(e) if e.kind().eq(&ErrorKind::NotFound) => {
                console.print_error(
                    "Warning: no git found on path, can't check for dirty repo. Proceeding anyway.",
                )?;
                None
            }
            r => Some(r.context("Couldn't detect dirty status of folder.")?),
        };

        let changes = status.filter(|o| o.status.success()).map(|o| {
            String::from_utf8_lossy(&o.stdout)
                .trim()
                .lines()
                .any(|l| !l.starts_with("??"))
        });

        if let (Some(true), false) = (changes, cmd.allow_dirty) {
            return Err(anyhow::anyhow!(
                "Refusing to initialize in a dirty repo. Stash your changes or use `--allow-dirty` to override."
            ));
        }
    }

    set_up_project(&absolute, git, !cmd.no_prelude)
}

fn initialize_buckconfig(repo_root: &AbsPath, prelude: bool, git: bool) -> anyhow::Result<()> {
    let mut buckconfig = std::fs::File::create(repo_root.join(".buckconfig"))?;
    writeln!(buckconfig, "[repositories]")?;
    writeln!(buckconfig, "root = .")?;
    writeln!(buckconfig, "prelude = prelude")?;

    // Add additional configs that depend on prelude / no-prelude mode
    if prelude {
        writeln!(buckconfig, "toolchains = toolchains")?;
        writeln!(buckconfig, "none = none")?;
        writeln!(buckconfig)?;
        writeln!(buckconfig, "[repository_aliases]")?;
        writeln!(buckconfig, "config = prelude")?;
        writeln!(buckconfig, "fbcode = none")?;
        writeln!(buckconfig, "fbsource = none")?;
        writeln!(buckconfig, "buck = none")?;
        writeln!(buckconfig)?;
        writeln!(buckconfig, "[parser]")?;
        writeln!(
            buckconfig,
            "target_platform_detector_spec = target:root//...->prelude//platforms:default"
        )?;
    } else {
        // For the no-prelude mode, create an empty prelude/prelude.bzl as Buck2 expects one.
        let prelude_dir = repo_root.join("prelude");
        fs_util::create_dir(&prelude_dir)?;
        fs_util::create_file(prelude_dir.join("prelude.bzl"))?;
    }
    if git {
        writeln!(buckconfig)?;
        writeln!(buckconfig, "[project]")?;
        writeln!(buckconfig, "ignore = .git")?;
    }
    Ok(())
}

fn initialize_toolchains_buck(repo_root: &AbsPath) -> anyhow::Result<()> {
    let mut buck = std::fs::File::create(repo_root.join("BUCK"))?;

    writeln!(
        buck,
        "load(\"@prelude//toolchains:genrule.bzl\", \"system_genrule_toolchain\")",
    )?;
    writeln!(buck)?;
    writeln!(buck, "system_genrule_toolchain(")?;
    writeln!(buck, "    name = \"genrule\",")?;
    writeln!(buck, "    visibility = [\"PUBLIC\"],")?;
    writeln!(buck, ")")?;

    Ok(())
}

fn initialize_root_buck(repo_root: &AbsPath, prelude: bool) -> anyhow::Result<()> {
    let mut buck = std::fs::File::create(repo_root.join("BUCK"))?;

    if prelude {
        writeln!(
            buck,
            "# A list of available rules and their signatures can be found here: https://buck2.build/docs/api/rules/"
        )?;
        writeln!(buck)?;
        writeln!(buck, "genrule(")?;
        writeln!(buck, "    name = \"hello_world\",")?;
        writeln!(buck, "    out = \"out.txt\",")?;
        writeln!(buck, "    cmd = \"echo BUILT BY BUCK2> $OUT\",")?;
        writeln!(buck, ")")?;
    }
    // TODO: Add a doc pointers for rules
    Ok(())
}

fn set_up_prelude(repo_root: &AbsPath, git: bool) -> anyhow::Result<()> {
    if git {
        if !Command::new("git")
            .args([
                "submodule",
                "add",
                "https://github.com/facebook/buck2-prelude.git",
                "prelude",
            ])
            .current_dir(repo_root)
            .status()?
            .success()
        {
            return Err(anyhow::anyhow!(
                "Unable to clone the prelude. Is the folder in use?"
            ));
        }
    } else {
        println!(
            "* Download https://github.com/facebookincubator/buck2-prelude.git into `prelude/` with a VCS of your choice."
        );
        println!("* If you wish to use git submodule, run the command again with --git",);
    }
    Ok(())
}

fn set_up_gitignore(repo_root: &AbsPath) -> anyhow::Result<()> {
    let gitignore = repo_root.join(".gitignore");
    // If .gitignore is empty or doesn't exist, add in buck-out
    if !gitignore.exists() || fs_util::metadata(&gitignore)?.len() == 0 {
        fs_util::write(gitignore, "/buck-out\n")?;
    }
    Ok(())
}

fn set_up_buckroot(repo_root: &AbsPath) -> anyhow::Result<()> {
    fs_util::write(repo_root.join(".buckroot"), "")?;
    Ok(())
}

fn set_up_project(repo_root: &AbsPath, git: bool, prelude: bool) -> anyhow::Result<()> {
    set_up_buckroot(repo_root)?;

    if git {
        if !Command::new("git")
            .arg("init")
            .current_dir(repo_root)
            .status()?
            .success()
        {
            return Err(anyhow::anyhow!("Failure when running `git init`."));
        };
        set_up_gitignore(repo_root)?;
    }

    if prelude {
        set_up_prelude(repo_root, git)?;
    }

    // If the project already contains a .buckconfig, leave it alone
    if repo_root.join(".buckconfig").exists() {
        return Ok(());
    }

    initialize_buckconfig(repo_root, prelude, git)?;
    if prelude {
        let toolchains = repo_root.join("toolchains");
        if !toolchains.exists() {
            fs_util::create_dir(&toolchains)?;
            initialize_toolchains_buck(&toolchains)?;
        }
    }
    if !repo_root.join("BUCK").exists() {
        initialize_root_buck(repo_root, prelude)?;
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use buck2_core::fs::fs_util;
    use buck2_core::fs::paths::abs_path::AbsPath;

    use crate::commands::init::initialize_buckconfig;
    use crate::commands::init::initialize_root_buck;
    use crate::commands::init::set_up_gitignore;
    use crate::commands::init::set_up_project;

    #[test]
    fn test_set_up_project_with_prelude_no_git() -> anyhow::Result<()> {
        let tempdir = tempfile::tempdir()?;
        let tempdir_path = tempdir.path();
        let tempdir_path = AbsPath::new(tempdir_path)?;
        fs_util::create_dir_all(tempdir_path)?;

        // no git, with prelude
        set_up_project(tempdir_path, false, true)?;
        assert!(tempdir_path.join(".buckconfig").exists());
        assert!(tempdir_path.join("toolchains").exists());
        assert!(tempdir_path.join("toolchains/BUCK").exists());
        assert!(tempdir_path.join("BUCK").exists());
        Ok(())
    }

    #[test]
    fn test_default_gitignore() -> anyhow::Result<()> {
        let tempdir = tempfile::tempdir()?;
        let tempdir_path = tempdir.path();
        let tempdir_path = AbsPath::new(tempdir_path)?;
        fs_util::create_dir_all(tempdir_path)?;

        // .gitignore does not exist yet
        set_up_gitignore(tempdir_path)?;
        let gitignore_path = tempdir_path.join(".gitignore");
        assert!(gitignore_path.exists());
        let actual = fs_util::read_to_string(&gitignore_path)?;
        let expected = "/buck-out\n";
        assert_eq!(actual, expected);

        // If an empty .buckconfig exists (this is the case we would hit after running `git init`), add `buck-out`
        fs_util::write(&gitignore_path, "")?;
        set_up_gitignore(tempdir_path)?;
        assert!(gitignore_path.exists());
        let actual = fs_util::read_to_string(&gitignore_path)?;
        assert_eq!(actual, expected);

        // If a non-empty.buckconfig exists, don't touch it
        fs_util::write(&gitignore_path, "foo\nbar\n")?;
        set_up_gitignore(tempdir_path)?;
        assert!(gitignore_path.exists());
        let actual = fs_util::read_to_string(&gitignore_path)?;
        let expected = "foo\nbar\n";
        assert_eq!(actual, expected);
        Ok(())
    }

    #[test]
    fn test_buckconfig_generation_with_prelude() -> anyhow::Result<()> {
        let tempdir = tempfile::tempdir()?;
        let tempdir_path = tempdir.path();
        let tempdir_path = AbsPath::new(tempdir_path)?;
        fs_util::create_dir_all(tempdir_path)?;

        let buckconfig_path = tempdir_path.join(".buckconfig");
        initialize_buckconfig(tempdir_path, true, true)?;
        let actual_buckconfig = fs_util::read_to_string(buckconfig_path)?;
        let expected_buckconfig = "[repositories]
root = .
prelude = prelude
toolchains = toolchains
none = none

[repository_aliases]
config = prelude
fbcode = none
fbsource = none
buck = none

[parser]
target_platform_detector_spec = target:root//...->prelude//platforms:default

[project]
ignore = .git
";
        assert_eq!(actual_buckconfig, expected_buckconfig);
        Ok(())
    }

    #[test]
    fn test_buckconfig_generation_without_prelude() -> anyhow::Result<()> {
        let tempdir = tempfile::tempdir()?;
        let tempdir_path = tempdir.path();
        let tempdir_path = AbsPath::new(tempdir_path)?;
        fs_util::create_dir_all(tempdir_path)?;

        let buckconfig_path = tempdir_path.join(".buckconfig");
        initialize_buckconfig(tempdir_path, false, false)?;
        let actual_buckconfig = fs_util::read_to_string(buckconfig_path)?;
        let expected_buckconfig = "[repositories]
root = .
prelude = prelude
";
        assert_eq!(actual_buckconfig, expected_buckconfig);

        // Test we have an empty prelude directory and prelude.bzl file
        assert!(tempdir_path.join("prelude/prelude.bzl").exists());
        Ok(())
    }

    #[test]
    fn test_buckfile_generation_with_prelude() -> anyhow::Result<()> {
        let tempdir = tempfile::tempdir()?;
        let tempdir_path = tempdir.path();
        let tempdir_path = AbsPath::new(tempdir_path)?;
        fs_util::create_dir_all(tempdir_path)?;

        let buck_path = tempdir_path.join("BUCK");
        initialize_root_buck(tempdir_path, true)?;
        let actual_buck = fs_util::read_to_string(buck_path)?;
        let expected_buck = "# A list of available rules and their signatures can be found here: https://buck2.build/docs/api/rules/

genrule(
    name = \"hello_world\",
    out = \"out.txt\",
    cmd = \"echo BUILT BY BUCK2> $OUT\",
)
";
        assert_eq!(actual_buck, expected_buck);
        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.
 */

use async_trait::async_trait;
use buck2_cli_proto::InstallRequest;
use buck2_client_ctx::client_ctx::ClientCommandContext;
use buck2_client_ctx::command_outcome::CommandOutcome;
use buck2_client_ctx::common::CommonBuildConfigurationOptions;
use buck2_client_ctx::common::CommonBuildOptions;
use buck2_client_ctx::common::CommonCommandOptions;
use buck2_client_ctx::common::CommonConsoleOptions;
use buck2_client_ctx::common::CommonDaemonCommandOptions;
use buck2_client_ctx::daemon::client::BuckdClientConnector;
use buck2_client_ctx::daemon::client::NoPartialResultHandler;
use buck2_client_ctx::exit_result::ExitResult;
use buck2_client_ctx::streaming::StreamingCommand;
use gazebo::prelude::*;

#[derive(Debug, clap::Parser)]
#[clap(name = "install", about = "Build and install an application")]
pub struct InstallCommand {
    #[clap(flatten)]
    common_opts: CommonCommandOptions,

    #[clap(flatten)]
    build_opts: CommonBuildOptions,

    #[clap(
        long,
        name = "installer-debug",
        help = "Prints installer output to stderr. It might break superconsole"
    )]
    installer_debug: bool,

    #[clap(flatten)]
    android_install_opts: AndroidInstallOptions,

    #[clap(name = "TARGET", help = "Target to build and install")]
    patterns: Vec<String>,

    #[clap(
        name = "INSTALL_ARGS",
        help = "Additional arguments passed to the install when running it",
        raw = true
    )]
    extra_run_args: Vec<String>,
}

/// Defines install options for Android that exist only for compatibility
/// with buck1, and which are all automatically forwarded to the installer.
#[derive(Debug, clap::Parser)]
struct AndroidInstallOptions {
    #[clap(
        short,
        long,
        help = "Run an Android activity. Here for compatibility with buck1 - it is automatically forwarded to the installer"
    )]
    run: bool,

    #[clap(
        short,
        long,
        help = "Use this option to use emulators only on Android. Here for compatibility with buck1 - it is automatically forwarded to the installer"
    )]
    emulator: bool,

    #[clap(
        short,
        long,
        help = "Use this option to use real devices only on Android. Here for compatibility with buck1 - it is automatically forwarded to the installer"
    )]
    device: bool,

    #[clap(
        short,
        long,
        alias = "udid",
        help = "Use Android device or emulator with specific serial or UDID number. Here for compatibility with buck1 - it is automatically forwarded to the installer"
    )]
    serial: Option<String>,

    #[clap(
        short = 'x',
        long,
        help = "Use all connected Android devices and/or emulators (multi-install mode). Here for compatibility with buck1 - it is automatically forwarded to the installer"
    )]
    all_devices: bool,

    #[clap(
        short,
        long,
        help = "Android activity to launch e.g. com.facebook/.LoginActivity. Implies -r. Here for compatibility with buck1 - it is automatically forwarded to the installer"
    )]
    activity: Option<String>,

    #[clap(
        short,
        long,
        help = "Android Intent URI to launch e.g. fb://profile. Implies -r. Here for compatibility with buck1 - it is automatically forwarded to the installer"
    )]
    intent_uri: Option<String>,

    #[clap(
        short,
        long,
        help = "Have the launched Android process wait for the debugger. Here for compatibility with buck1 - it is automatically forwarded to the installer"
    )]
    wait_for_debugger: bool,
}

#[async_trait]
impl StreamingCommand for InstallCommand {
    const COMMAND_NAME: &'static str = "install";
    async fn exec_impl(
        self,
        buckd: &mut BuckdClientConnector,
        matches: &clap::ArgMatches,
        ctx: &mut ClientCommandContext<'_>,
    ) -> ExitResult {
        let mut extra_run_args: Vec<String> = self.extra_run_args.clone();
        if self.android_install_opts.run {
            extra_run_args.push("-r".to_owned());
        }
        if self.android_install_opts.emulator {
            extra_run_args.push("-e".to_owned());
        }
        if self.android_install_opts.device {
            extra_run_args.push("-d".to_owned());
        }
        if let Some(serial) = self.android_install_opts.serial {
            extra_run_args.push("-s".to_owned());
            extra_run_args.push(serial);
        }
        if self.android_install_opts.all_devices {
            extra_run_args.push("-x".to_owned());
        }
        if let Some(activity) = self.android_install_opts.activity {
            extra_run_args.push("-a".to_owned());
            extra_run_args.push(activity);
        }
        if let Some(intent_uri) = self.android_install_opts.intent_uri {
            extra_run_args.push("-i".to_owned());
            extra_run_args.push(intent_uri);
        }
        if self.android_install_opts.wait_for_debugger {
            extra_run_args.push("-w".to_owned());
        }

        let context = ctx.client_context(
            &self.common_opts.config_opts,
            matches,
            ctx.sanitized_argv.argv.clone(),
        )?;
        let response = buckd
            .with_flushing()
            .install(
                InstallRequest {
                    context: Some(context),
                    target_patterns: self.patterns.map(|pat| buck2_data::TargetPattern {
                        value: pat.to_owned(),
                    }),
                    build_opts: Some(self.build_opts.to_proto()),
                    installer_run_args: extra_run_args,
                    installer_debug: self.installer_debug,
                },
                ctx.stdin()
                    .console_interaction_stream(&self.common_opts.console_opts),
                &mut NoPartialResultHandler,
            )
            .await;
        let console = self.common_opts.console_opts.final_console();

        match response {
            Ok(CommandOutcome::Success(_)) => {
                console.print_success("INSTALL SUCCEEDED")?;
                ExitResult::success()
            }
            Ok(CommandOutcome::Failure(_)) | Err(_) => {
                console.print_error("INSTALL FAILED")?;
                ExitResult::failure()
            }
        }
    }

    fn console_opts(&self) -> &CommonConsoleOptions {
        &self.common_opts.console_opts
    }

    fn event_log_opts(&self) -> &CommonDaemonCommandOptions {
        &self.common_opts.event_log_opts
    }

    fn common_opts(&self) -> &CommonBuildConfigurationOptions {
        &self.common_opts.config_opts
    }
}


/*
 * 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::time::Duration;

use anyhow::Context as _;
use buck2_client_ctx::argv::Argv;
use buck2_client_ctx::argv::SanitizedArgv;
use buck2_client_ctx::client_ctx::ClientCommandContext;
use buck2_client_ctx::daemon::client::connect::buckd_startup_timeout;
use buck2_client_ctx::daemon::client::connect::BuckdProcessInfo;
use buck2_client_ctx::daemon::client::BuckdLifecycleLock;
use buck2_client_ctx::exit_result::ExitResult;
use buck2_client_ctx::startup_deadline::StartupDeadline;

/// Kill the buck daemon.
///
/// Note there's also `buck2 killall` and `buck2 clean`.
///
/// `buck2 killall` kills all the buck2 processes on the machine.
///
/// `buck2 clean` kills the buck2 daemon and also deletes the buck2 state files.
#[derive(Debug, clap::Parser)]
pub struct KillCommand {}

impl KillCommand {
    pub fn exec(self, _matches: &clap::ArgMatches, ctx: ClientCommandContext<'_>) -> ExitResult {
        ctx.instant_command("kill", async move |ctx| {
            let daemon_dir = ctx.paths()?.daemon_dir()?;

            let lifecycle_lock = BuckdLifecycleLock::lock_with_timeout(
                daemon_dir.clone(),
                StartupDeadline::duration_from_now(Duration::from_secs(10))?,
            )
            .await
            .with_context(|| "Error locking buckd lifecycle.lock")?;

            kill_command_impl(&lifecycle_lock, "`buck kill` was invoked").await
        })
    }

    pub fn sanitize_argv(&self, argv: Argv) -> SanitizedArgv {
        argv.no_need_to_sanitize()
    }
}

pub(crate) async fn kill_command_impl(
    lifecycle_lock: &BuckdLifecycleLock,
    reason: &str,
) -> anyhow::Result<()> {
    let process = match BuckdProcessInfo::load(lifecycle_lock.daemon_dir()) {
        Ok(p) => p,
        Err(e) => {
            tracing::debug!("No BuckdProcessInfo: {:#}", e);
            buck2_client_ctx::eprintln!("no buckd server running")?;
            return Ok(());
        }
    };

    let buckd = tokio::time::timeout(buckd_startup_timeout()?, async {
        process.create_channel().await?.upgrade().await
    })
    .await;

    let response = match buckd {
        Ok(Ok(mut buckd)) => {
            buck2_client_ctx::eprintln!("killing buckd server")?;
            Some(buckd.kill(reason).await?)
        }
        Ok(Err(e)) => {
            // No time out: we just errored out. This is likely indicative that there is no
            // buckd (i.e. our connection got rejected), so let's check for this and then
            // provide some information.

            if e.is::<tonic::transport::Error>() {
                // OK, looks like the server
                tracing::debug!("Connect failed with a Tonic error: {:#}", e);
                buck2_client_ctx::eprintln!("no buckd server running")?;
            } else {
                buck2_client_ctx::eprintln!(
                    "unexpected error connecting to Buck2: {:#} \
                            (no buckd server running?)",
                    e
                )?;
            }

            None
        }
        Err(e) => {
            tracing::debug!("Connect timed out: {:#}", e);

            // If we timeout, then considering the generous timeout we give ourselves, then
            // that must mean we're not getting a reply back from Buck, but that we did
            // succeed in opening a connection to it (because if we didn't, we'd have
            // errored out).
            //
            // This means the socket is probably open. We can reasonably got and kill this
            // process if both the PID and the port exist.
            buck2_client_ctx::eprintln!("killing unresponsive buckd server")?;
            Some(process.hard_kill().await?)
        }
    };

    if let Some(response) = response {
        response.log()?;
    }

    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.
 */

use buck2_client_ctx::argv::Argv;
use buck2_client_ctx::argv::SanitizedArgv;
use buck2_client_ctx::client_ctx::ClientCommandContext;
use buck2_client_ctx::exit_result::ExitResult;
use buck2_wrapper_common::is_buck2::WhoIsAsking;

#[derive(Debug, clap::Parser)]
#[clap(about = "Kill all buck2 processes on the machine")]
pub struct KillallCommand {}

impl KillallCommand {
    pub fn exec(self, _matches: &clap::ArgMatches, ctx: ClientCommandContext<'_>) -> ExitResult {
        ctx.instant_command("killall", async move |_ctx| {
            buck2_wrapper_common::killall(WhoIsAsking::Buck2, |s| {
                let _ignored = buck2_client_ctx::eprintln!("{}", s);
            })
            .then_some(())
            .ok_or(anyhow::anyhow!("Killall command failed"))
        })
    }

    pub fn sanitize_argv(&self, argv: Argv) -> SanitizedArgv {
        argv.no_need_to_sanitize()
    }
}


/*
 * 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 async_trait::async_trait;
use buck2_cli_proto::LspRequest;
use buck2_client_ctx::client_ctx::ClientCommandContext;
use buck2_client_ctx::common::CommonBuildConfigurationOptions;
use buck2_client_ctx::common::CommonConsoleOptions;
use buck2_client_ctx::common::CommonDaemonCommandOptions;
use buck2_client_ctx::common::ConsoleType;
use buck2_client_ctx::daemon::client::BuckdClientConnector;
use buck2_client_ctx::events_ctx::PartialResultCtx;
use buck2_client_ctx::events_ctx::PartialResultHandler;
use buck2_client_ctx::exit_result::ExitResult;
use buck2_client_ctx::ide_support::ide_message_stream;
use buck2_client_ctx::stream_util::reborrow_stream_for_static;
use buck2_client_ctx::streaming::StreamingCommand;
use futures::stream::StreamExt;
use lsp_server::Message;
use once_cell::sync::Lazy;

#[derive(Debug, clap::Parser)]
#[clap(about = "Start an LSP server for starlark files")]
pub struct LspCommand {
    #[clap(flatten)]
    config_opts: CommonBuildConfigurationOptions,

    #[clap(flatten)]
    event_log_opts: CommonDaemonCommandOptions,
}

#[async_trait]
impl StreamingCommand for LspCommand {
    const COMMAND_NAME: &'static str = "lsp";

    async fn exec_impl(
        self,
        buckd: &mut BuckdClientConnector,
        matches: &clap::ArgMatches,
        ctx: &mut ClientCommandContext<'_>,
    ) -> ExitResult {
        let client_context =
            ctx.client_context(&self.config_opts, matches, ctx.sanitized_argv.argv.clone())?;
        let stream = ide_message_stream::<_, Message>(ctx.stdin()).filter_map(|m| async move {
            match m {
                Ok(lsp_json) => Some(LspRequest { lsp_json }),
                Err(e) => {
                    let _ignored =
                        buck2_client_ctx::eprintln!("Could not read message from stdin: `{}`", e);
                    None
                }
            }
        });

        let mut partial_result_handler = LspPartialResultHandler;
        reborrow_stream_for_static(
            stream,
            |stream| async move {
                buckd
                    .with_flushing()
                    .lsp(client_context, stream, &mut partial_result_handler)
                    .await
            },
            // The LSP server side does not handle hangups. So, until it does... we never hang up:
            // Err(Status { code: FailedPrecondition, message: "received a message that is not a `StreamingRequest`", source: None })
            || None,
        )
        .await??;

        ExitResult::success()
    }

    fn console_opts(&self) -> &CommonConsoleOptions {
        // This should only be communicated with by an IDE, so disable anything other
        // than the simple console
        static SIMPLE_CONSOLE: Lazy<CommonConsoleOptions> = Lazy::new(|| CommonConsoleOptions {
            console_type: ConsoleType::Simple,
            ui: vec![],
            no_interactive_console: true,
        });
        &SIMPLE_CONSOLE
    }

    fn event_log_opts(&self) -> &CommonDaemonCommandOptions {
        &self.event_log_opts
    }

    fn common_opts(&self) -> &CommonBuildConfigurationOptions {
        &self.config_opts
    }

    fn should_show_waiting_message(&self) -> bool {
        // If we're running the LSP, do not show "Waiting for daemon..." if we do not get any spans.
        false
    }
}

struct LspPartialResultHandler;

#[async_trait]
impl PartialResultHandler for LspPartialResultHandler {
    type PartialResult = buck2_cli_proto::LspMessage;

    async fn handle_partial_result(
        &mut self,
        mut ctx: PartialResultCtx<'_, '_>,
        partial_res: Self::PartialResult,
    ) -> anyhow::Result<()> {
        let lsp_message: lsp_server::Message = serde_json::from_str(&partial_res.lsp_json)?;
        let mut buffer = Vec::new();
        lsp_message.write(&mut buffer)?;
        ctx.stdout(&buffer).await
    }
}


/*
 * 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 bxl;
pub mod clean;
pub mod clean_stale;
pub mod ctargets;
pub mod debug;
pub mod init;
pub mod install;
pub mod kill;
pub mod killall;
pub mod log;
pub mod lsp;
pub mod profile;
pub mod query;
pub mod rage;
pub mod root;
pub mod run;
pub mod server;
pub mod status;
pub mod subscribe;
pub mod targets;
pub mod test;