// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1

package configs

import (
	"fmt"
	"slices"

	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/hcl/v2/hclsyntax"

	"github.com/hashicorp/terraform/internal/addrs"
	"github.com/hashicorp/terraform/internal/lang/langrefs"
	"github.com/hashicorp/terraform/internal/tfdiags"
)

func invalidActionDiag(subj *hcl.Range) *hcl.Diagnostic {
	return &hcl.Diagnostic{
		Severity: hcl.DiagError,
		Summary:  `Invalid action argument inside action_triggers`,
		Detail:   `action_triggers.actions must only refer to actions in the current module.`,
		Subject:  subj,
	}
}

// Action represents an "action" block inside a configuration
type Action struct {
	Name    string
	Type    string
	Config  hcl.Body
	Count   hcl.Expression
	ForEach hcl.Expression

	ProviderConfigRef *ProviderConfigRef
	Provider          addrs.Provider

	DeclRange hcl.Range
	TypeRange hcl.Range
}

// ActionTrigger represents a configured "action_trigger" inside the lifecycle
// block of a managed resource.
type ActionTrigger struct {
	Condition hcl.Expression
	Events    []ActionTriggerEvent
	Actions   []ActionRef // References to actions

	DeclRange hcl.Range
}

// ActionTriggerEvent is an enum for valid values for events for action
// triggers.
type ActionTriggerEvent int

//go:generate go tool golang.org/x/tools/cmd/stringer -type ActionTriggerEvent

const (
	Unknown ActionTriggerEvent = iota
	BeforeCreate
	AfterCreate
	BeforeUpdate
	AfterUpdate
	BeforeDestroy
	AfterDestroy
	Invoke
)

// ActionRef represents a reference to a configured Action
type ActionRef struct {
	Expr  hcl.Expression
	Range hcl.Range
}

func decodeActionTriggerBlock(block *hcl.Block) (*ActionTrigger, hcl.Diagnostics) {
	var diags hcl.Diagnostics
	a := &ActionTrigger{
		Events:    []ActionTriggerEvent{},
		Actions:   []ActionRef{},
		Condition: nil,
	}

	content, bodyDiags := block.Body.Content(actionTriggerSchema)
	diags = append(diags, bodyDiags...)

	var refs []*addrs.Reference
	var refDiags tfdiags.Diagnostics
	if attr, exists := content.Attributes["condition"]; exists {
		a.Condition = attr.Expr

		refs, refDiags = langrefs.ReferencesInExpr(addrs.ParseRef, attr.Expr)
		diags = append(diags, refDiags.ToHCL()...)

		for _, ref := range refs {
			if ref.Subject == addrs.Self {
				diags = diags.Append(&hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  "Self reference not allowed",
					Detail:   `The condition expression cannot reference "self".`,
					Subject:  attr.Expr.Range().Ptr(),
				})
			}
		}
	}

	if attr, exists := content.Attributes["events"]; exists {
		exprs, ediags := hcl.ExprList(attr.Expr)
		diags = append(diags, ediags...)

		events := []ActionTriggerEvent{}
		containsBefore := false

		for _, expr := range exprs {
			var event ActionTriggerEvent
			switch hcl.ExprAsKeyword(expr) {
			case "before_create":
				event = BeforeCreate
				containsBefore = true
			case "after_create":
				event = AfterCreate
			case "before_update":
				event = BeforeUpdate
				containsBefore = true
			case "after_update":
				event = AfterUpdate
			default:
				diags = append(diags, &hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  fmt.Sprintf("Invalid \"event\" value %s", hcl.ExprAsKeyword(expr)),
					Detail:   "The \"event\" argument supports the following values: before_create, after_create, before_update, after_update.",
					Subject:  expr.Range().Ptr(),
				})
				continue
			}

			// Check for duplicate events
			if slices.Contains(events, event) {
				diags = append(diags, &hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  fmt.Sprintf("Duplicate %q event", hcl.ExprAsKeyword(expr)),
					Detail:   "The event is already defined in this action_trigger block.",
					Subject:  expr.Range().Ptr(),
				})
				continue
			}

			// Check that there aren't any before_ events using self, count or for_each in the condition
			if containsBefore && refs != nil { // if refs isn't empty, there was a condition
				for _, ref := range refs {
					if _, ok := ref.Subject.(addrs.CountAttr); ok {
						diags = diags.Append(&hcl.Diagnostic{
							Severity: hcl.DiagError,
							Summary:  "Count reference not allowed",
							Detail:   `The condition expression cannot reference "count" if the action is run before the resource is applied.`,
							Subject:  a.Condition.Range().Ptr(),
						})
					}

					if _, ok := ref.Subject.(addrs.ForEachAttr); ok {
						diags = diags.Append(&hcl.Diagnostic{
							Severity: hcl.DiagError,
							Summary:  "Each reference not allowed",
							Detail:   `The condition expression cannot reference "each" if the action is run before the resource is applied.`,
							Subject:  a.Condition.Range().Ptr(),
						})
					}
				}
			}

			events = append(events, event)
		}

		a.Events = events
	}

	if attr, exists := content.Attributes["actions"]; exists {
		actionRefs, ediags := decodeActionTriggerRef(attr.Expr)
		diags = append(diags, ediags...)
		a.Actions = actionRefs
	}

	if len(a.Actions) == 0 {
		diags = append(diags, &hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "No actions specified",
			Detail:   "At least one action must be specified for an action_trigger.",
			Subject:  block.DefRange.Ptr(),
		})
	}

	if len(a.Events) == 0 {
		diags = append(diags, &hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "No events specified",
			Detail:   "At least one event must be specified for an action_trigger.",
			Subject:  block.DefRange.Ptr(),
		})
	}
	return a, diags
}

func decodeActionBlock(block *hcl.Block) (*Action, hcl.Diagnostics) {
	var diags hcl.Diagnostics
	a := &Action{
		Type:      block.Labels[0],
		Name:      block.Labels[1],
		DeclRange: block.DefRange,
		TypeRange: block.LabelRanges[0],
	}

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

	content, moreDiags := block.Body.Content(actionBlockSchema)
	diags = append(diags, moreDiags...)

	if attr, exists := content.Attributes["count"]; exists {
		a.Count = attr.Expr
	}

	if attr, exists := content.Attributes["for_each"]; exists {
		a.ForEach = attr.Expr
		// Cannot have count and for_each on the same action block
		if a.Count != nil {
			diags = append(diags, &hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  `Invalid combination of "count" and "for_each"`,
				Detail:   `The "count" and "for_each" meta-arguments are mutually-exclusive, only one should be used.`,
				Subject:  &attr.NameRange,
			})
		}
	}

	for _, block := range content.Blocks {
		switch block.Type {
		case "config":
			if a.Config != nil {
				diags = diags.Append(&hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  "Duplicate config block",
					Detail:   "An action must contain only one nested \"config\" block.",
					Subject:  block.DefRange.Ptr(),
				})
				return nil, diags
			}
			a.Config = block.Body
		default:
			// Should not get here because the above should cover all
			// block types declared in the schema.
			panic(fmt.Sprintf("unhandled block type %q", block.Type))
		}
	}

	if attr, exists := content.Attributes["provider"]; exists {
		var providerDiags hcl.Diagnostics
		a.ProviderConfigRef, providerDiags = decodeProviderConfigRef(attr.Expr, "provider")
		diags = append(diags, providerDiags...)
	}

	return a, diags
}

// actionBlockSchema is the schema for an action type within terraform.
var actionBlockSchema = &hcl.BodySchema{
	Attributes: commonActionAttributes,
	Blocks: []hcl.BlockHeaderSchema{
		{Type: "config"},
	},
}

var commonActionAttributes = []hcl.AttributeSchema{
	{
		Name: "count",
	},
	{
		Name: "for_each",
	},
	{
		Name: "provider",
	},
}

var actionTriggerSchema = &hcl.BodySchema{
	Attributes: []hcl.AttributeSchema{
		{
			Name:     "events",
			Required: true,
		},
		{
			Name:     "condition",
			Required: false,
		},
		{
			Name:     "actions",
			Required: true,
		},
	},
}

func (a *Action) moduleUniqueKey() string {
	return a.Addr().String()
}

// Addr returns a resource address for the receiver that is relative to the
// resource's containing module.
func (a *Action) Addr() addrs.Action {
	return addrs.Action{
		Type: a.Type,
		Name: a.Name,
	}
}

// ProviderConfigAddr returns the address for the provider configuration that
// should be used for this action. This function returns a default provider
// config addr if an explicit "provider" argument was not provided.
func (a *Action) ProviderConfigAddr() addrs.LocalProviderConfig {
	if a.ProviderConfigRef == nil {
		// If no specific "provider" argument is given, we want to look up the
		// provider config where the local name matches the implied provider
		// from the resource type. This may be different from the resource's
		// provider type.
		return addrs.LocalProviderConfig{
			LocalName: a.Addr().ImpliedProvider(),
		}
	}

	return addrs.LocalProviderConfig{
		LocalName: a.ProviderConfigRef.Name,
		Alias:     a.ProviderConfigRef.Alias,
	}
}

// decodeActionTriggerRef decodes and does basic validation of the Actions
// expression list inside a resource's ActionTrigger block, ensuring each only
// reference a single action. This function was largely copied from
// decodeReplaceTriggeredBy, but is much more permissive in what References are
// allowed.
func decodeActionTriggerRef(expr hcl.Expression) ([]ActionRef, hcl.Diagnostics) {
	exprs, diags := hcl.ExprList(expr)
	if diags.HasErrors() {
		return nil, diags
	}
	actionRefs := make([]ActionRef, len(exprs))

	for i, expr := range exprs {
		// Since we are manually parsing the action_trigger.Actions argument, we
		// need to specially handle json configs, in which case the values will
		// be json strings rather than hcl. To simplify parsing however we will
		// decode the individual list elements, rather than the entire
		// expression.
		var jsDiags hcl.Diagnostics
		expr, jsDiags = unwrapJSONRefExpr(expr)
		diags = diags.Extend(jsDiags)
		if diags.HasErrors() {
			continue
		}
		actionRefs[i] = ActionRef{
			Expr:  expr,
			Range: expr.Range(),
		}

		refs, refDiags := langrefs.ReferencesInExpr(addrs.ParseRef, expr)
		for _, diag := range refDiags {
			severity := hcl.DiagError
			if diag.Severity() == tfdiags.Warning {
				severity = hcl.DiagWarning
			}

			diags = append(diags, &hcl.Diagnostic{
				Severity: severity,
				Summary:  diag.Description().Summary,
				Detail:   diag.Description().Detail,
				Subject:  expr.Range().Ptr(),
			})
		}

		if refDiags.HasErrors() {
			continue
		}

		actionCount := 0
		for _, ref := range refs {
			switch ref.Subject.(type) {
			case addrs.Action, addrs.ActionInstance:
				actionCount++
			case addrs.ModuleCall, addrs.ModuleCallInstance, addrs.ModuleCallInstanceOutput:
				diags = append(diags, &hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  "Invalid reference to action outside this module",
					Detail:   "Actions can only be referenced in the module they are declared in.",
					Subject:  expr.Range().Ptr(),
				})
				continue
			case addrs.Resource, addrs.ResourceInstance:
				// definitely not an action
				diags = append(diags, invalidActionDiag(expr.Range().Ptr()))
				continue
			default:
				// we've checked what we can
			}
		}

		switch {
		case actionCount == 0:
			diags = append(diags, &hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "No actions specified",
				Detail:   "At least one action must be specified for an action_trigger.",
				Subject:  expr.Range().Ptr(),
			})
		case actionCount > 1:
			diags = append(diags, &hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Invalid action expression",
				Detail:   "Multiple action references in actions expression.",
				Subject:  expr.Range().Ptr(),
			})
		}

	}

	return actionRefs, diags
}
