// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package configs

import (
	"context"
	"errors"
	"fmt"

	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/hcl/v2/hclsyntax"
	"github.com/zclconf/go-cty/cty"

	"github.com/opentofu/opentofu/internal/addrs"
	"github.com/opentofu/opentofu/internal/getmodules"
)

// ModuleCall represents a "module" block in a module or file.
type ModuleCall struct {
	Name string

	Source        hcl.Expression
	SourceAddrRaw string
	SourceAddr    addrs.ModuleSource
	SourceSet     bool

	// Used when building the corresponding StaticModuleCall
	Variables StaticModuleVariables
	Workspace string

	Config hcl.Body

	VersionAttr *hcl.Attribute
	Version     VersionConstraint

	Count   hcl.Expression
	ForEach hcl.Expression
	Enabled hcl.Expression

	Providers []PassedProviderConfig

	DependsOn []hcl.Traversal

	DeclRange hcl.Range
}

func decodeModuleBlock(block *hcl.Block, override bool) (*ModuleCall, hcl.Diagnostics) {
	var diags hcl.Diagnostics

	mc := &ModuleCall{
		DeclRange: block.DefRange,
		Name:      block.Labels[0],
	}

	schema := moduleBlockSchema
	if override {
		schema = schemaForOverrides(schema)
	}

	content, remain, moreDiags := block.Body.PartialContent(schema)
	diags = append(diags, moreDiags...)
	mc.Config = remain

	if !hclsyntax.ValidIdentifier(mc.Name) {
		diags = append(diags, &hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Invalid module instance name",
			Detail:   badIdentifierDetail,
			Subject:  &block.LabelRanges[0],
		})
	}

	if attr, exists := content.Attributes["version"]; exists {
		mc.VersionAttr = attr
	}

	if attr, exists := content.Attributes["source"]; exists {
		mc.SourceSet = true
		mc.Source = attr.Expr
	}

	var countRng, forEachRng, enabledRng hcl.Range
	repetitionArgs := 0
	if attr, exists := content.Attributes["count"]; exists {
		mc.Count = attr.Expr
		countRng = attr.NameRange
		repetitionArgs++
	}

	if attr, exists := content.Attributes["for_each"]; exists {
		mc.ForEach = attr.Expr
		forEachRng = attr.NameRange
		repetitionArgs++
	}

	if attr, exists := content.Attributes["depends_on"]; exists {
		deps, depsDiags := decodeDependsOn(attr)
		diags = append(diags, depsDiags...)
		mc.DependsOn = append(mc.DependsOn, deps...)
	}

	if attr, exists := content.Attributes["providers"]; exists {
		providers, providerDiags := decodePassedProviderConfigs(attr)
		diags = append(diags, providerDiags...)
		mc.Providers = append(mc.Providers, providers...)
	}

	var seenLifecycle *hcl.Block
	var seenEscapeBlock *hcl.Block
	for _, block := range content.Blocks {
		switch block.Type {
		case "lifecycle":
			if seenLifecycle != nil {
				diags = append(diags, &hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  "Duplicate lifecycle block",
					Detail:   fmt.Sprintf("This resource already has a lifecycle block at %s.", seenLifecycle.DefRange),
					Subject:  &block.DefRange,
				})
				continue
			}
			seenLifecycle = block

			lcContent, lcDiags := block.Body.Content(moduleLifecycleBlockSchema)
			diags = append(diags, lcDiags...)

			if attr, exists := lcContent.Attributes["enabled"]; exists {
				mc.Enabled = attr.Expr
				enabledRng = attr.NameRange
				repetitionArgs++
			}
		case "_":
			if seenEscapeBlock != nil {
				diags = append(diags, &hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  "Duplicate escaping block",
					Detail: fmt.Sprintf(
						"The special block type \"_\" can be used to force particular arguments to be interpreted as module input variables rather than as meta-arguments, but each module block can have only one such block. The first escaping block was at %s.",
						seenEscapeBlock.DefRange,
					),
					Subject: &block.DefRange,
				})
				continue
			}
			seenEscapeBlock = block

			// When there's an escaping block its content merges with the
			// existing config we extracted earlier, so later decoding
			// will see a blend of both.
			mc.Config = hcl.MergeBodies([]hcl.Body{mc.Config, block.Body})

		default:
			// All of the other block types in our schema are reserved.
			diags = append(diags, &hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Reserved block type name in module block",
				Detail:   fmt.Sprintf("The block type name %q is reserved for use by OpenTofu in a future version.", block.Type),
				Subject:  &block.TypeRange,
			})
		}
	}

	if repetitionArgs >= 2 {
		complainRng, complainMsg := complainRngAndMsg(countRng, enabledRng, forEachRng)
		diags = append(diags, &hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  fmt.Sprintf(`Invalid combination of %s`, complainMsg),
			Detail:   fmt.Sprintf(`The %s meta-arguments are mutually-exclusive. Only one should be used to be explicit about the number of module instances to be created.`, complainMsg),
			Subject:  complainRng,
		})
	}

	return mc, diags
}

func (mc *ModuleCall) decodeStaticFields(ctx context.Context, eval *StaticEvaluator) hcl.Diagnostics {
	mc.Workspace = eval.call.workspace
	mc.decodeStaticVariables(ctx, eval)

	var diags hcl.Diagnostics
	diags = diags.Extend(mc.decodeStaticVersion(ctx, eval))
	// decodeStaticSource depends on mc.VersionAttr, so it must be called after decodeStaticVersion
	diags = diags.Extend(mc.decodeStaticSource(ctx, eval))
	return diags
}

func (mc *ModuleCall) decodeStaticSource(ctx context.Context, eval *StaticEvaluator) hcl.Diagnostics {
	if mc.Source == nil {
		// This is an invalid module.  We already have error handling that has more context and can produce better errors in this
		// scenario.  Follow the trail of mc.SourceAddr -> req.SourceAddr through the command package.
		return nil
	}

	// Decode source field
	diags := eval.DecodeExpression(ctx, mc.Source, StaticIdentifier{Module: eval.call.addr, Subject: fmt.Sprintf("module.%s.source", mc.Name), DeclRange: mc.Source.Range()}, &mc.SourceAddrRaw)
	if !diags.HasErrors() {
		// NOTE: This code was originally executed as part of decodeModuleBlock and is now deferred until we have the config merged and static context built
		var err error
		if mc.VersionAttr != nil {
			mc.SourceAddr, err = addrs.ParseModuleSourceRegistry(mc.SourceAddrRaw)
		} else {
			mc.SourceAddr, err = addrs.ParseModuleSource(mc.SourceAddrRaw)
		}
		if err != nil {
			// NOTE: We leave SourceAddr as nil for any situation where the
			// source attribute is invalid, so any code which tries to carefully
			// use the partial result of a failed config decode must be
			// resilient to that.
			mc.SourceAddr = nil

			// NOTE: In practice it's actually very unlikely to end up here,
			// because our source address parser can turn just about any string
			// into some sort of remote package address, and so for most errors
			// we'll detect them only during module installation. There are
			// still a _few_ purely-syntax errors we can catch at parsing time,
			// though, mostly related to remote package sub-paths and local
			// paths.
			var pathErr *getmodules.MaybeRelativePathErr
			if errors.As(err, &pathErr) {
				diags = append(diags, &hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  "Invalid module source address",
					Detail: fmt.Sprintf(
						"OpenTofu failed to determine your intended installation method for remote module package %q.\n\nIf you intended this as a path relative to the current module, use \"./%s\" instead. The \"./\" prefix indicates that the address is a relative filesystem path.",
						pathErr.Addr, pathErr.Addr,
					),
					Subject: mc.Source.Range().Ptr(),
				})
			} else {
				if mc.VersionAttr != nil {
					// In this case we'll include some extra context that
					// we assumed a registry source address due to the
					// version argument.
					diags = append(diags, &hcl.Diagnostic{
						Severity: hcl.DiagError,
						Summary:  "Invalid registry module source address",
						Detail:   fmt.Sprintf("Failed to parse module registry address: %s.\n\nOpenTofu assumed that you intended a module registry source address because you also set the argument \"version\", which applies only to registry modules.", err),
						Subject:  mc.Source.Range().Ptr(),
					})
				} else {
					diags = append(diags, &hcl.Diagnostic{
						Severity: hcl.DiagError,
						Summary:  "Invalid module source address",
						Detail:   fmt.Sprintf("Failed to parse module source address: %s.", err),
						Subject:  mc.Source.Range().Ptr(),
					})
				}
			}
		}
	}

	return diags
}

func (mc *ModuleCall) decodeStaticVersion(ctx context.Context, eval *StaticEvaluator) hcl.Diagnostics {
	var diags hcl.Diagnostics

	if mc.VersionAttr == nil {
		return diags
	}

	val, valDiags := eval.Evaluate(ctx, mc.VersionAttr.Expr, StaticIdentifier{
		Module:    eval.call.addr,
		Subject:   fmt.Sprintf("module.%s.version", mc.Name),
		DeclRange: mc.VersionAttr.Range,
	})
	diags = diags.Extend(valDiags)
	if diags.HasErrors() {
		return diags
	}

	// If the version evaluates to null, treat it as if no version was specified
	if val.IsNull() {
		mc.VersionAttr = nil
		return diags
	}

	var verDiags hcl.Diagnostics
	mc.Version, verDiags = decodeVersionConstraintValue(mc.VersionAttr, val)
	return diags.Extend(verDiags)
}

func (mc *ModuleCall) decodeStaticVariables(ctx context.Context, eval *StaticEvaluator) {
	attr, _ := mc.Config.JustAttributes()

	mc.Variables = func(variable *Variable) (cty.Value, hcl.Diagnostics) {
		v, ok := attr[variable.Name]
		if !ok {
			if variable.Required() {
				return cty.NilVal, hcl.Diagnostics{&hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  "Missing required variable in module call",
					Subject:  mc.Config.MissingItemRange().Ptr(),
				}}
			}
			return variable.Default, nil
		}

		ident := StaticIdentifier{
			Module:    eval.call.addr.Child(mc.Name),
			Subject:   fmt.Sprintf("var.%s", variable.Name),
			DeclRange: v.Range,
		}
		return eval.Evaluate(ctx, v.Expr, ident)
	}
}

// EntersNewPackage returns true if this call is to an external module, either
// directly via a remote source address or indirectly via a registry source
// address.
//
// Other behaviors in OpenTofu may treat package crossings as a special
// situation, because that indicates that the caller and callee can change
// independently of one another and thus we should disallow using any features
// where the caller assumes anything about the callee other than its input
// variables, required provider configurations, and output values.
func (mc *ModuleCall) EntersNewPackage() bool {
	return moduleSourceAddrEntersNewPackage(mc.SourceAddr)
}

// PassedProviderConfig represents a provider config explicitly passed down to
// a child module, possibly giving it a new local address in the process.
type PassedProviderConfig struct {
	InChild  *ProviderConfigRef
	InParent *ProviderConfigRef
}

func decodePassedProviderConfigs(attr *hcl.Attribute) ([]PassedProviderConfig, hcl.Diagnostics) {
	var diags hcl.Diagnostics
	var providers []PassedProviderConfig

	seen := make(map[string]hcl.Range)
	pairs, pDiags := hcl.ExprMap(attr.Expr)
	diags = append(diags, pDiags...)
	for _, pair := range pairs {
		key, keyDiags := decodeProviderConfigRef(pair.Key, "providers")
		diags = append(diags, keyDiags...)
		value, valueDiags := decodeProviderConfigRef(pair.Value, "providers")
		diags = append(diags, valueDiags...)
		if keyDiags.HasErrors() || valueDiags.HasErrors() {
			continue
		}

		matchKey := key.String()
		if prev, exists := seen[matchKey]; exists {
			diags = append(diags, &hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Duplicate provider address",
				Detail:   fmt.Sprintf("A provider configuration was already passed to %s at %s. Each child provider configuration can be assigned only once.", matchKey, prev),
				Subject:  pair.Value.Range().Ptr(),
			})
			continue
		}

		rng := hcl.RangeBetween(pair.Key.Range(), pair.Value.Range())
		seen[matchKey] = rng
		providers = append(providers, PassedProviderConfig{
			InChild:  key,
			InParent: value,
		})
	}
	return providers, diags
}

var moduleBlockSchema = &hcl.BodySchema{
	Attributes: []hcl.AttributeSchema{
		{
			Name:     "source",
			Required: true,
		},
		{
			Name: "version",
		},
		{
			Name: "count",
		},
		{
			Name: "for_each",
		},
		{
			Name: "depends_on",
		},
		{
			Name: "providers",
		},
	},
	Blocks: []hcl.BlockHeaderSchema{
		{Type: "_"}, // meta-argument escaping block

		// These are all reserved for future use.
		{Type: "lifecycle"},
		{Type: "locals"},
		{Type: "provider", LabelNames: []string{"type"}},
	},
}

var moduleLifecycleBlockSchema = &hcl.BodySchema{
	Attributes: []hcl.AttributeSchema{
		{
			Name: "enabled",
		},
	},
}

func moduleSourceAddrEntersNewPackage(addr addrs.ModuleSource) bool {
	switch addr.(type) {
	case nil:
		// There are only two situations where we should get here:
		// - We've been asked about the source address of the root module,
		//   which is always nil.
		// - We've been asked about a ModuleCall that is part of the partial
		//   result of a failed decode.
		// The root module exists outside of all module packages, so we'll
		// just return false for that case. For the error case it doesn't
		// really matter what we return as long as we don't panic, because
		// we only make a best-effort to allow careful inspection of objects
		// representing invalid configuration.
		return false
	case addrs.ModuleSourceLocal:
		// Local source addresses are the only address type that remains within
		// the same package.
		return false
	default:
		// All other address types enter a new package.
		return true
	}
}
