// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package stackconfig

import (
	"fmt"

	"github.com/apparentlymart/go-versions/versions/constraints"
	"github.com/hashicorp/go-slug/sourceaddrs"
	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/hcl/v2/gohcl"
	"github.com/hashicorp/hcl/v2/hclsyntax"
	"github.com/hashicorp/terraform/internal/addrs"
	"github.com/hashicorp/terraform/internal/tfdiags"
)

// Component represents the declaration of a single component within a
// particular [Stack].
//
// Components are the most important object in a stack configuration, just as
// resources are the most important object in a Terraform module: each one
// refers to a Terraform module that describes the infrastructure that the
// component is "made of".
type Component struct {
	Name string

	SourceAddr                               sourceaddrs.Source
	VersionConstraints                       constraints.IntersectionSpec
	SourceAddrRange, VersionConstraintsRange tfdiags.SourceRange

	// FinalSourceAddr is populated only when a configuration is loaded
	// through [LoadConfigDir], and in that case contains the finalized
	// address produced by resolving the SourceAddr field relative to
	// the address of the file where the component was declared. This
	// is the address to use if you intend to load the component's
	// root module from a source bundle.
	//
	// If this Component was created through one of the narrower configuration
	// loading functions, such as [LoadSingleStackConfig] or [ParseFileSource],
	// then this field will be nil and it won't be possible to determine the
	// finalized source location for the root module.
	FinalSourceAddr sourceaddrs.FinalSource

	ForEach hcl.Expression

	// Inputs is an expression that should produce a value that can convert
	// to an object type derived from the component's input variable
	// declarations, and whose attribute values will then be used to populate
	// those input variables.
	Inputs hcl.Expression

	// ProviderConfigs describes the mapping between the static provider
	// configuration slots declared in the component's root module and the
	// dynamic provider configuration objects in scope in the calling
	// stack configuration.
	//
	// This map deals with the slight schism between the stacks language's
	// treatment of provider configurations as regular values of a special
	// data type vs. the main Terraform language's treatment of provider
	// configurations as something special passed out of band from the
	// input variables. The overall structure and the map keys are fixed
	// statically during decoding, but the final provider configuration objects
	// are determined only at runtime by normal expression evaluation.
	//
	// The keys of this map refer to provider configuration slots inside
	// the module being called, but use the local names defined in the
	// calling stack configuration. The stacks language runtime will
	// translate the caller's local names into the callee's declared provider
	// configurations by using the stack configuration's table of local
	// provider names.
	ProviderConfigs map[addrs.LocalProviderConfig]hcl.Expression

	DeclRange tfdiags.SourceRange
}

func decodeComponentBlock(block *hcl.Block) (*Component, tfdiags.Diagnostics) {
	var diags tfdiags.Diagnostics
	ret := &Component{
		Name:      block.Labels[0],
		DeclRange: tfdiags.SourceRangeFromHCL(block.DefRange),
	}
	if !hclsyntax.ValidIdentifier(ret.Name) {
		diags = diags.Append(invalidNameDiagnostic(
			"Invalid component name",
			block.LabelRanges[0],
		))
		return nil, diags
	}

	content, hclDiags := block.Body.Content(componentBlockSchema)
	diags = diags.Append(hclDiags)
	if hclDiags.HasErrors() {
		return nil, diags
	}

	sourceAddr, versionConstraints, moreDiags := decodeSourceAddrArguments(
		content.Attributes["source"],
		content.Attributes["version"],
	)
	diags = diags.Append(moreDiags)
	if moreDiags.HasErrors() {
		return nil, diags
	}

	ret.SourceAddr = sourceAddr
	ret.VersionConstraints = versionConstraints
	ret.SourceAddrRange = tfdiags.SourceRangeFromHCL(content.Attributes["source"].Range)
	if content.Attributes["version"] != nil {
		ret.VersionConstraintsRange = tfdiags.SourceRangeFromHCL(content.Attributes["version"].Range)
	}
	// Now that we've populated the mandatory source location fields we can
	// safely return a partial ret if we encounter any further errors, as
	// long as we leave the other fields either unset or in some other
	// reasonable state for careful partial analysis.

	if attr, ok := content.Attributes["for_each"]; ok {
		ret.ForEach = attr.Expr
	}
	if attr, ok := content.Attributes["inputs"]; ok {
		ret.Inputs = attr.Expr
	}
	if attr, ok := content.Attributes["providers"]; ok {
		// This particular argument has some enforced static structure because
		// it's populating an inflexible part of Terraform Core's input.
		// This argument, if present, must always be an object constructor
		// whose attributes are Terraform Core-style provider configuration
		// addresses, but whose values are just arbitrary expressions for now
		// and will be resolved into specific provider configuration addresses
		// dynamically at runtime.
		pairs, hclDiags := hcl.ExprMap(attr.Expr)
		diags = diags.Append(hclDiags)
		if !hclDiags.HasErrors() {
			ret.ProviderConfigs = make(map[addrs.LocalProviderConfig]hcl.Expression, len(pairs))
			for _, pair := range pairs {
				insideAddrExpr := pair.Key
				outsideAddrExpr := pair.Value

				traversal, hclDiags := hcl.AbsTraversalForExpr(insideAddrExpr)
				diags = diags.Append(hclDiags)
				if hclDiags.HasErrors() {
					continue
				}

				if len(traversal) < 1 || len(traversal) > 2 {
					diags = diags.Append(&hcl.Diagnostic{
						Severity: hcl.DiagError,
						Summary:  "Invalid provider configuration reference",
						Detail:   "Each item in the providers argument requires a provider local name, optionally followed by a period and then a configuration alias, matching one of the provider configuration import slots declared by the component's root module.",
						Subject:  insideAddrExpr.Range().Ptr(),
					})
					continue
				}

				localName := traversal.RootName()
				if !hclsyntax.ValidIdentifier(localName) {
					diags = diags.Append(invalidNameDiagnostic(
						"Invalid provider local name",
						traversal[0].SourceRange(),
					))
					continue
				}

				var alias string
				if len(traversal) > 1 {
					aliasStep, ok := traversal[1].(hcl.TraverseAttr)
					if !ok {
						diags = diags.Append(&hcl.Diagnostic{
							Severity: hcl.DiagError,
							Summary:  "Invalid provider configuration reference",
							Detail:   "Provider local name must either stand alone or be followed by a period and then a configuration alias.",
							Subject:  traversal[1].SourceRange().Ptr(),
						})
						continue
					}
					alias = aliasStep.Name
				}

				addr := addrs.LocalProviderConfig{
					LocalName: localName,
					Alias:     alias,
				}
				if existing, exists := ret.ProviderConfigs[addr]; exists {
					diags = diags.Append(&hcl.Diagnostic{
						Severity: hcl.DiagError,
						Summary:  "Duplicate provider configuration assignment",
						Detail: fmt.Sprintf(
							"A provider configuration for %s was already assigned at %s.",
							addr.StringCompact(), existing.Range().Ptr(),
						),
						Subject: outsideAddrExpr.Range().Ptr(),
					})
					continue
				} else {
					ret.ProviderConfigs[addr] = outsideAddrExpr
				}
			}
		}
	}

	return ret, diags
}

func decodeSourceAddrArguments(sourceAttr, versionAttr *hcl.Attribute) (sourceaddrs.Source, constraints.IntersectionSpec, tfdiags.Diagnostics) {
	var diags tfdiags.Diagnostics

	var sourceStr string
	hclDiags := gohcl.DecodeExpression(sourceAttr.Expr, nil, &sourceStr)
	diags = diags.Append(hclDiags)
	if hclDiags.HasErrors() {
		return nil, nil, diags
	}

	sourceAddr, err := sourceaddrs.ParseSource(sourceStr)
	if err != nil {
		diags = diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Invalid source address",
			Detail: fmt.Sprintf(
				"Cannot parse %q as a source address: %s.",
				sourceStr, err,
			),
			Subject: sourceAttr.Expr.Range().Ptr(),
		})
		return nil, nil, diags
	}

	var versionConstraints constraints.IntersectionSpec
	if sourceAddr.SupportsVersionConstraints() {
		if versionAttr == nil {
			diags = diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Missing required version constraints",
				Detail:   "The specified source address requires version constraints specified in a separate \"version\" argument.",
				Subject:  sourceAttr.Expr.Range().Ptr(),
			})
			return nil, nil, diags
		}
		var versionStr string
		hclDiags := gohcl.DecodeExpression(versionAttr.Expr, nil, &versionStr)
		diags = diags.Append(hclDiags)
		if hclDiags.HasErrors() {
			return nil, nil, diags
		}
		versionConstraints, err = constraints.ParseRubyStyleMulti(versionStr)
		if err != nil {
			diags = diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Invalid version constraints",
				Detail: fmt.Sprintf(
					"Cannot parse %q as source package version constraints: %s.",
					versionStr, err,
				),
				Subject: versionAttr.Expr.Range().Ptr(),
			})
			return nil, nil, diags
		}
	} else {
		if versionAttr != nil {
			diags = diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Unsupported version constraints",
				Detail:   "The specified source address does not support version constraints.",
				Subject:  versionAttr.Range.Ptr(),
			})
			return nil, nil, diags
		}
	}

	return sourceAddr, versionConstraints, diags
}

var componentBlockSchema = &hcl.BodySchema{
	Attributes: []hcl.AttributeSchema{
		{Name: "source", Required: true},
		{Name: "version", Required: false},
		{Name: "for_each", Required: false},
		{Name: "inputs", Required: false},
		{Name: "providers", Required: false},
	},
}
