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

package tofu

import (
	"context"
	"fmt"
	"sort"

	"github.com/hashicorp/hcl/v2"
	"github.com/opentofu/opentofu/internal/addrs"
	"github.com/opentofu/opentofu/internal/configs"
	"github.com/opentofu/opentofu/internal/didyoumean"
	"github.com/opentofu/opentofu/internal/tfdiags"
)

// StaticValidateReferences checks the given references against schemas and
// other statically-checkable rules, producing error diagnostics if any
// problems are found.
//
// If this method returns errors for a particular reference then evaluating
// that reference is likely to generate a very similar error, so callers should
// not run this method and then also evaluate the source expression(s) and
// merge the two sets of diagnostics together, since this will result in
// confusing redundant errors.
//
// This method can find more errors than can be found by evaluating an
// expression with a partially-populated scope, since it checks the referenced
// names directly against the schema rather than relying on evaluation errors.
//
// The result may include warning diagnostics if, for example, deprecated
// features are referenced.
func (d *evaluationStateData) StaticValidateReferences(ctx context.Context, refs []*addrs.Reference, self addrs.Referenceable, source addrs.Referenceable) tfdiags.Diagnostics {
	var diags tfdiags.Diagnostics
	for _, ref := range refs {
		moreDiags := d.staticValidateReference(ctx, ref, self, source)
		diags = diags.Append(moreDiags)
	}
	return diags
}

func (d *evaluationStateData) staticValidateReference(ctx context.Context, ref *addrs.Reference, self addrs.Referenceable, source addrs.Referenceable) tfdiags.Diagnostics {
	modCfg := d.Evaluator.Config.DescendentForInstance(d.ModulePath)
	if modCfg == nil {
		// This is a bug in the caller rather than a problem with the
		// reference, but rather than crashing out here in an unhelpful way
		// we'll just ignore it and trust a different layer to catch it.
		return nil
	}

	if ref.Subject == addrs.Self {
		// The "self" address is a special alias for the address given as
		// our self parameter here, if present.
		if self == nil {
			var diags tfdiags.Diagnostics
			diags = diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  `Invalid "self" reference`,
				// This detail message mentions some current practice that
				// this codepath doesn't really "know about". If the "self"
				// object starts being supported in more contexts later then
				// we'll need to adjust this message.
				Detail:  `The "self" object is not available in this context. This object can be used only in resource provisioner, connection, and postcondition blocks.`,
				Subject: ref.SourceRange.ToHCL().Ptr(),
			})
			return diags
		}

		synthRef := *ref // shallow copy
		synthRef.Subject = self
		ref = &synthRef
	}

	switch addr := ref.Subject.(type) {

	// For static validation we validate both resource and resource instance references the same way.
	// We mostly disregard the index, though we do some simple validation of
	// its _presence_ in staticValidateSingleResourceReference and
	// staticValidateMultiResourceReference respectively.
	case addrs.Resource:
		var diags tfdiags.Diagnostics
		diags = diags.Append(d.staticValidateSingleResourceReference(ctx, modCfg, addr, ref.Remaining, ref.SourceRange))
		diags = diags.Append(d.staticValidateResourceReference(ctx, modCfg, addr, source, ref.Remaining, ref.SourceRange))
		return diags
	case addrs.ResourceInstance:
		var diags tfdiags.Diagnostics
		diags = diags.Append(d.staticValidateMultiResourceReference(ctx, modCfg, addr, ref.Remaining, ref.SourceRange))
		diags = diags.Append(d.staticValidateResourceReference(ctx, modCfg, addr.ContainingResource(), source, ref.Remaining, ref.SourceRange))
		return diags

	// We also handle all module call references the same way, disregarding index.
	case addrs.ModuleCall:
		return d.staticValidateModuleCallReference(modCfg, addr, ref.Remaining, ref.SourceRange)
	case addrs.ModuleCallInstance:
		return d.staticValidateModuleCallReference(modCfg, addr.Call, ref.Remaining, ref.SourceRange)
	case addrs.ModuleCallInstanceOutput:
		// This one is a funny one because we will take the output name referenced
		// and use it to fake up a "remaining" that would make sense for the
		// module call itself, rather than for the specific output, and then
		// we can just re-use our static module call validation logic.
		remain := make(hcl.Traversal, len(ref.Remaining)+1)
		copy(remain[1:], ref.Remaining)
		remain[0] = hcl.TraverseAttr{
			Name: addr.Name,

			// Using the whole reference as the source range here doesn't exactly
			// match how HCL would normally generate an attribute traversal,
			// but is close enough for our purposes.
			SrcRange: ref.SourceRange.ToHCL(),
		}
		return d.staticValidateModuleCallReference(modCfg, addr.Call.Call, remain, ref.SourceRange)

	default:
		// Anything else we'll just permit through without any static validation
		// and let it be caught during dynamic evaluation, in evaluate.go .
		return nil
	}
}

func (d *evaluationStateData) staticValidateSingleResourceReference(_ context.Context, modCfg *configs.Config, addr addrs.Resource, remain hcl.Traversal, rng tfdiags.SourceRange) tfdiags.Diagnostics {
	// If we have at least one step in "remain" and this resource has
	// "count" set then we know for sure this in invalid because we have
	// something like:
	//     aws_instance.foo.bar
	// ...when we really need
	//     aws_instance.foo[count.index].bar

	// It is _not_ safe to do this check when remain is empty, because that
	// would also match aws_instance.foo[count.index].bar due to `count.index`
	// not being statically-resolvable as part of a reference, and match
	// direct references to the whole aws_instance.foo tuple.
	if len(remain) == 0 {
		return nil
	}

	var diags tfdiags.Diagnostics

	cfg := modCfg.Module.ResourceByAddr(addr)
	if cfg == nil {
		// We'll just bail out here and catch this in our subsequent call to
		// staticValidateResourceReference, then.
		return diags
	}

	if cfg.Count != nil {
		diags = diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  `Missing resource instance key`,
			Detail:   fmt.Sprintf("Because %s has \"count\" set, its attributes must be accessed on specific instances.\n\nFor example, to correlate with indices of a referring resource, use:\n    %s[count.index]", addr, addr),
			Subject:  rng.ToHCL().Ptr(),
		})
	}
	if cfg.ForEach != nil {
		diags = diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  `Missing resource instance key`,
			Detail:   fmt.Sprintf("Because %s has \"for_each\" set, its attributes must be accessed on specific instances.\n\nFor example, to correlate with indices of a referring resource, use:\n    %s[each.key]", addr, addr),
			Subject:  rng.ToHCL().Ptr(),
		})
	}

	return diags
}

func (d *evaluationStateData) staticValidateMultiResourceReference(ctx context.Context, modCfg *configs.Config, addr addrs.ResourceInstance, remain hcl.Traversal, rng tfdiags.SourceRange) tfdiags.Diagnostics {
	var diags tfdiags.Diagnostics

	cfg := modCfg.Module.ResourceByAddr(addr.ContainingResource())
	if cfg == nil {
		// We'll just bail out here and catch this in our subsequent call to
		// staticValidateResourceReference, then.
		return diags
	}

	if addr.Key == addrs.NoKey {
		// This is a different path into staticValidateSingleResourceReference
		return d.staticValidateSingleResourceReference(ctx, modCfg, addr.ContainingResource(), remain, rng)
	} else {
		if cfg.Count == nil && cfg.ForEach == nil {
			diags = diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  `Unexpected resource instance key`,
				Detail:   fmt.Sprintf(`Because %s does not have "count" or "for_each" set, references to it must not include an index key. Remove the bracketed index to refer to the single instance of this resource.`, addr.ContainingResource()),
				Subject:  rng.ToHCL().Ptr(),
			})
		}
	}

	return diags
}

func (d *evaluationStateData) staticValidateResourceReference(ctx context.Context, modCfg *configs.Config, addr addrs.Resource, source addrs.Referenceable, remain hcl.Traversal, rng tfdiags.SourceRange) tfdiags.Diagnostics {
	var diags tfdiags.Diagnostics

	var modeAdjective string
	switch addr.Mode {
	case addrs.ManagedResourceMode:
		modeAdjective = "managed"
	case addrs.DataResourceMode:
		modeAdjective = "data"
	case addrs.EphemeralResourceMode:
		modeAdjective = "ephemeral"
	default:
		// should never happen
		modeAdjective = "<invalid-mode>"
	}

	cfg := modCfg.Module.ResourceByAddr(addr)
	if cfg == nil {
		// A common mistake is omitting the "data." (or "ephemeral.") prefix when trying to refer
		// to a data (or ephemeral) resource, so we'll add a special hint for that.
		suggestion := candidateSuggestion(addr, modCfg)

		diags = diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  `Reference to undeclared resource`,
			Detail:   fmt.Sprintf(`There is no %s resource %q %q definition in %s.%s`, modeAdjective, addr.Type, addr.Name, moduleConfigDisplayAddr(modCfg.Path), suggestion),
			Subject:  rng.ToHCL().Ptr(),
		})
		return diags
	}

	if cfg.Container != nil && (source == nil || !cfg.Container.Accessible(source)) {
		diags = diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  `Reference to scoped resource`,
			Detail:   fmt.Sprintf(`The referenced %s resource %q %q is not available from this context.`, modeAdjective, addr.Type, addr.Name),
			Subject:  rng.ToHCL().Ptr(),
		})
	}

	// TODO: Plugin a suitable context.Context through to here.
	providerFqn := modCfg.Module.ProviderForLocalConfig(cfg.ProviderConfigAddr())
	schema, _, moreDiags := d.Evaluator.Plugins.ResourceTypeSchema(ctx, providerFqn, addr.Mode, addr.Type)
	diags = diags.Append(moreDiags)
	if moreDiags.HasErrors() {
		// Prior validation should've taken care of a schema lookup error,
		// so we should never get here but we'll handle it here anyway for
		// robustness.
		diags = diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  `Failed provider schema lookup`,
			Detail:   fmt.Sprintf(`Couldn't load schema for %s resource type %q in %s: %s.`, modeAdjective, addr.Type, providerFqn.String(), diags.Err()),
			Subject:  rng.ToHCL().Ptr(),
		})
	}

	if schema == nil {
		// Prior validation should've taken care of a resource block with an
		// unsupported type, so we should never get here but we'll handle it
		// here anyway for robustness.
		diags = diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  `Invalid resource type`,
			Detail:   fmt.Sprintf(`The %s resource type %q is not supported by provider %q.`, modeAdjective, addr.Type, providerFqn.String()),
			Subject:  rng.ToHCL().Ptr(),
		})
		return diags
	}

	// As a special case we'll detect attempts to access an attribute called
	// "count" and produce a special error for it, since versions of Terraform
	// prior to v0.12 offered this as a weird special case that we can no
	// longer support.
	if len(remain) > 0 {
		if step, ok := remain[0].(hcl.TraverseAttr); ok && step.Name == "count" {
			diags = diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  `Invalid resource count attribute`,
				Detail:   fmt.Sprintf(`The special "count" attribute is no longer supported after OpenTofu v0.12. Instead, use length(%s) to count resource instances.`, addr),
				Subject:  rng.ToHCL().Ptr(),
			})
			return diags
		}
	}

	// If we got this far then we'll try to validate the remaining traversal
	// steps against our schema.
	moreDiags = schema.StaticValidateTraversal(remain)
	diags = diags.Append(moreDiags)

	return diags
}

// candidateSuggestion is trying to return a close candidate by simply trying to find a different type of resource
// with the same type and name to suggest. We are trying to do so because a common mistake is omitting the "data." (or "ephemeral.")
// prefix when trying to refer to a data (or ephemeral) resource, so we'll add a special hint for that.
func candidateSuggestion(addr addrs.Resource, cfg *configs.Config) interface{} {
	candidateAddr := addr // not a pointer, so this is a copy
	tpl := "\n\nDid you mean the %s resource %s?"
	filterOnOtherModes := func(targetModes []addrs.ResourceMode) *configs.Resource {
		for _, candidateMode := range targetModes {
			candidateAddr.Mode = candidateMode
			if b := cfg.Module.ResourceByAddr(candidateAddr); b != nil {
				return b
			}
		}
		return nil
	}
	switch addr.Mode {
	case addrs.ManagedResourceMode:
		if candidateCfg := filterOnOtherModes([]addrs.ResourceMode{addrs.DataResourceMode, addrs.EphemeralResourceMode}); candidateCfg != nil {
			return fmt.Sprintf(tpl, addrs.ResourceModeBlockName(candidateAddr.Mode), candidateAddr)
		}
	case addrs.DataResourceMode:
		if candidateCfg := filterOnOtherModes([]addrs.ResourceMode{addrs.ManagedResourceMode, addrs.EphemeralResourceMode}); candidateCfg != nil {
			return fmt.Sprintf(tpl, addrs.ResourceModeBlockName(candidateAddr.Mode), candidateAddr)
		}
	case addrs.EphemeralResourceMode:
		if candidateCfg := filterOnOtherModes([]addrs.ResourceMode{addrs.ManagedResourceMode, addrs.DataResourceMode}); candidateCfg != nil {
			return fmt.Sprintf(tpl, addrs.ResourceModeBlockName(candidateAddr.Mode), candidateAddr)
		}
	}

	return ""
}

func (d *evaluationStateData) staticValidateModuleCallReference(modCfg *configs.Config, addr addrs.ModuleCall, remain hcl.Traversal, rng tfdiags.SourceRange) tfdiags.Diagnostics {
	var diags tfdiags.Diagnostics

	// For now, our focus here is just in testing that the referenced module
	// call exists. All other validation is deferred until evaluation time.
	_, exists := modCfg.Module.ModuleCalls[addr.Name]
	if !exists {
		var suggestions []string
		for name := range modCfg.Module.ModuleCalls {
			suggestions = append(suggestions, name)
		}
		sort.Strings(suggestions)
		suggestion := didyoumean.NameSuggestion(addr.Name, suggestions)
		if suggestion != "" {
			suggestion = fmt.Sprintf(" Did you mean %q?", suggestion)
		}

		diags = diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  `Reference to undeclared module`,
			Detail:   fmt.Sprintf(`No module call named %q is declared in %s.%s`, addr.Name, moduleConfigDisplayAddr(modCfg.Path), suggestion),
			Subject:  rng.ToHCL().Ptr(),
		})
		return diags
	}

	return diags
}

// moduleConfigDisplayAddr returns a string describing the given module
// address that is appropriate for returning to users in situations where the
// root module is possible. Specifically, it returns "the root module" if the
// root module instance is given, or a string representation of the module
// address otherwise.
func moduleConfigDisplayAddr(addr addrs.Module) string {
	switch {
	case addr.IsRoot():
		return "the root module"
	default:
		return addr.String()
	}
}
