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

package refactoring

import (
	"fmt"

	"github.com/hashicorp/hcl/v2"

	"github.com/hashicorp/terraform/internal/addrs"
	"github.com/hashicorp/terraform/internal/configs"
	"github.com/hashicorp/terraform/internal/tfdiags"
)

// RemoveStatement is the fully-specified form of addrs.Remove
type RemoveStatement struct {
	// From is the absolute address of the configuration object being removed.
	From addrs.ConfigMoveable

	// Destroy indicates that the resource should be destroyed, not just removed
	// from state.
	Destroy   bool
	DeclRange tfdiags.SourceRange
}

// FindRemoveStatements recurses through the modules of the given configuration
// and returns a set of all "removed" blocks defined within after deduplication
// on the From address.
//
// Error diagnostics are returned if any resource or module targeted by a remove
// block is still defined in configuration.
//
// A "removed" block in a parent module overrides a removed block in a child
// module when both target the same configuration object.
func FindRemoveStatements(rootCfg *configs.Config) (addrs.Map[addrs.ConfigMoveable, RemoveStatement], tfdiags.Diagnostics) {
	stmts := findRemoveStatements(rootCfg, addrs.MakeMap[addrs.ConfigMoveable, RemoveStatement]())
	diags := validateRemoveStatements(rootCfg, stmts)
	return stmts, diags
}

func validateRemoveStatements(cfg *configs.Config, stmts addrs.Map[addrs.ConfigMoveable, RemoveStatement]) (diags tfdiags.Diagnostics) {
	for _, rst := range stmts.Keys() {
		switch rst := rst.(type) {
		case addrs.ConfigResource:
			m := cfg.Descendent(rst.Module)
			if m == nil {
				break
			}

			if r := m.Module.ResourceByAddr(rst.Resource); r != nil {
				diags = diags.Append(&hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  "Removed resource still exists",
					Detail:   fmt.Sprintf("This statement declares that %s was removed, but it is still declared in configuration.", rst),
					Subject:  r.DeclRange.Ptr(),
				})
			}
		case addrs.Module:
			if m := cfg.Descendent(rst); m != nil {
				diags = diags.Append(&hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  "Removed module still exists",
					Detail:   fmt.Sprintf("This statement declares that %s was removed, but it is still declared in configuration.", rst),
					Subject:  m.CallRange.Ptr(),
				})
			}
		}
	}
	return diags
}

func findRemoveStatements(cfg *configs.Config, into addrs.Map[addrs.ConfigMoveable, RemoveStatement]) addrs.Map[addrs.ConfigMoveable, RemoveStatement] {
	for _, mc := range cfg.Module.Removed {
		switch mc.From.ObjectKind() {
		case addrs.RemoveTargetResource:
			// First, stitch together the module path and the RelSubject to form
			// the absolute address of the config object being removed.
			res := mc.From.RelSubject.(addrs.ConfigResource)
			fromAddr := addrs.ConfigResource{
				Module:   append(cfg.Path, res.Module...),
				Resource: res.Resource,
			}

			// If we already have a remove statement for this ConfigResource, it
			// must have come from a parent module, because duplicate removed
			// blocks in the same module are ignored during parsing.
			// The removed block in the parent module overrides the block in the
			// child module.
			existingStatement, ok := into.GetOk(fromAddr)
			if ok {
				if existingResource, ok := existingStatement.From.(addrs.ConfigResource); ok &&
					existingResource.Equal(fromAddr) {
					continue
				}
			}

			into.Put(fromAddr, RemoveStatement{
				From:      fromAddr,
				Destroy:   mc.Destroy,
				DeclRange: tfdiags.SourceRangeFromHCL(mc.DeclRange),
			})
		case addrs.RemoveTargetModule:
			// First, stitch together the module path and the RelSubject to form
			// the absolute address of the config object being removed.
			mod := mc.From.RelSubject.(addrs.Module)
			absMod := append(cfg.Path, mod...)

			// If there is already a statement for this Module, it must
			// have come from a parent module, because duplicate removed blocks
			// in the same module are ignored during parsing.
			// The removed block in the parent module overrides the block in the
			// child module.
			existingStatement, ok := into.GetOk(mc.From.RelSubject)
			if ok {
				if existingModule, ok := existingStatement.From.(addrs.Module); ok &&
					existingModule.Equal(absMod) {
					continue
				}
			}

			into.Put(absMod, RemoveStatement{
				From:      absMod,
				Destroy:   mc.Destroy,
				DeclRange: tfdiags.SourceRangeFromHCL(mc.DeclRange),
			})
		default:
			panic("Unsupported remove target kind")
		}
	}

	for _, childCfg := range cfg.Children {
		into = findRemoveStatements(childCfg, into)
	}

	return into
}
