package configs

import (
	"fmt"

	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/terraform/experiments"
)

// sniffActiveExperiments does minimal parsing of the given body for
// "terraform" blocks with "experiments" attributes, returning the
// experiments found.
//
// This is separate from other processing so that we can be sure that all of
// the experiments are known before we process the result of the module config,
// and thus we can take into account which experiments are active when deciding
// how to decode.
func sniffActiveExperiments(body hcl.Body) (experiments.Set, hcl.Diagnostics) {
	rootContent, _, diags := body.PartialContent(configFileTerraformBlockSniffRootSchema)

	ret := experiments.NewSet()

	for _, block := range rootContent.Blocks {
		content, _, blockDiags := block.Body.PartialContent(configFileExperimentsSniffBlockSchema)
		diags = append(diags, blockDiags...)

		attr, exists := content.Attributes["experiments"]
		if !exists {
			continue
		}

		exps, expDiags := decodeExperimentsAttr(attr)
		diags = append(diags, expDiags...)
		if !expDiags.HasErrors() {
			ret = experiments.SetUnion(ret, exps)
		}
	}

	return ret, diags
}

func decodeExperimentsAttr(attr *hcl.Attribute) (experiments.Set, hcl.Diagnostics) {
	var diags hcl.Diagnostics

	exprs, moreDiags := hcl.ExprList(attr.Expr)
	diags = append(diags, moreDiags...)
	if moreDiags.HasErrors() {
		return nil, diags
	}

	var ret = experiments.NewSet()
	for _, expr := range exprs {
		kw := hcl.ExprAsKeyword(expr)
		if kw == "" {
			diags = diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Invalid experiment keyword",
				Detail:   "Elements of \"experiments\" must all be keywords representing active experiments.",
				Subject:  expr.Range().Ptr(),
			})
			continue
		}

		exp, err := experiments.GetCurrent(kw)
		switch err := err.(type) {
		case experiments.UnavailableError:
			diags = diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Unknown experiment keyword",
				Detail:   fmt.Sprintf("There is no current experiment with the keyword %q.", kw),
				Subject:  expr.Range().Ptr(),
			})
		case experiments.ConcludedError:
			diags = diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Experiment has concluded",
				Detail:   fmt.Sprintf("Experiment %q is no longer available. %s", kw, err.Message),
				Subject:  expr.Range().Ptr(),
			})
		case nil:
			// No error at all means it's valid and current.
			ret.Add(exp)

			// However, experimental features are subject to breaking changes
			// in future releases, so we'll warn about them to help make sure
			// folks aren't inadvertently using them in places where that'd be
			// inappropriate, particularly if the experiment is active in a
			// shared module they depend on.
			diags = diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagWarning,
				Summary:  fmt.Sprintf("Experimental feature %q is active", exp.Keyword()),
				Detail:   "Experimental features are subject to breaking changes in future minor or patch releases, based on feedback.\n\nIf you have feedback on the design of this feature, please open a GitHub issue to discuss it.",
				Subject:  expr.Range().Ptr(),
			})

		default:
			// This should never happen, because GetCurrent is not documented
			// to return any other error type, but we'll handle it to be robust.
			diags = diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Invalid experiment keyword",
				Detail:   fmt.Sprintf("Could not parse %q as an experiment keyword: %s.", kw, err.Error()),
				Subject:  expr.Range().Ptr(),
			})
		}
	}
	return ret, diags
}

func checkModuleExperiments(m *Module) hcl.Diagnostics {
	var diags hcl.Diagnostics

	// When we have current experiments, this is a good place to check that
	// the features in question can only be used when the experiments are
	// active. Return error diagnostics if a feature is being used without
	// opting in to the feature. For example:
	/*
		if !m.ActiveExperiments.Has(experiments.ResourceForEach) {
			for _, rc := range m.ManagedResources {
				if rc.ForEach != nil {
					diags = append(diags, &hcl.Diagnostic{
						Severity: hcl.DiagError,
						Summary:  "Resource for_each is experimental",
						Detail:   "This feature is currently an opt-in experiment, subject to change in future releases based on feedback.\n\nActivate the feature for this module by adding resource_for_each to the list of active experiments.",
						Subject:  rc.ForEach.Range().Ptr(),
					})
				}
			}
			for _, rc := range m.DataResources {
				if rc.ForEach != nil {
					diags = append(diags, &hcl.Diagnostic{
						Severity: hcl.DiagError,
						Summary:  "Resource for_each is experimental",
						Detail:   "This feature is currently an opt-in experiment, subject to change in future releases based on feedback.\n\nActivate the feature for this module by adding resource_for_each to the list of active experiments.",
						Subject:  rc.ForEach.Range().Ptr(),
					})
				}
			}
		}
	*/

	if !m.ActiveExperiments.Has(experiments.VariableValidation) {
		for _, vc := range m.Variables {
			if len(vc.Validations) != 0 {
				diags = diags.Append(&hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  "Custom variable validation is experimental",
					Detail:   "This feature is currently an opt-in experiment, subject to change in future releases based on feedback.\n\nActivate the feature for this module by adding variable_validation to the list of active experiments.",
					Subject:  vc.Validations[0].DeclRange.Ptr(),
				})
			}
		}
	}

	return diags
}
