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

//! bxl additional artifact types

use std::fmt::Display;
use std::fmt::Formatter;
use std::hash::Hash;
use std::hash::Hasher;

use allocative::Allocative;
use anyhow::Context as _;
use buck2_build_api::artifact_groups::ArtifactGroup;
use buck2_build_api::artifact_groups::ResolvedArtifactGroup;
use buck2_build_api::deferred::calculation::DeferredCalculation;
use buck2_build_api::interpreter::rule_defs::artifact::starlark_artifact_like::StarlarkArtifactLike;
use buck2_build_api::interpreter::rule_defs::artifact::StarlarkArtifact;
use buck2_build_api::interpreter::rule_defs::artifact::StarlarkDeclaredArtifact;
use buck2_execute::path::artifact_path::ArtifactPath;
use dice::DiceComputations;
use dupe::Dupe;
use dupe::IterDupedExt;
use serde::Serialize;
use serde::Serializer;
use starlark::any::ProvidesStaticType;
use starlark::collections::SmallSet;
use starlark::collections::StarlarkHasher;
use starlark::docs::StarlarkDocs;
use starlark::environment::Methods;
use starlark::environment::MethodsBuilder;
use starlark::environment::MethodsStatic;
use starlark::starlark_module;
use starlark::values::starlark_value;
use starlark::values::AllocValue;
use starlark::values::Heap;
use starlark::values::StarlarkValue;
use starlark::values::Trace;
use starlark::values::UnpackValue;
use starlark::values::Value;
use starlark::values::ValueLike;
use starlark::values::ValueOf;

#[derive(Clone, Debug, Trace, ProvidesStaticType, StarlarkDocs, Allocative)]
#[repr(C)]
#[starlark_docs(directory = "bxl")]
pub enum EnsuredArtifact {
    Artifact {
        artifact: StarlarkArtifact,
        abs: bool,
    },
    DeclaredArtifact {
        artifact: StarlarkDeclaredArtifact,
        abs: bool,
    },
}

#[derive(Clone, Debug, Trace, ProvidesStaticType, StarlarkDocs, Allocative)]
#[repr(C)]
pub struct EnsuredArtifactGroupInner {
    pub(crate) ags: Vec<ArtifactGroup>,
}

pub async fn visit_artifact_path_without_associated_deduped<'v>(
    ags: &'v [ArtifactGroup],
    abs: bool,
    mut visitor: impl FnMut(ArtifactPath, bool) -> anyhow::Result<()>,
    ctx: &'v DiceComputations,
) -> anyhow::Result<()> {
    // If there's a case where a tset projection returns a projection, we want to make sure
    // we are not reprocessing the nested projection over again. Since we are using

    // `get_projection_sub_inputs()` to iterate through the tset, this is also needed to make
    // sure we aren't re-processing tests.
    let mut visited = SmallSet::new();

    let mut todo = Vec::new();
    todo.extend(ags.iter().duped());

    while let Some(ag) = todo.pop() {
        if !visited.insert(ag.dupe()) {
            continue;
        }
        match ag.resolved()? {
            ResolvedArtifactGroup::Artifact(a) => {
                visitor(a.get_path(), abs)?;
            }
            ResolvedArtifactGroup::TransitiveSetProjection(t) => {
                let set = ctx
                    .compute_deferred_data(&t.key)
                    .await
                    .context("Failed to compute deferred for transitive set projection key")?;

                let set = set.as_transitive_set();

                todo.extend(set.get_projection_sub_inputs(t.projection)?);
            }
        }
    }

    Ok(())
}

#[derive(Clone, Debug, Trace, ProvidesStaticType, StarlarkDocs, Allocative)]
#[repr(C)]
#[starlark_docs(directory = "bxl")]
pub struct EnsuredArtifactGroup<'v> {
    // Have `EnsuredArtifactGroup` be a wrapper around `EnsuredArtifactGroupInner` as a Starlark `Value`
    // so that we don't have to copy all of its artifact groups whenever we call `abs_path()` or `rel_path()`,
    // and we can instead just copy the `Value` (which is a pointer)
    pub(crate) inner: Value<'v>,
    pub(crate) abs: bool,
}

impl<'v> EnsuredArtifactGroup<'v> {
    pub fn new(ags: Vec<ArtifactGroup>, abs: bool, heap: &'v Heap) -> Self {
        EnsuredArtifactGroup {
            inner: heap.alloc(EnsuredArtifactGroupInner { ags }),

            abs,
        }
    }

    pub fn inner(&self) -> &Vec<ArtifactGroup> {
        &<&EnsuredArtifactGroupInner>::unpack_value(self.inner)
            .unwrap()
            .ags
    }

    pub async fn visit_artifact_path_without_associated_deduped(
        &self,
        visitor: impl FnMut(ArtifactPath, bool) -> anyhow::Result<()>,
        ctx: &'v DiceComputations,
    ) -> anyhow::Result<()> {
        visit_artifact_path_without_associated_deduped(self.inner(), self.abs, visitor, ctx).await
    }
}

#[starlark_value(type = "ensured_artifact_group", StarlarkTypeRepr, UnpackValue)]
impl<'v> StarlarkValue<'v> for EnsuredArtifactGroup<'v>
where
    Self: ProvidesStaticType<'v>,
{
    fn get_methods() -> Option<&'static Methods> {
        static RES: MethodsStatic = MethodsStatic::new();
        RES.methods(artifact_group_methods)
    }

    fn write_hash(&self, hasher: &mut StarlarkHasher) -> anyhow::Result<()> {
        let _hash = self.inner.write_hash(hasher);
        self.abs.hash(hasher);
        Ok(())
    }
}

#[starlark_value(type = "ensured_artifact_group_inner", StarlarkTypeRepr, UnpackValue)]
impl<'v> StarlarkValue<'v> for EnsuredArtifactGroupInner
where
    Self: ProvidesStaticType<'v>,
{
    fn write_hash(&self, hasher: &mut StarlarkHasher) -> anyhow::Result<()> {
        self.ags.hash(hasher);
        Ok(())
    }
}

impl Hash for EnsuredArtifact {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.as_artifact().fingerprint().hash(state);
    }
}

impl PartialEq for EnsuredArtifact {
    fn eq(&self, other: &Self) -> bool {
        self.abs() == other.abs()
            && self.as_artifact().fingerprint() == other.as_artifact().fingerprint()
    }
}

impl Eq for EnsuredArtifact {}

impl EnsuredArtifact {
    pub fn new<'v>(artifact: Value<'v>) -> anyhow::Result<Self> {
        let artifact = artifact
            .downcast_ref::<StarlarkArtifact>()
            .map(|o| EnsuredArtifact::Artifact {
                artifact: o.dupe(),
                abs: false,
            })
            .or_else(|| {
                artifact
                    .downcast_ref::<StarlarkDeclaredArtifact>()
                    .map(|o| EnsuredArtifact::DeclaredArtifact {
                        artifact: o.dupe(),
                        abs: false,
                    })
            });

        match artifact {
            Some(artifact) => Ok(artifact),
            None => Err(anyhow::anyhow!("must be artifact like")),
        }
    }

    pub fn as_artifact(&self) -> &dyn StarlarkArtifactLike {
        match self {
            EnsuredArtifact::Artifact { artifact, .. } => artifact as &dyn StarlarkArtifactLike,
            EnsuredArtifact::DeclaredArtifact { artifact, .. } => {
                artifact as &dyn StarlarkArtifactLike
            }
        }
    }

    pub fn abs(&self) -> bool {
        match self {
            EnsuredArtifact::Artifact { abs, .. } => *abs,
            EnsuredArtifact::DeclaredArtifact { abs, .. } => *abs,
        }
    }

    pub(crate) fn get_artifact_path(&self) -> ArtifactPath {
        match self {
            EnsuredArtifact::Artifact { artifact, .. } => artifact.get_artifact_path(),
            EnsuredArtifact::DeclaredArtifact { artifact, .. } => artifact.get_artifact_path(),
        }
    }
}

impl Display for EnsuredArtifact {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        // this can't be converted to string in starlark and used as path arbitrarily.
        // this can only be printed as path via `ctx.output.print()`. Anywhere that wants to use
        // it as a path to commands should use the normal starlark artifacts to construct their
        // command
        write!(f, "<ensured {}>", self.as_artifact())
    }
}

impl<'v> Display for EnsuredArtifactGroup<'v> {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        // this can't be converted to string in starlark and used as path arbitrarily.
        // this can only be printed as path via `ctx.output.print()`. Anywhere that wants to use
        // it as a path to commands should use the normal starlark artifacts to construct their
        // command
        write!(f, "<ensured group>")
    }
}

impl Display for EnsuredArtifactGroupInner {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        // This is not exposed to users.
        write!(f, "<ensured group inner>")
    }
}

impl<'v> AllocValue<'v> for EnsuredArtifact {
    fn alloc_value(self, heap: &'v Heap) -> Value<'v> {
        heap.alloc_complex_no_freeze(self)
    }
}

impl<'v> AllocValue<'v> for EnsuredArtifactGroup<'v> {
    fn alloc_value(self, heap: &'v Heap) -> Value<'v> {
        heap.alloc_complex_no_freeze(self)
    }
}

impl<'v> AllocValue<'v> for EnsuredArtifactGroupInner {
    fn alloc_value(self, heap: &'v Heap) -> Value<'v> {
        heap.alloc_complex_no_freeze(self)
    }
}

impl Serialize for EnsuredArtifact {
    fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        s.collect_str(self)
    }
}

impl<'v> Serialize for EnsuredArtifactGroup<'v> {
    fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        s.collect_str(self)
    }
}

impl Serialize for EnsuredArtifactGroupInner {
    fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        s.collect_str(self)
    }
}

#[starlark_value(type = "ensured_artifact", StarlarkTypeRepr, UnpackValue)]
impl<'v> StarlarkValue<'v> for EnsuredArtifact
where
    Self: ProvidesStaticType<'v>,
{
    fn get_methods() -> Option<&'static Methods> {
        static RES: MethodsStatic = MethodsStatic::new();
        RES.methods(artifact_methods)
    }

    fn write_hash(&self, hasher: &mut StarlarkHasher) -> anyhow::Result<()> {
        let _artifact_write_hash = self.as_artifact().write_hash(hasher);
        self.abs().hash(hasher);
        Ok(())
    }
}

/// An artifact that will be materialized to buck-out at the end of the bxl invocation.
/// These artifacts can be printed to bxl's results. Doing so will print the path of the artifact
/// rather than the standard representation.
///
/// Ensured artifacts are serializable and hashable.
#[starlark_module]
fn artifact_methods(builder: &mut MethodsBuilder) {
    /// Converts this artifact to be printed by its absolute path. Note that this will only print out the
    /// absolute path via `ctx.output.print()`. Starlark's `print()` will print out the display info for an
    /// ensured artifact.
    ///
    /// Sample usage:
    /// ```text
    /// def _impl_abs_path(ctx):
    ///     actions = ctx.bxl_actions().actions
    ///     output = actions.write("my_output", "my_content")
    ///     ensured = ctx.output.ensure(output) # currently defaults to creating an EnsuredArtifact with a relative path
    ///
    ///     ensured_with_abs_path = ensured.abs_path() # create a new EnsuredArtifact with absolute path to reuse
    ///     print(ensured_with_abs_path) # should return something like <ensured artifact ... >
    ///     ctx.output.print(ensured_with_abs_path) # should return the absolute path of the artifact
    /// ```
    fn abs_path<'v>(
        this: ValueOf<'v, &'v EnsuredArtifact>,
        heap: &'v Heap,
    ) -> anyhow::Result<Value<'v>> {
        if this.typed.abs() {
            Ok(this.value)
        } else {
            let artifact = match this.typed {
                EnsuredArtifact::Artifact { artifact, .. } => EnsuredArtifact::Artifact {
                    artifact: artifact.dupe(),
                    abs: true,
                },
                EnsuredArtifact::DeclaredArtifact { artifact, .. } => {
                    EnsuredArtifact::DeclaredArtifact {
                        artifact: artifact.dupe(),
                        abs: true,
                    }
                }
            };

            Ok(heap.alloc(artifact))
        }
    }

    /// Converts this artifact to be printed by its path relative to the project root.
    /// Note that this will only print out the relative path via `ctx.output.print()`.
    /// Starlark's `print()` will print out the display info for an ensured artifact.
    ///
    /// Sample usage:
    /// ```text
    /// def _impl_rel_path(ctx):
    ///     actions = ctx.bxl_actions().actions
    ///     output = actions.write("my_output", "my_content")
    ///     ensured = ctx.output.ensure(output) # currently defaults to creating an EnsuredArtifact with a relative path
    ///
    ///     ensured_with_rel_path = ensured.rel_path() # create a new EnsuredArtifact with relative path to reuse
    ///     print(ensured_with_rel_path) # should return something like <ensured artifact ... >
    ///     ctx.output.print(ensured_with_rel_path) # should return the relative path of the artifact
    /// ```
    fn rel_path<'v>(
        this: ValueOf<'v, &'v EnsuredArtifact>,
        heap: &'v Heap,
    ) -> anyhow::Result<Value<'v>> {
        if !this.typed.abs() {
            Ok(this.value)
        } else {
            let artifact = match this.typed {
                EnsuredArtifact::Artifact { artifact, .. } => EnsuredArtifact::Artifact {
                    artifact: artifact.dupe(),
                    abs: false,
                },
                EnsuredArtifact::DeclaredArtifact { artifact, .. } => {
                    EnsuredArtifact::DeclaredArtifact {
                        artifact: artifact.dupe(),
                        abs: false,
                    }
                }
            };

            Ok(heap.alloc(artifact))
        }
    }
}

/// An artifact group that will be materialized to buck-out at the end of the bxl invocation.
/// These artifacts can be printed to bxl's results. Doing so will print the path of each artifact in
/// the group rather than the standard representation.
///
/// Ensured artifact groups are serializable and hashable.
#[starlark_module]
fn artifact_group_methods(builder: &mut MethodsBuilder) {
    /// Converts each artifact in this artifact group to be printed by its absolute path. Note that this will only print out the
    /// absolute path via `ctx.output.print()`. Starlark's `print()` will print out the display info for an
    /// ensured artifact group.
    ///
    /// Sample usage:
    /// ```text
    /// def _impl_abs_path(ctx):
    ///     # some target with RunInfo outputs
    ///     result = ctx.analysis("root//bin/kind:target_with_outputs")
    ///     ensured = ctx.output.ensure_multiple(result.providers()[RunInfo]) # currently defaults to creating an EnsuredArtifactGroup with a relative path
    ///
    ///     ensured_with_abs_path = ensured.abs_path() # create a new EnsuredArtifactGroup with absolute path to reuse
    ///     print(ensured_with_abs_path) # should return something like <ensured group ... >
    ///     ctx.output.print(ensured_with_abs_path) # should return the absolute path of the artifact
    /// ```
    fn abs_path<'v>(
        this: ValueOf<'v, &'v EnsuredArtifactGroup<'v>>,
        heap: &'v Heap,
    ) -> anyhow::Result<Value<'v>> {
        if this.typed.abs {
            Ok(this.value)
        } else {
            let artifact = EnsuredArtifactGroup {
                inner: this.typed.inner,
                abs: true,
            };

            Ok(heap.alloc(artifact))
        }
    }

    /// Converts each artifact in this artifact group to be printed by its path relative to the project root.
    /// Note that this will only print out the relative path via `ctx.output.print()`.
    /// Starlark's `print()` will print out the display info for an ensured artifact group.
    ///
    /// Sample usage:
    /// ```text
    /// def _impl_rel_path(ctx):
    ///     # some target with RunInfo outputs
    ///     result = ctx.analysis("root//bin/kind:target_with_outputs")
    ///     ensured = ctx.output.ensure_multiple(result.providers()[RunInfo]) # currently defaults to creating an EnsuredArtifactGroup with a relative path
    ///
    ///     ensured_with_rel_path = ensured.rel_path() # create a new EnsuredArtifact with relative path to reuse
    ///     print(ensured_with_rel_path) # should return something like <ensured group ... >
    ///     ctx.output.print(ensured_with_rel_path) # should return the relative path of the artifact
    /// ```
    fn rel_path<'v>(
        this: ValueOf<'v, &'v EnsuredArtifactGroup<'v>>,
        heap: &'v Heap,
    ) -> anyhow::Result<Value<'v>> {
        if !this.typed.abs {
            Ok(this.value)
        } else {
            let artifact = EnsuredArtifactGroup {
                inner: this.typed.inner,
                abs: false,
            };

            Ok(heap.alloc(artifact))
        }
    }
}


/*
 * 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 allocative::Allocative;
use buck2_build_api::audit_output::audit_output;
use buck2_build_api::audit_output::AuditOutputResult;
use buck2_core::cells::CellResolver;
use buck2_core::fs::project_rel_path::ProjectRelativePathBuf;
use buck2_core::target::label::TargetLabel;
use buck2_interpreter::types::target_label::StarlarkTargetLabel;
use derivative::Derivative;
use derive_more::Display;
use starlark::any::ProvidesStaticType;
use starlark::environment::Methods;
use starlark::environment::MethodsBuilder;
use starlark::environment::MethodsStatic;
use starlark::starlark_module;
use starlark::values::none::NoneType;
use starlark::values::starlark_value;
use starlark::values::AllocValue;
use starlark::values::Heap;
use starlark::values::NoSerialize;
use starlark::values::StarlarkValue;
use starlark::values::Trace;
use starlark::values::Value;
use starlark::StarlarkDocs;

use crate::bxl::starlark_defs::context::BxlContext;
use crate::bxl::starlark_defs::nodes::action::StarlarkAction;
use crate::bxl::value_as_starlark_target_label::ValueAsStarlarkTargetLabel;

#[derive(
    ProvidesStaticType,
    Derivative,
    Display,
    Trace,
    NoSerialize,
    Allocative,
    StarlarkDocs
)]
#[starlark_docs(directory = "bxl")]
#[derivative(Debug)]
#[display(fmt = "{:?}", self)]
#[allocative(skip)]
pub struct StarlarkAuditCtx<'v> {
    #[trace(unsafe_ignore)]
    #[derivative(Debug = "ignore")]
    ctx: &'v BxlContext<'v>,
    #[trace(unsafe_ignore)]
    #[derivative(Debug = "ignore")]
    working_dir: ProjectRelativePathBuf,
    #[trace(unsafe_ignore)]
    #[derivative(Debug = "ignore")]
    cell_resolver: CellResolver,
    global_target_platform: Option<TargetLabel>,
}

#[starlark_value(type = "audit_ctx", StarlarkTypeRepr, UnpackValue)]
impl<'v> StarlarkValue<'v> for StarlarkAuditCtx<'v> {
    fn get_methods() -> Option<&'static Methods> {
        static RES: MethodsStatic = MethodsStatic::new();
        RES.methods(register_audit)
    }
}

impl<'v> AllocValue<'v> for StarlarkAuditCtx<'v> {
    fn alloc_value(self, heap: &'v Heap) -> Value<'v> {
        heap.alloc_complex_no_freeze(self)
    }
}

impl<'v> StarlarkAuditCtx<'v> {
    pub fn new(
        ctx: &'v BxlContext<'v>,
        working_dir: ProjectRelativePathBuf,
        cell_resolver: CellResolver,
        global_target_platform: Option<TargetLabel>,
    ) -> anyhow::Result<Self> {
        Ok(Self {
            ctx,
            working_dir,
            cell_resolver,
            global_target_platform,
        })
    }
}

/// The context for performing `audit` operations in bxl. The functions offered on this ctx are
/// the same behaviour as the audit functions available within audit command.
#[starlark_module]
fn register_audit(builder: &mut MethodsBuilder) {
    /// Returns either:
    ///  - The `StarlarkAction` which created the buck-out path, if exists.
    ///  - The `StarlarkTargetLabel` (unconfigured target label) constructed from the buck-out path, if the configuration hashes do not match.
    ///  - None, if the configuration hash of the buck-out path matches the one passed into this function, or the default target
    /// configuration, but no action could be found that generated the buck-out path.
    ///
    /// Takes in an optional target platform, otherwise will use the default target platform.
    ///
    /// Sample usage:
    /// ```text
    /// def _impl_audit_output(ctx):
    ///     target_platform = "foo"
    ///     result = ctx.audit().output("buck-out/v2/gen/fbcode/some_cfg_hash/path/to/__target__/artifact", target_platform)
    ///     ctx.output.print(result)
    /// ```
    fn output<'v>(
        this: &StarlarkAuditCtx<'v>,
        output_path: &'v str,
        #[starlark(default = NoneType)] target_platform: Value<'v>,
        heap: &'v Heap,
    ) -> anyhow::Result<Option<Value<'v>>> {
        let target_platform = target_platform.parse_target_platforms(
            &this.ctx.target_alias_resolver,
            &this.ctx.cell_resolver,
            this.ctx.cell_name,
            &this.global_target_platform,
        )?;

        this.ctx.async_ctx.via_dice(|ctx| async move {
            Ok::<_, anyhow::Error>(
                audit_output(
                    output_path,
                    &this.working_dir,
                    &this.cell_resolver,
                    ctx,
                    target_platform,
                )
                .await?
                .map(|result| match result {
                    AuditOutputResult::Match(action) => heap.alloc(StarlarkAction(action.action())),
                    AuditOutputResult::MaybeRelevant(label) => {
                        heap.alloc(StarlarkTargetLabel::new(label))
                    }
                }),
            )
        })
    }
}


/*
 * 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 allocative::Allocative;
use buck2_build_api::bxl::build_result::BxlBuildResult;
use starlark::environment::Methods;
use starlark::environment::MethodsBuilder;
use starlark::environment::MethodsStatic;
use starlark::starlark_module;
use starlark::starlark_simple_value;
use starlark::values::starlark_value;
use starlark::values::NoSerialize;
use starlark::values::ProvidesStaticType;
use starlark::values::StarlarkValue;
use starlark::values::Value;
use starlark::values::ValueLike;
use starlark::StarlarkDocs;

use crate::bxl::starlark_defs::context::build::StarlarkFailedArtifactIterable;
use crate::bxl::starlark_defs::context::build::StarlarkFailedArtifactIterableGen;
use crate::bxl::starlark_defs::context::build::StarlarkProvidersArtifactIterable;
use crate::bxl::starlark_defs::context::build::StarlarkProvidersArtifactIterableGen;

/// Starlark object for `StarlarkBuildResult` (which is not Starlark value).
#[derive(
    Clone,
    Debug,
    derive_more::Display,
    ProvidesStaticType,
    NoSerialize,
    StarlarkDocs,
    Allocative
)]
#[starlark_docs(directory = "bxl")]
pub(crate) struct StarlarkBxlBuildResult(pub(crate) BxlBuildResult);

/// The result of building in bxl.
#[starlark_module]
fn starlark_build_result_methods(builder: &mut MethodsBuilder) {
    /// Returns an optional iterable of artifacts that was successfully built.
    ///
    /// Sample usage:
    /// ```text
    /// def _impl(ctx):
    ///     outputs = {}
    ///     for target, value in ctx.build(ctx.cli_args.target).items():
    ///         ctx.output.print(value.artifacts())
    /// ```
    fn artifacts<'v>(
        this: Value<'v>,
    ) -> anyhow::Result<Option<StarlarkProvidersArtifactIterable<'v>>> {
        match &this.downcast_ref::<StarlarkBxlBuildResult>().unwrap().0 {
            BxlBuildResult::None => Ok(None),
            BxlBuildResult::Built { .. } => Ok(Some(StarlarkProvidersArtifactIterableGen(this))),
        }
    }

    /// Returns an optional of iterable of artifacts that failed to be built.
    ///
    /// Sample usage:
    /// ```text
    /// def _impl(ctx):
    ///     outputs = {}
    ///     for target, value in ctx.build(ctx.cli_args.target).items():
    ///         ctx.output.print(value.failures())
    /// ```
    fn failures<'v>(this: Value<'v>) -> anyhow::Result<Option<StarlarkFailedArtifactIterable<'v>>> {
        match &this.downcast_ref::<StarlarkBxlBuildResult>().unwrap().0 {
            BxlBuildResult::None => Ok(None),
            BxlBuildResult::Built { .. } => Ok(Some(StarlarkFailedArtifactIterableGen(this))),
        }
    }
}

starlark_simple_value!(StarlarkBxlBuildResult);

#[starlark_value(type = "bxl_build_result")]
impl<'v> StarlarkValue<'v> for StarlarkBxlBuildResult {
    fn get_methods() -> Option<&'static Methods> {
        static RES: MethodsStatic = MethodsStatic::new();
        RES.methods(starlark_build_result_methods)
    }
}


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

//! Command line arguments definition for bxl functions

use std::collections::HashSet;
use std::fmt::Display;
use std::fmt::Formatter;
use std::sync::Arc;

use allocative::Allocative;
use anyhow::Context as _;
use buck2_common::result::SharedResult;
use buck2_core::pattern::lex_target_pattern;
use buck2_core::pattern::pattern_type::ProvidersPatternExtra;
use buck2_core::pattern::pattern_type::TargetPatternExtra;
use buck2_core::pattern::ParsedPattern;
use buck2_core::provider::label::ProvidersLabel;
use buck2_core::target::label::TargetLabel;
use buck2_interpreter::types::label::StarlarkProvidersLabel;
use buck2_interpreter::types::target_label::StarlarkTargetLabel;
use buck2_node::load_patterns::load_patterns;
use buck2_node::load_patterns::MissingTargetBehavior;
use derive_more::Display;
use dupe::Dupe;
use futures::future::BoxFuture;
use futures::FutureExt;
use gazebo::variants::VariantName;
use itertools::Itertools;
use starlark::any::ProvidesStaticType;
use starlark::environment::GlobalsBuilder;
use starlark::starlark_module;
use starlark::starlark_simple_value;
use starlark::values::float::StarlarkFloat;
use starlark::values::list::AllocList;
use starlark::values::list::ListRef;
use starlark::values::none::NoneType;
use starlark::values::starlark_value;
use starlark::values::Heap;
use starlark::values::NoSerialize;
use starlark::values::StarlarkValue;
use starlark::values::UnpackValue;
use starlark::values::Value;
use starlark::values::ValueError;
use starlark::values::ValueLike;
use thiserror::Error;

use crate::bxl::eval::CliResolutionCtx;

/// Defines the cli args for the bxl function
#[derive(Clone, Debug, Display, ProvidesStaticType, NoSerialize, Allocative)]
#[display(fmt = "{:?}", self)]
pub(crate) struct CliArgs {
    /// The default value. If None, the value is not optional and must be provided by the user
    pub(crate) default: Option<Arc<CliArgValue>>,
    /// Documentation for what the attribute actually means
    doc: String,
    /// The coercer to take this parameter's value from Starlark value -> an
    /// internal representation
    coercer: CliArgType,
    /// The shorthand representation of the CLI arg
    #[allocative(skip)]
    pub(crate) short: Option<char>,
}

starlark_simple_value!(CliArgs);

#[starlark_value(type = "bxl_cli_args")]
impl<'v> StarlarkValue<'v> for CliArgs {}

impl CliArgs {
    /// Helper to create an attribute from attrs.foo functions
    fn new<'v>(
        default: Option<Value<'v>>,
        doc: &str,
        coercer: CliArgType,
        short: Option<Value<'v>>,
    ) -> anyhow::Result<Self> {
        let default = match default {
            None => None,
            Some(x) => Some(Arc::new(
                coercer
                    .coerce_value(x)
                    .map_err(|_| ValueError::IncorrectParameterType)?,
            )),
        };

        let short = match short {
            None => None,
            Some(s) => match s.unpack_str() {
                Some(s) => {
                    if s.len() != 1 {
                        return Err(ValueError::IncorrectParameterType.into());
                    }
                    Some(s.chars().next().unwrap())
                }
                None => return Err(ValueError::IncorrectParameterType.into()),
            },
        };

        Ok(Self {
            default,
            doc: doc.to_owned(),
            coercer,
            short,
        })
    }

    pub fn to_clap<'a>(&'a self, arg: clap::Arg<'a>) -> clap::Arg<'a> {
        let mut arg = self.coercer.to_clap(arg.help(self.doc.as_str()));
        if let Some(short) = self.short {
            arg = arg.short(short);
        }

        if self.default.is_some() {
            arg = arg.required(false);
        }

        arg
    }

    pub async fn parse_clap<'a>(
        &self,
        clap: ArgAccessor<'a>,
        ctx: &CliResolutionCtx<'a>,
    ) -> anyhow::Result<CliArgValue> {
        Ok(match self.coercer.parse_clap(clap, ctx).await? {
            None => (**self.default.as_ref().ok_or(CliArgError::MissingCliArg)?).clone(),
            Some(v) => v,
        })
    }
}

#[derive(
    Debug,
    derive_more::Display,
    PartialEq,
    Eq,
    Clone,
    Hash,
    Ord,
    PartialOrd,
    Allocative
)]
pub(crate) enum CliArgValue {
    Bool(bool),
    Int(i32),
    // store this as a string here for eq, hash since generally this comes from cmdline.
    // Note that this means `3.0` and `3.00` would not be equal. The string should already have
    // been verified to be a f64.
    Float(String),
    String(String),
    // Type of list elements is used to verify that concatenation is valid.
    // That only can be checked after configuration took place,
    // so pass the type info together with values to be used later.
    #[display(fmt = "{}", "_0.iter().map(|v| v.to_string()).join(\",\")")]
    List(Vec<CliArgValue>),
    None,
    TargetLabel(TargetLabel),
    ProvidersLabel(ProvidersLabel),
}

impl CliArgValue {
    pub(crate) fn as_starlark<'v>(&self, heap: &'v Heap) -> Value<'v> {
        match self {
            CliArgValue::Bool(b) => Value::new_bool(*b),
            CliArgValue::Int(i) => heap.alloc(*i),
            CliArgValue::Float(f) => heap.alloc(f.parse::<f64>().expect("already verified")),
            CliArgValue::String(s) => heap.alloc(s),
            CliArgValue::List(l) => heap.alloc(AllocList(l.iter().map(|v| v.as_starlark(heap)))),
            CliArgValue::None => Value::new_none(),
            CliArgValue::TargetLabel(t) => heap.alloc(StarlarkTargetLabel::new(t.dupe())),
            CliArgValue::ProvidersLabel(p) => heap.alloc(StarlarkProvidersLabel::new(p.clone())),
        }
    }
}

#[derive(Debug, VariantName, Clone, Dupe, Allocative)]
#[cfg_attr(feature = "gazebo_lint", allow(gazebo_lint_arc_on_dupe))] // recursive type
pub(crate) enum CliArgType {
    Bool,
    Int,
    Float,
    String,
    Enumeration(Arc<HashSet<String>>),
    List(Arc<CliArgType>),
    Option(Arc<CliArgType>),
    TargetLabel,
    TargetExpr,
    SubTarget,
    SubTargetExpr,
}

impl Display for CliArgType {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            CliArgType::Enumeration(t) => {
                write!(
                    f,
                    "{}(variants={})",
                    self.variant_name(),
                    t.iter().join(",")
                )
            }
            CliArgType::List(t) => {
                write!(f, "{}({})", self.variant_name(), t)
            }
            CliArgType::Option(t) => {
                write!(f, "{}({})", self.variant_name(), t)
            }
            x => write!(f, "{}", x.variant_name()),
        }
    }
}

impl CliArgType {
    fn bool() -> Self {
        CliArgType::Bool
    }

    fn int() -> Self {
        CliArgType::Int
    }

    fn float() -> Self {
        CliArgType::Float
    }

    fn string() -> Self {
        CliArgType::String
    }

    fn list(t: CliArgType) -> Self {
        CliArgType::List(Arc::new(t))
    }

    fn option(t: CliArgType) -> Self {
        CliArgType::Option(Arc::new(t))
    }

    fn target_label() -> Self {
        CliArgType::TargetLabel
    }

    fn target_expr() -> Self {
        CliArgType::TargetExpr
    }

    fn sub_target() -> Self {
        CliArgType::SubTarget
    }

    fn sub_target_expr() -> Self {
        CliArgType::SubTargetExpr
    }

    fn enumeration(vs: HashSet<String>) -> Self {
        CliArgType::Enumeration(Arc::new(vs))
    }
}

#[derive(Debug, Error)]
pub(crate) enum CliArgError {
    #[error("Expected default value of type `{}`, but got `{}`", _0, _1)]
    DefaultValueTypeError(CliArgType, String),
    #[error("Missing cli arg from command line that isn't optional nor has any default values")]
    MissingCliArg,
    #[error("`{0}` is not a {1} label.")]
    NotALabel(String, &'static str),
    #[error("Defaults are not allowed for cli arg type `{0}`.")]
    NoDefaultsAllowed(CliArgType),
    #[error("Duplicate short args are not allowed: `{0}` was already used")]
    DuplicateShort(char),
    #[error("An argument can be kebab-case OR snake-case, not both: `{0}`")]
    DefinedBothKebabAndSnakeCase(String),
}

impl CliArgType {
    fn coerce_value<'v>(&self, value: Value<'v>) -> anyhow::Result<CliArgValue> {
        Ok(match self {
            CliArgType::Bool => CliArgValue::Bool(value.unpack_bool().ok_or_else(|| {
                CliArgError::DefaultValueTypeError(self.dupe(), value.get_type().to_owned())
            })?),
            CliArgType::Int => CliArgValue::Int(value.unpack_i32().ok_or_else(|| {
                CliArgError::DefaultValueTypeError(self.dupe(), value.get_type().to_owned())
            })?),
            CliArgType::Float => CliArgValue::Float(
                StarlarkFloat::unpack_value(value)
                    .ok_or_else(|| {
                        CliArgError::DefaultValueTypeError(self.dupe(), value.get_type().to_owned())
                    })?
                    .0
                    .to_string(),
            ),
            CliArgType::String => CliArgValue::String(
                value
                    .unpack_str()
                    .ok_or_else(|| {
                        CliArgError::DefaultValueTypeError(self.dupe(), value.get_type().to_owned())
                    })?
                    .to_owned(),
            ),
            CliArgType::Enumeration(vs) => {
                let v = value
                    .unpack_str()
                    .ok_or_else(|| {
                        CliArgError::DefaultValueTypeError(self.dupe(), value.get_type().to_owned())
                    })?
                    .to_owned();
                if vs.contains(&v) {
                    CliArgValue::String(v)
                } else {
                    return Err(anyhow::anyhow!(CliArgError::DefaultValueTypeError(
                        self.dupe(),
                        v
                    )));
                }
            }
            CliArgType::List(inner) => {
                let list = ListRef::from_value(value).ok_or_else(|| {
                    CliArgError::DefaultValueTypeError(self.dupe(), value.get_type().to_owned())
                })?;
                let mut result = vec![];
                for item in list.iter() {
                    result.push(inner.coerce_value(item)?);
                }

                CliArgValue::List(result)
            }
            CliArgType::Option(inner) => {
                if value.is_none() {
                    CliArgValue::None
                } else {
                    inner.coerce_value(value)?
                }
            }
            CliArgType::TargetLabel => {
                let label = value.downcast_ref::<StarlarkTargetLabel>().ok_or_else(|| {
                    CliArgError::DefaultValueTypeError(self.dupe(), value.get_type().to_owned())
                })?;
                CliArgValue::TargetLabel(label.label().dupe())
            }
            CliArgType::SubTarget => {
                let label = value
                    .downcast_ref::<StarlarkProvidersLabel>()
                    .ok_or_else(|| {
                        CliArgError::DefaultValueTypeError(self.dupe(), value.get_type().to_owned())
                    })?;
                CliArgValue::ProvidersLabel((*label.label()).clone())
            }
            CliArgType::TargetExpr => {
                return Err(anyhow::anyhow!(CliArgError::NoDefaultsAllowed(
                    CliArgType::TargetExpr
                )));
            }
            CliArgType::SubTargetExpr => {
                return Err(anyhow::anyhow!(CliArgError::NoDefaultsAllowed(
                    CliArgType::SubTargetExpr
                )));
            }
        })
    }

    #[allow(deprecated)] // TODO(nga): fix.
    pub fn to_clap<'a>(&'a self, clap: clap::Arg<'a>) -> clap::Arg<'a> {
        match self {
            CliArgType::Bool => clap.takes_value(true).validator(|x| x.parse::<bool>()),
            CliArgType::Int => clap.takes_value(true).validator(|x| x.parse::<i32>()),
            CliArgType::Float => clap.takes_value(true).validator(|x| x.parse::<f64>()),
            CliArgType::String => clap.takes_value(true),
            CliArgType::Enumeration(variants) => clap
                .takes_value(true)
                .possible_values(variants.iter().map(String::as_str)),
            CliArgType::List(inner) => inner.to_clap(clap.takes_value(true).multiple(true)),
            CliArgType::Option(inner) => inner.to_clap(clap.required(false)),
            CliArgType::TargetLabel => clap.takes_value(true).validator(|x| {
                lex_target_pattern::<TargetPatternExtra>(x, false)
                    .and_then(|parsed| parsed.pattern.infer_target())
                    .and_then(|parsed| {
                        parsed
                            .target()
                            .context(CliArgError::NotALabel(x.to_owned(), "target"))
                            .map(|_| ())
                    })
            }),
            CliArgType::SubTarget => clap.takes_value(true).validator(|x| {
                lex_target_pattern::<ProvidersPatternExtra>(x, false)
                    .and_then(|parsed| parsed.pattern.infer_target())
                    .and_then(|parsed| {
                        parsed
                            .target()
                            .context(CliArgError::NotALabel(x.to_owned(), "target"))
                            .map(|_| ())
                    })
            }),
            CliArgType::TargetExpr => clap.takes_value(true),
            CliArgType::SubTargetExpr => clap.takes_value(true),
        }
    }

    pub fn parse_clap<'a>(
        &'a self,
        clap: ArgAccessor<'a>,
        ctx: &'a CliResolutionCtx<'a>,
    ) -> BoxFuture<'a, anyhow::Result<Option<CliArgValue>>> {
        async move {
            Ok(match self {
                CliArgType::Bool => clap.value_of().map_or(Ok(None), |x| {
                    let r: anyhow::Result<_> = try { CliArgValue::Bool(x.parse::<bool>()?) };
                    r.map(Some)
                })?,
                CliArgType::Int => clap.value_of().map_or(Ok(None), |x| {
                    let r: anyhow::Result<_> = try { CliArgValue::Int(x.parse::<i32>()?) };
                    r.map(Some)
                })?,
                CliArgType::Float => clap.value_of().map_or(Ok(None), |x| {
                    let r: anyhow::Result<_> = try {
                        CliArgValue::Float({
                            x.parse::<f64>()?;
                            x.to_owned()
                        })
                    };
                    r.map(Some)
                })?,
                CliArgType::String => clap.value_of().map(|s| CliArgValue::String(s.to_owned())),
                CliArgType::Enumeration(_variants) => {
                    // the validators should have already made sure the strings are only of the variants
                    clap.value_of().map(|s| CliArgValue::String(s.to_owned()))
                }
                CliArgType::List(inner) => match clap.values_of() {
                    None => None,
                    Some(values) => Some(CliArgValue::List(
                        futures::future::join_all(values.map(async move |v| try {
                            inner
                                .parse_clap(ArgAccessor::Literal(v), ctx)
                                .await?
                                .expect("shouldn't be empty when parsing list items")
                        }))
                        .await
                        .into_iter()
                        .collect::<anyhow::Result<_>>()?,
                    )),
                },
                CliArgType::Option(inner) => Some(if clap.value_of().is_some() {
                    inner
                        .parse_clap(clap, ctx)
                        .await?
                        .expect("arg is verified to be present")
                } else {
                    CliArgValue::None
                }),
                CliArgType::TargetLabel => clap.value_of().map_or(Ok(None), |x| {
                    let r: anyhow::Result<_> = try {
                        CliArgValue::TargetLabel(
                            ParsedPattern::<TargetPatternExtra>::parse_relaxed(
                                &ctx.target_alias_resolver,
                                ctx.relative_dir.as_cell_path(),
                                x,
                                &ctx.cell_resolver,
                            )?
                            .as_target_label(x)?,
                        )
                    };
                    r.map(Some)
                })?,
                CliArgType::SubTarget => clap.value_of().map_or(Ok(None), |x| {
                    let r: anyhow::Result<_> = try {
                        CliArgValue::ProvidersLabel(
                            ParsedPattern::<ProvidersPatternExtra>::parse_relaxed(
                                &ctx.target_alias_resolver,
                                ctx.relative_dir.as_cell_path(),
                                x,
                                &ctx.cell_resolver,
                            )?
                            .as_providers_label(x)?,
                        )
                    };
                    r.map(Some)
                })?,
                CliArgType::TargetExpr => {
                    let x = clap.value_of().unwrap_or("");
                    let pattern = ParsedPattern::<TargetPatternExtra>::parse_relaxed(
                        &ctx.target_alias_resolver,
                        ctx.relative_dir.as_cell_path(),
                        x,
                        &ctx.cell_resolver,
                    )?;
                    let loaded =
                        load_patterns(ctx.dice, vec![pattern], MissingTargetBehavior::Fail).await?;
                    Some(CliArgValue::List(
                        loaded
                            .iter_loaded_targets()
                            .map_ok(|t| CliArgValue::TargetLabel(t.label().dupe()))
                            .collect::<SharedResult<_>>()?,
                    ))
                }
                CliArgType::SubTargetExpr => {
                    let x = clap.value_of().unwrap_or("");
                    let pattern = ParsedPattern::<ProvidersPatternExtra>::parse_relaxed(
                        &ctx.target_alias_resolver,
                        ctx.relative_dir.as_cell_path(),
                        x,
                        &ctx.cell_resolver,
                    )?;
                    let loaded =
                        load_patterns(ctx.dice, vec![pattern], MissingTargetBehavior::Fail).await?;

                    Some(CliArgValue::List(
                        loaded
                            .into_iter()
                            .flat_map(|(pkg, result)| match result {
                                Ok(res) => res
                                    .keys()
                                    .map(|(target, pattern)| {
                                        Ok(CliArgValue::ProvidersLabel(
                                            pattern
                                                .to_owned()
                                                .into_providers_label(pkg.dupe(), target.as_ref()),
                                        ))
                                    })
                                    .collect::<Vec<_>>(),
                                Err(e) => vec![Err(e.dupe())],
                            })
                            .collect::<SharedResult<Vec<_>>>()?,
                    ))
                }
            })
        }
        .boxed()
    }
}

#[starlark_module]
pub(crate) fn cli_args_module(registry: &mut GlobalsBuilder) {
    fn string<'v>(
        default: Option<Value<'v>>,
        #[starlark(default = "")] doc: &str,
        #[starlark(require = named)] short: Option<Value<'v>>,
    ) -> anyhow::Result<CliArgs> {
        CliArgs::new(default, doc, CliArgType::string(), short)
    }

    fn list<'v>(
        #[starlark(require = pos)] inner: &CliArgs,
        default: Option<Value<'v>>,
        #[starlark(default = "")] doc: &str,
        #[starlark(require = named)] short: Option<Value<'v>>,
    ) -> anyhow::Result<CliArgs> {
        let coercer = CliArgType::list(inner.coercer.dupe());
        CliArgs::new(default, doc, coercer, short)
    }

    fn bool<'v>(
        #[starlark(default = false)] default: Value<'v>,
        #[starlark(default = "")] doc: &str,
        #[starlark(require = named)] short: Option<Value<'v>>,
    ) -> anyhow::Result<CliArgs> {
        CliArgs::new(Some(default), doc, CliArgType::bool(), short)
    }

    fn int<'v>(
        default: Option<Value<'v>>,
        #[starlark(default = "")] doc: &str,
        #[starlark(require = named)] short: Option<Value<'v>>,
    ) -> anyhow::Result<CliArgs> {
        CliArgs::new(default, doc, CliArgType::int(), short)
    }

    fn float<'v>(
        default: Option<Value<'v>>,
        #[starlark(default = "")] doc: &str,
        #[starlark(require = named)] short: Option<Value<'v>>,
    ) -> anyhow::Result<CliArgs> {
        CliArgs::new(default, doc, CliArgType::float(), short)
    }

    fn option<'v>(
        inner: &CliArgs,
        #[starlark(default = "")] doc: &str,
        #[starlark(default = NoneType)] default: Value<'v>,
        #[starlark(require = named)] short: Option<Value<'v>>,
    ) -> anyhow::Result<CliArgs> {
        let coercer = CliArgType::option(inner.coercer.dupe());
        CliArgs::new(Some(default), doc, coercer, short)
    }

    fn r#enum<'v>(
        #[starlark(require = pos)] variants: Vec<String>,
        default: Option<Value<'v>>,
        #[starlark(default = "")] doc: &str,
        #[starlark(require = named)] short: Option<Value<'v>>,
    ) -> anyhow::Result<CliArgs> {
        // Value seems to usually be a `[String]`, listing the possible values of the
        // enumeration. Unfortunately, for things like `exported_lang_preprocessor_flags`
        // it ends up being `Type` which doesn't match the data we see.
        CliArgs::new(
            default,
            doc,
            CliArgType::enumeration(variants.into_iter().collect()),
            short,
        )
    }

    fn target_label<'v>(
        #[starlark(default = "")] doc: &str,
        #[starlark(require = named)] short: Option<Value<'v>>,
    ) -> anyhow::Result<CliArgs> {
        CliArgs::new(None, doc, CliArgType::target_label(), short)
    }

    fn sub_target<'v>(
        #[starlark(default = "")] doc: &str,
        #[starlark(require = named)] short: Option<Value<'v>>,
    ) -> anyhow::Result<CliArgs> {
        CliArgs::new(None, doc, CliArgType::sub_target(), short)
    }

    fn target_expr<'v>(
        #[starlark(default = "")] doc: &str,
        #[starlark(require = named)] short: Option<Value<'v>>,
    ) -> anyhow::Result<CliArgs> {
        CliArgs::new(None, doc, CliArgType::target_expr(), short)
    }

    fn sub_target_expr<'v>(
        #[starlark(default = "")] doc: &str,
        #[starlark(require = named)] short: Option<Value<'v>>,
    ) -> anyhow::Result<CliArgs> {
        CliArgs::new(None, doc, CliArgType::sub_target_expr(), short)
    }
}

pub fn register_cli_args_module(registry: &mut GlobalsBuilder) {
    cli_args_module(registry)
}

#[cfg(test)]
mod tests {
    use std::collections::HashSet;

    use buck2_core::provider::label::testing::ProvidersLabelTestExt;
    use buck2_core::provider::label::ProvidersLabel;
    use buck2_core::target::label::TargetLabel;
    use buck2_interpreter::types::label::StarlarkProvidersLabel;
    use buck2_interpreter::types::target_label::StarlarkTargetLabel;
    use starlark::values::Heap;
    use starlark::values::Value;

    use crate::bxl::starlark_defs::cli_args::CliArgType;
    use crate::bxl::starlark_defs::cli_args::CliArgValue;

    #[test]
    fn print_cli_arg_list() -> anyhow::Result<()> {
        let args = vec![
            CliArgValue::Bool(true),
            CliArgValue::String("test".to_owned()),
            CliArgValue::Int(1),
        ];

        let cli_arg = CliArgValue::List(args);
        let printed = format!("{}", cli_arg);
        assert_eq!(printed, "true,test,1");

        Ok(())
    }

    #[test]
    fn coerce_starlark() -> anyhow::Result<()> {
        let heap = Heap::new();

        assert_eq!(
            CliArgType::bool().coerce_value(Value::new_bool(true))?,
            CliArgValue::Bool(true)
        );

        assert_eq!(
            CliArgType::int().coerce_value(heap.alloc(42))?,
            CliArgValue::Int(42)
        );

        assert_eq!(
            CliArgType::float().coerce_value(heap.alloc(4.2))?,
            CliArgValue::Float("4.2".to_owned())
        );

        assert_eq!(
            CliArgType::string().coerce_value(heap.alloc("foobar"))?,
            CliArgValue::String("foobar".to_owned())
        );

        assert_eq!(
            CliArgType::enumeration(HashSet::from_iter([
                "a".to_owned(),
                "b".to_owned(),
                "c".to_owned()
            ]))
            .coerce_value(heap.alloc("a"))?,
            CliArgValue::String("a".to_owned())
        );

        assert_eq!(
            CliArgType::option(CliArgType::int()).coerce_value(Value::new_none())?,
            CliArgValue::None
        );

        assert_eq!(
            CliArgType::option(CliArgType::bool()).coerce_value(Value::new_bool(true))?,
            CliArgValue::Bool(true)
        );

        assert_eq!(
            CliArgType::bool().coerce_value(Value::new_bool(false))?,
            CliArgValue::Bool(false)
        );

        assert_eq!(
            CliArgType::list(CliArgType::int()).coerce_value(heap.alloc(vec![1, 4, 2]))?,
            CliArgValue::List(vec![
                CliArgValue::Int(1),
                CliArgValue::Int(4),
                CliArgValue::Int(2)
            ])
        );

        assert_eq!(
            CliArgType::target_label().coerce_value(heap.alloc(StarlarkTargetLabel::new(
                TargetLabel::testing_parse("root//foo:bar")
            )))?,
            CliArgValue::TargetLabel(TargetLabel::testing_parse("root//foo:bar"))
        );

        assert_eq!(
            CliArgType::sub_target().coerce_value(heap.alloc(StarlarkProvidersLabel::new(
                ProvidersLabel::testing_new("foo", "pkg", "bar", Some(&["a", "b"]))
            )))?,
            CliArgValue::ProvidersLabel(ProvidersLabel::testing_new(
                "foo",
                "pkg",
                "bar",
                Some(&["a", "b"])
            ))
        );

        Ok(())
    }
}

pub(crate) enum ArgAccessor<'a> {
    Clap {
        clap: &'a clap::ArgMatches,
        arg: &'a str,
    },
    Literal(&'a str),
}

#[allow(deprecated)] // TODO(nga): fix.
impl<'a> ArgAccessor<'a> {
    fn value_of(&self) -> Option<&str> {
        match self {
            ArgAccessor::Clap { clap, arg } => clap.value_of(arg),
            ArgAccessor::Literal(s) => Some(s),
        }
    }

    fn values_of(&self) -> Option<impl Iterator<Item = &str>> {
        match self {
            ArgAccessor::Clap { clap, arg } => clap.values_of(arg).map(itertools::Either::Left),
            ArgAccessor::Literal(s) => Some(itertools::Either::Right(std::iter::once(*s))),
        }
    }
}


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

//! The context containing the available buck commands and query operations for `bxl` functions.
//!

use std::cell::RefCell;
use std::io::Write;
use std::iter;
use std::sync::Arc;

use allocative::Allocative;
use buck2_artifact::artifact::build_artifact::BuildArtifact;
use buck2_build_api::analysis::registry::AnalysisRegistry;
use buck2_build_api::artifact_groups::ArtifactGroup;
use buck2_build_api::bxl::execution_platform::EXECUTION_PLATFORM;
use buck2_build_api::interpreter::rule_defs::context::AnalysisActions;
use buck2_cli_proto::build_request::Materializations;
use buck2_common::dice::cells::HasCellResolver;
use buck2_common::dice::data::HasIoProvider;
use buck2_common::events::HasEvents;
use buck2_common::target_aliases::BuckConfigTargetAliasResolver;
use buck2_core::cells::cell_path::CellPath;
use buck2_core::cells::name::CellName;
use buck2_core::cells::CellResolver;
use buck2_core::fs::artifact_path_resolver::ArtifactFs;
use buck2_core::fs::paths::abs_norm_path::AbsNormPathBuf;
use buck2_core::fs::project::ProjectRoot;
use buck2_core::fs::project_rel_path::ProjectRelativePathBuf;
use buck2_core::pattern::query_file_literal::parse_query_file_literal;
use buck2_core::provider::label::ConfiguredProvidersLabel;
use buck2_core::provider::label::ProvidersLabel;
use buck2_core::target::label::TargetLabel;
use buck2_execute::digest_config::DigestConfig;
use buck2_interpreter::starlark_promise::StarlarkPromise;
use buck2_interpreter::types::label::Label;
use buck2_interpreter::types::label::StarlarkProvidersLabel;
use buck2_node::nodes::configured::ConfiguredTargetNode;
use buck2_node::nodes::configured_frontend::ConfiguredTargetNodeCalculation;
use buck2_node::nodes::frontend::TargetGraphCalculation;
use buck2_node::nodes::unconfigured::TargetNode;
use dashmap::DashMap;
use derivative::Derivative;
use derive_more::Display;
use dupe::Dupe;
use either::Either;
use indexmap::IndexSet;
use itertools::Itertools;
use starlark::any::ProvidesStaticType;
use starlark::environment::Methods;
use starlark::environment::MethodsBuilder;
use starlark::environment::MethodsStatic;
use starlark::eval::Evaluator;
use starlark::starlark_module;
use starlark::values::dict::Dict;
use starlark::values::none::NoneType;
use starlark::values::starlark_value;
use starlark::values::structs::AllocStruct;
use starlark::values::AllocValue;
use starlark::values::Heap;
use starlark::values::NoSerialize;
use starlark::values::StarlarkValue;
use starlark::values::Trace;
use starlark::values::Value;
use starlark::values::ValueOf;
use starlark::values::ValueOfUnchecked;
use starlark::values::ValueTyped;
use starlark::StarlarkDocs;

use crate::bxl::key::BxlKey;
use crate::bxl::starlark_defs::alloc_node::AllocNode;
use crate::bxl::starlark_defs::audit::StarlarkAuditCtx;
use crate::bxl::starlark_defs::context::actions::resolve_bxl_execution_platform;
use crate::bxl::starlark_defs::context::actions::validate_action_instantiation;
use crate::bxl::starlark_defs::context::actions::BxlActions;
use crate::bxl::starlark_defs::context::fs::BxlFilesystem;
use crate::bxl::starlark_defs::context::output::EnsuredArtifactOrGroup;
use crate::bxl::starlark_defs::context::output::OutputStream;
use crate::bxl::starlark_defs::context::starlark_async::BxlSafeDiceComputations;
use crate::bxl::starlark_defs::cquery::StarlarkCQueryCtx;
use crate::bxl::starlark_defs::event::to_starlark_user_event;
use crate::bxl::starlark_defs::providers_expr::ProvidersExpr;
use crate::bxl::starlark_defs::target_expr::filter_incompatible;
use crate::bxl::starlark_defs::target_expr::TargetExpr;
use crate::bxl::starlark_defs::targetset::StarlarkTargetSet;
use crate::bxl::starlark_defs::uquery::StarlarkUQueryCtx;
use crate::bxl::value_as_starlark_target_label::ValueAsStarlarkTargetLabel;

pub mod actions;
pub mod analysis;
pub mod build;
pub mod fs;
pub mod output;
pub mod starlark_async;

#[derive(
    ProvidesStaticType,
    Derivative,
    Display,
    Trace,
    NoSerialize,
    StarlarkDocs,
    Allocative
)]
#[starlark_docs(directory = "bxl")]
#[derivative(Debug)]
#[display(fmt = "{:?}", self)]
pub struct BxlContext<'v> {
    pub(crate) current_bxl: BxlKey,
    #[derivative(Debug = "ignore")]
    pub(crate) target_alias_resolver: BuckConfigTargetAliasResolver,
    pub(crate) cell_name: CellName,
    pub(crate) cell_root_abs: AbsNormPathBuf,
    #[derivative(Debug = "ignore")]
    pub(crate) cell_resolver: CellResolver,
    cli_args: Value<'v>, // Struct of the cli args
    #[trace(unsafe_ignore)]
    #[derivative(Debug = "ignore")]
    #[allocative(skip)]
    pub(crate) async_ctx: BxlSafeDiceComputations<'v>,
    pub(crate) state: ValueTyped<'v, AnalysisActions<'v>>,
    pub(crate) output_stream: ValueTyped<'v, OutputStream<'v>>,
    pub(crate) error_stream: ValueTyped<'v, OutputStream<'v>>,
    pub(crate) global_target_platform: Option<TargetLabel>,
    #[derivative(Debug = "ignore")]
    #[allocative(skip)]
    /// Use a RefCell/Option so when we are done with it, without obtaining exclusive access,
    /// we can take the internal state without having to clone it.
    pub(crate) materializations: Arc<DashMap<BuildArtifact, ()>>,
}

impl<'v> BxlContext<'v> {
    pub(crate) fn new(
        heap: &'v Heap,
        current_bxl: BxlKey,
        cli_args: Value<'v>,
        target_alias_resolver: BuckConfigTargetAliasResolver,
        project_fs: ProjectRoot,
        artifact_fs: ArtifactFs,
        cell_resolver: CellResolver,
        cell_name: CellName,
        async_ctx: BxlSafeDiceComputations<'v>,
        output_sink: RefCell<Box<dyn Write>>,
        error_sink: RefCell<Box<dyn Write>>,
        digest_config: DigestConfig,
        global_target_platform: Option<TargetLabel>,
    ) -> anyhow::Result<Self> {
        let cell_root_abs = project_fs.root().join(
            cell_resolver
                .get(cell_name)?
                .path()
                .as_project_relative_path(),
        );
        Ok(Self {
            current_bxl,
            target_alias_resolver,
            cell_name,
            cell_root_abs,
            cell_resolver,
            cli_args,
            async_ctx: async_ctx.clone(),
            state: heap.alloc_typed(AnalysisActions {
                state: RefCell::new(None),
                // TODO(nga): attributes struct should not be accessible to BXL.
                attributes: ValueOfUnchecked::new_checked(heap.alloc(AllocStruct::EMPTY)).unwrap(),
                digest_config,
            }),
            output_stream: heap.alloc_typed(OutputStream::new(
                project_fs.clone(),
                artifact_fs.clone(),
                output_sink,
                async_ctx.clone(),
            )),
            error_stream: heap.alloc_typed(OutputStream::new(
                project_fs,
                artifact_fs,
                error_sink,
                async_ctx,
            )),
            global_target_platform,
            materializations: Arc::new(DashMap::new()),
        })
    }

    // Used for caching error logs emitted from within the BXL core.
    pub(crate) fn print_to_error_stream(&self, msg: String) -> anyhow::Result<()> {
        writeln!(self.error_stream.sink.borrow_mut(), "{}", msg)?;
        Ok(())
    }

    pub(crate) fn project_root(&self) -> &ProjectRoot {
        &self.output_stream.project_fs
    }

    /// Working dir for resolving literals.
    /// Note, unlike buck2 command line UI, we resolve targets and literals
    /// against the cell root instead of user working dir.
    pub(crate) fn working_dir(&self) -> anyhow::Result<ProjectRelativePathBuf> {
        let cell = self.cell_resolver.get(self.cell_name)?;
        Ok(cell.path().as_project_relative_path().to_owned())
    }

    pub(crate) fn parse_query_file_literal(&self, literal: &str) -> anyhow::Result<CellPath> {
        parse_query_file_literal(
            literal,
            self.cell_resolver
                .get(self.cell_name)?
                .cell_alias_resolver(),
            &self.cell_resolver,
            // NOTE(nga): we pass cell root as working directory here,
            //   which is inconsistent with the rest of buck2:
            //   The same query `owner(foo.h)` is resolved using
            //   current directory in `buck2 query`, but relative to cell root in BXL.
            &self.cell_root_abs,
            self.project_root(),
        )
    }

    /// Must take an `AnalysisContext` and `OutputStream` which has never had `take_state` called on it before.
    pub(crate) fn take_state(
        value: ValueTyped<'v, BxlContext<'v>>,
    ) -> anyhow::Result<(
        Option<AnalysisRegistry<'v>>,
        IndexSet<ArtifactGroup>,
        Arc<DashMap<BuildArtifact, ()>>,
    )> {
        let this = value.as_ref();
        Ok((
            this.state.as_ref().state.borrow_mut().take(),
            // artifacts should be bound by now as the bxl has finished running
            this.output_stream
                .as_ref()
                .take_artifacts()
                .into_iter()
                .map(|ensured_artifact_type| match ensured_artifact_type {
                    EnsuredArtifactOrGroup::Artifact(artifact) => {
                        let as_artifact = artifact.as_artifact();
                        let bound_artifact = as_artifact.get_bound_artifact()?;
                        let associated_artifacts = as_artifact.get_associated_artifacts();

                        Ok(associated_artifacts
                            .iter()
                            .flat_map(|v| v.iter())
                            .cloned()
                            .chain(iter::once(ArtifactGroup::Artifact(bound_artifact)))
                            .collect::<Vec<_>>())
                    }
                    EnsuredArtifactOrGroup::ArtifactGroup(ag) => Ok(vec![ag]),
                })
                .flatten_ok()
                .collect::<anyhow::Result<IndexSet<ArtifactGroup>>>()?,
            this.materializations.dupe(),
        ))
    }
}

#[starlark_value(type = "bxl_ctx", StarlarkTypeRepr, UnpackValue)]
impl<'v> StarlarkValue<'v> for BxlContext<'v> {
    fn get_methods() -> Option<&'static Methods> {
        static RES: MethodsStatic = MethodsStatic::new();
        RES.methods(register_context)
    }
}

impl<'v> AllocValue<'v> for BxlContext<'v> {
    fn alloc_value(self, heap: &'v Heap) -> Value<'v> {
        heap.alloc_complex_no_freeze(self)
    }
}

/// The bxl context that the top level bxl implementation receives as parameter.
/// This context contains all the core bxl functions to query, build, create actions, etc.
#[starlark_module]
fn register_context(builder: &mut MethodsBuilder) {
    /// Gets the output stream to the console via stdout. Items written to the output stream
    /// are considered to be the results of a bxl script, which will be displayed to stdout by
    /// buck2 even when the script is cached.
    ///
    /// Prints that are not result of the bxl should be printed via stderr via the stdlib `print`
    /// and `pprint`.
    #[starlark(attribute)]
    fn output<'v>(this: &'v BxlContext) -> anyhow::Result<Value<'v>> {
        Ok(this.output_stream.to_value())
    }

    /// Returns the absolute path to the root of the repository
    fn root(this: &BxlContext) -> anyhow::Result<String> {
        Ok(this
            .async_ctx
            .0
            .global_data()
            .get_io_provider()
            .project_root()
            .root()
            .to_str()?
            .to_owned())
    }

    /// Gets the target nodes for the `labels`, accepting an optional `target_platform` which is the
    /// target platform configuration used to resolve configurations of any unconfigured target
    /// nodes.
    /// The `target_platform` is either a string that can be parsed as a target label, or a
    /// target label.
    ///
    /// The given `labels` is a [`TargetExpr`], which is either:
    ///     - a single string that is a `target pattern`.
    ///     - a single target node or label, configured or unconfigured
    ///     - a list of the two options above.
    ///
    /// Note that this function does not accept `Label` (which is a configured provider label), since this
    /// is the label of a subtarget. You can get the underlying configured target label on the `Label`
    /// using `configured_targets()` (ex: `my_label.configured_target()`).
    ///
    /// This returns either a single [`StarlarkConfiguredTargetNode`] if the given `labels`
    /// is "singular", a dict keyed by target labels of [`StarlarkConfiguredTargetNode`] if the
    /// given `labels` is list-like
    fn configured_targets<'v>(
        this: &'v BxlContext<'v>,
        #[starlark(require = pos)] labels: Value<'v>,
        #[starlark(default = NoneType)] target_platform: Value<'v>,
        eval: &mut Evaluator<'v, '_>,
    ) -> anyhow::Result<Value<'v>> {
        let target_platform = target_platform.parse_target_platforms(
            &this.target_alias_resolver,
            &this.cell_resolver,
            this.cell_name,
            &this.global_target_platform,
        )?;

        let res: anyhow::Result<Value<'v>> = this.async_ctx.via_dice(|ctx| async move {
            Ok(
                match TargetExpr::<'v, ConfiguredTargetNode>::unpack(
                    labels,
                    &target_platform,
                    this,
                    eval,
                )
                .await?
                {
                    TargetExpr::Label(label) => {
                        let set = filter_incompatible(
                            iter::once(ctx.get_configured_target_node(&label).await?),
                            this,
                        )?;

                        // When a target label is passed in, we should only get one target node.
                        // filter_incompatible() returns a set, so lets assert the size
                        assert!(set.len() <= 1);

                        if let Some(node) = set.iter().next() {
                            node.clone().alloc(eval.heap())
                        } else {
                            Value::new_none()
                        }
                    }

                    TargetExpr::Node(node) => node.alloc(eval.heap()),
                    multi => eval
                        .heap()
                        .alloc(StarlarkTargetSet::from(filter_incompatible(
                            multi.get(this.async_ctx.0).await?.into_iter(),
                            this,
                        )?)),
                },
            )
        });

        res
    }

    /// Gets the unconfigured target nodes for the `labels`
    ///
    /// The given `labels` is either:
    ///     - a single string that is a `target pattern`.
    ///     - a single unconfigured  target node or label
    ///     - a list of the two options above.
    ///
    /// This returns either a single [`StarlarkTargetNode`] if the given `labels`
    /// is "singular", a dict keyed by target labels of [`StarlarkTargetNode`] if the
    /// given `labels` is list-like
    fn unconfigured_targets<'v>(
        this: &'v BxlContext<'v>,
        labels: Value<'v>,
        eval: &mut Evaluator<'v, '_>,
    ) -> anyhow::Result<Value<'v>> {
        let res: anyhow::Result<Value<'v>> = this.async_ctx.via_dice(|ctx| async move {
            Ok(
                match TargetExpr::<'v, TargetNode>::unpack(labels, this, eval).await? {
                    TargetExpr::Label(label) => {
                        let node = ctx.get_target_node(&label).await?;

                        node.alloc(eval.heap())
                    }

                    TargetExpr::Node(node) => node.alloc(eval.heap()),
                    multi => eval
                        .heap()
                        .alloc(StarlarkTargetSet::from(multi.get(ctx).await?.into_owned())),
                },
            )
        });

        res
    }

    /// Returns the [`StarlarkUQueryCtx`] that holds all uquery functions.
    fn uquery<'v>(this: &'v BxlContext<'v>) -> anyhow::Result<StarlarkUQueryCtx<'v>> {
        StarlarkUQueryCtx::new(this)
    }

    /// Returns the [`StarlarkCQueryCtx`] that holds all the cquery functions.
    /// This function takes an optional parameter `target_platform`, which is the target platform
    /// configuration used to configured any unconfigured target nodes.
    ///
    /// The `target_platform` is a target label, or a string that is a target label.
    fn cquery<'v>(
        this: &'v BxlContext<'v>,
        // TODO(brasselsprouts): I would like to strongly type this.
        #[starlark(default = NoneType)] target_platform: Value<'v>,
    ) -> anyhow::Result<StarlarkCQueryCtx<'v>> {
        this.async_ctx
            .via(|| StarlarkCQueryCtx::new(this, target_platform, &this.global_target_platform))
    }

    /// Returns the bxl actions to create and register actions for this
    /// bxl function. This will have the execution platform resolved according to the execution
    /// deps and toolchains you pass into this function.
    /// You'll be able to access the analysis action factory of the correct execution platform,
    /// toolchains, and execution deps of the corresponding configuration via this context.
    ///
    /// Actions created by bxl will not be built by default. Instead, they are marked to be built
    /// by `ctx.output.ensure(artifact)` on the output module of the [`BxlContext`]. Only artifacts
    /// marked by ensure will be built.
    ///
    /// Sample usage:
    /// ```text
    /// def _impl_write_action(ctx):
    ///     bxl_actions = ctx.bxl_actions()
    ///     output = bxl_actions.actions.write("my_output", "my_content")
    ///     ensured = ctx.output.ensure(output)
    ///     ctx.output.print(ensured)
    /// ```
    ///
    /// There are several optional named parameters:
    ///
    /// `exec_deps` - These are dependencies you wish to access as executables for creating the action.
    /// This is usually the same set of targets one would pass to rule's `attr.exec_dep`.
    /// `toolchains` - The set of toolchains needed for the actions you intend to create.
    /// `target_platform` - The intended target platform for your toolchains
    /// `exec_compatible_with` - Explicit list of configuration nodes (like platforms or constraints)
    /// that these actions are compatible with. This is the 'exec_compatible_with' attribute of a target.
    fn bxl_actions<'v>(
        this: &'v BxlContext<'v>,
        #[starlark(require = named, default = NoneType)] exec_deps: Value<'v>,
        #[starlark(require = named, default = NoneType)] toolchains: Value<'v>,
        #[starlark(require = named, default = NoneType)] target_platform: Value<'v>,
        #[starlark(require = named, default = NoneType)] exec_compatible_with: Value<'v>,
        eval: &mut Evaluator<'v, '_>,
    ) -> anyhow::Result<BxlActions<'v>> {
        let execution_resolution = this.async_ctx.via_dice(|ctx| async {
            let target_platform = target_platform.parse_target_platforms(
                &this.target_alias_resolver,
                &this.cell_resolver,
                this.cell_name,
                &this.global_target_platform,
            )?;

            let exec_deps = if exec_deps.is_none() {
                Vec::new()
            } else {
                ProvidersExpr::<ProvidersLabel>::unpack(exec_deps, this, eval)?
                    .labels()
                    .cloned()
                    .collect()
            };

            let toolchains = if toolchains.is_none() {
                Vec::new()
            } else {
                ProvidersExpr::<ProvidersLabel>::unpack(toolchains, this, eval)?
                    .labels()
                    .cloned()
                    .collect()
            };

            let exec_compatible_with = if exec_compatible_with.is_none() {
                Vec::new()
            } else {
                TargetExpr::<TargetNode>::unpack(exec_compatible_with, this, eval)
                    .await?
                    .get(ctx)
                    .await?
                    .iter()
                    .map(|n| n.label().dupe())
                    .collect()
            };

            resolve_bxl_execution_platform(
                ctx,
                this.cell_name.dupe(),
                exec_deps,
                toolchains,
                target_platform,
                exec_compatible_with,
                eval.module(),
            )
            .await
        })?;

        validate_action_instantiation(this, execution_resolution.resolved_execution)?;

        let exec_deps = eval.heap().alloc(Dict::new(
            execution_resolution
                .exec_deps_configured
                .into_iter()
                .map(|(k, v)| {
                    Ok((
                        eval.heap()
                            .alloc(StarlarkProvidersLabel::new(k))
                            .get_hashed()?,
                        eval.heap().alloc(v),
                    ))
                })
                .collect::<anyhow::Result<_>>()?,
        ));
        let exec_deps = ValueOfUnchecked::new_checked(exec_deps)?;
        let toolchains = eval.heap().alloc(Dict::new(
            execution_resolution
                .toolchain_deps_configured
                .into_iter()
                .map(|(k, v)| {
                    Ok((
                        eval.heap()
                            .alloc(StarlarkProvidersLabel::new(k))
                            .get_hashed()?,
                        eval.heap().alloc(v),
                    ))
                })
                .collect::<anyhow::Result<_>>()?,
        ));
        let toolchains = ValueOfUnchecked::new_checked(toolchains)?;

        Ok(BxlActions::new(this.state, exec_deps, toolchains))
    }

    /// DO NOT USE - will be deprecated soon.
    #[starlark(attribute)]
    fn actions_factory<'v>(this: ValueOf<'v, &'v BxlContext<'v>>) -> anyhow::Result<Value<'v>> {
        validate_action_instantiation(this.typed, EXECUTION_PLATFORM.dupe())?;

        Ok(this.typed.state.to_value())
    }

    /// Runs analysis on the given `labels`, accepting an optional `target_platform` which is the
    /// target platform configuration used to resolve configurations of any unconfigured target
    /// nodes, and an optional `skip_incompatible` boolean that indicates whether to skip analysis
    /// of nodes that are incompatible with the target platform.
    /// The `target_platform` is either a string that can be parsed as a target label, or a
    /// target label.
    ///
    /// The given `labels` is a providers expression, which is either:
    ///     - a single string that is a `target pattern`.
    ///     - a single target node or label, configured or unconfigured
    ///     - a single sub target label, configured or unconfigured
    ///     - a list of the two options above.
    ///
    /// This returns either a single [`StarlarkAnalysisResult`] if the given `labels` is "singular",
    /// or a dict keyed by sub target labels of [`StarlarkAnalysisResult`] if the given `labels`
    /// is list-like
    fn analysis<'v>(
        this: &'v BxlContext<'v>,
        labels: Value<'v>,
        #[starlark(default = NoneType)] target_platform: Value<'v>,
        #[starlark(default = true)] skip_incompatible: bool,
        eval: &mut Evaluator<'v, '_>,
    ) -> anyhow::Result<Value<'v>> {
        let providers =
            ProvidersExpr::<ConfiguredProvidersLabel>::unpack(labels, target_platform, this, eval)?;

        let res: anyhow::Result<_> = this
            .async_ctx
            .via_dice(|ctx| analysis::analysis(ctx, providers, skip_incompatible));

        Ok(match res? {
            Either::Left(single) => eval.heap().alloc(single),
            Either::Right(many) => eval.heap().alloc(Dict::new(
                many.into_iter()
                    .map(|(t, v)| {
                        Ok((
                            eval.heap().alloc(Label::new(t)).get_hashed()?,
                            eval.heap().alloc(v),
                        ))
                    })
                    .collect::<anyhow::Result<_>>()?,
            )),
        })
    }

    /// Runs a build on the given `labels`, accepting an optional `target_platform` which is the
    /// target platform configuration used to resolve configurations. Note that when `build()` is called,
    /// the artifacts are materialized without needing to additionally call `ensure()` on them.
    ///
    /// The given `labels` is a providers expression, which is either:
    ///     - a single string that is a `target pattern`.
    ///     - a single target node or label, configured or unconfigured
    ///     - a single provider label, configured or unconfigured
    ///     - a list of the two options above.
    ///
    /// This returns a dict keyed by sub target labels of [`StarlarkBuildResult`] if the
    /// given `labels` is list-like
    ///
    fn build<'v>(
        this: &'v BxlContext<'v>,
        spec: Value<'v>,
        #[starlark(default = NoneType)] target_platform: Value<'v>,
        #[starlark(require = named, default = "default")] materializations: &str,
        eval: &mut Evaluator<'v, '_>,
    ) -> anyhow::Result<Value<'v>> {
        Ok(eval.heap().alloc(Dict::new(build::build(
            this,
            spec,
            target_platform,
            Materializations::from_str_name(&materializations.to_uppercase()).ok_or_else(|| {
                anyhow::anyhow!("Unknown materialization setting `{}`", materializations)
            })?,
            eval,
        )?)))
    }

    /// A struct of the command line args as declared using the [`cli_args`] module.
    /// These command lines are resolved per the users input on the cli when invoking the bxl script.
    ///
    /// If you wish to pass in a kebab-cased arg, the arg accessed from the BXL context's `cli_args`
    /// attrbute will always be in snakecase. For example, if you passed in `my-arg`, accessing it
    /// within BXL would look like `ctx.cli_args.my_arg`.
    #[starlark(attribute)]
    fn cli_args<'v>(this: &BxlContext<'v>) -> anyhow::Result<Value<'v>> {
        Ok(this.cli_args)
    }

    /// Returns the [`BxlFilesystem`] for performing a basic set of filesystem operations within bxl
    #[starlark(attribute)]
    fn fs<'v>(this: &BxlContext<'v>) -> anyhow::Result<BxlFilesystem<'v>> {
        Ok(BxlFilesystem::new(this))
    }

    /// Returns the [`StarlarkAuditCtx`] that holds all the audit functions.
    fn audit<'v>(this: &'v BxlContext<'v>) -> anyhow::Result<StarlarkAuditCtx<'v>> {
        this.async_ctx.via_dice(|ctx| async move {
            let working_dir = this
                .cell_resolver
                .get(this.cell_name)?
                .path()
                .as_project_relative_path();
            let cell_resolver = ctx.get_cell_resolver().await?;

            StarlarkAuditCtx::new(
                this,
                working_dir.to_buf(),
                cell_resolver,
                this.global_target_platform.clone(),
            )
        })
    }

    /// Awaits a promise and returns an optional value of the promise.
    ///
    /// Sample usage:
    /// ```python
    /// load("//path/to/rules:rules.bzl", "my_anon_targets_rule", "my_map_function")
    ///
    /// def _resolve_impl(ctx):
    ///     actions = ctx.bxl_actions().actions
    ///     my_attrs = {
    ///         "false": False,
    ///         "int": 42,
    ///         "list_string": ["a", "b", "c"],
    ///         "string": "a-string",
    ///         "true": True,
    ///     }
    ///
    ///     promise = actions.anon_target(my_anon_targets_rule, attrs).map(my_map_function)
    ///     providers_result = ctx.resolve(actions, promise) # result is `provider_callable` type, which is a collection of `provider`s
    ///     ctx.output.print(providers_result[0].my_field)
    /// ```
    fn resolve<'v>(
        this: &'v BxlContext<'v>,
        action_factory: ValueTyped<'v, AnalysisActions<'v>>,
        promise: ValueTyped<'v, StarlarkPromise<'v>>,
        eval: &mut Evaluator<'v, '_>,
    ) -> anyhow::Result<Option<Value<'v>>> {
        this.async_ctx.via_dice(|dice| {
            action_factory.run_promises(dice, eval, format!("bxl$promises:{}", &this.current_bxl))
        })?;
        Ok(promise.get())
    }

    /// Emits a user-defined instant event, taking in a required string id and a metadata dictionary where the
    /// keys are strings, and values are either strings, bools, or ints. The id is user-supplied, and used to
    /// identify the instant events in the event logs more easily.
    fn instant_event<'v>(
        this: &BxlContext<'v>,
        #[starlark(require = named)] id: &str,
        #[starlark(require = named)] metadata: Value<'v>,
    ) -> anyhow::Result<NoneType> {
        let event = to_starlark_user_event(id, metadata)?;

        this.async_ctx
            .0
            .per_transaction_data()
            .get_dispatcher()
            .instant_event(event);

        Ok(NoneType)
    }
}