package terraform

import (
	"fmt"

	"github.com/hashicorp/terraform/config"
)

// NodeDestroyResource represents a resource that is to be destroyed.
type NodeDestroyResource struct {
	*NodeAbstractResource
}

func (n *NodeDestroyResource) Name() string {
	return n.NodeAbstractResource.Name() + " (destroy)"
}

// GraphNodeDestroyer
func (n *NodeDestroyResource) DestroyAddr() *ResourceAddress {
	return n.Addr
}

// GraphNodeDestroyerCBD
func (n *NodeDestroyResource) CreateBeforeDestroy() bool {
	// If we have no config, we just assume no
	if n.Config == nil {
		return false
	}

	return n.Config.Lifecycle.CreateBeforeDestroy
}

// GraphNodeDestroyerCBD
func (n *NodeDestroyResource) ModifyCreateBeforeDestroy(v bool) error {
	// If we have no config, do nothing since it won't affect the
	// create step anyways.
	if n.Config == nil {
		return nil
	}

	// Set CBD to true
	n.Config.Lifecycle.CreateBeforeDestroy = true

	return nil
}

// GraphNodeReferenceable, overriding NodeAbstractResource
func (n *NodeDestroyResource) ReferenceableName() []string {
	// We modify our referenceable name to have the suffix of ".destroy"
	// since depending on the creation side doesn't necessarilly mean
	// depending on destruction.
	suffix := ".destroy"

	// If we're CBD, we also append "-cbd". This is because CBD will setup
	// its own edges (in CBDEdgeTransformer). Depending on the "destroy"
	// side generally doesn't mean depending on CBD as well. See GH-11349
	if n.CreateBeforeDestroy() {
		suffix += "-cbd"
	}

	result := n.NodeAbstractResource.ReferenceableName()
	for i, v := range result {
		result[i] = v + suffix
	}

	return result
}

// GraphNodeReferencer, overriding NodeAbstractResource
func (n *NodeDestroyResource) References() []string {
	// If we have a config, then we need to include destroy-time dependencies
	if c := n.Config; c != nil {
		var result []string
		for _, p := range c.Provisioners {
			// We include conn info and config for destroy time provisioners
			// as dependencies that we have.
			if p.When == config.ProvisionerWhenDestroy {
				result = append(result, ReferencesFromConfig(p.ConnInfo)...)
				result = append(result, ReferencesFromConfig(p.RawConfig)...)
			}
		}

		return result
	}

	return nil
}

// GraphNodeDynamicExpandable
func (n *NodeDestroyResource) DynamicExpand(ctx EvalContext) (*Graph, error) {
	// If we have no config we do nothing
	if n.Addr == nil {
		return nil, nil
	}

	state, lock := ctx.State()
	lock.RLock()
	defer lock.RUnlock()

	// Start creating the steps
	steps := make([]GraphTransformer, 0, 5)

	// We want deposed resources in the state to be destroyed
	steps = append(steps, &DeposedTransformer{
		State:            state,
		View:             n.Addr.stateId(),
		ResolvedProvider: n.ResolvedProvider,
	})

	// Target
	steps = append(steps, &TargetsTransformer{
		ParsedTargets: n.Targets,
	})

	// Always end with the root being added
	steps = append(steps, &RootTransformer{})

	// Build the graph
	b := &BasicGraphBuilder{
		Steps: steps,
		Name:  "NodeResourceDestroy",
	}
	return b.Build(ctx.Path())
}

// GraphNodeEvalable
func (n *NodeDestroyResource) EvalTree() EvalNode {
	// stateId is the ID to put into the state
	stateId := n.Addr.stateId()

	// Build the instance info. More of this will be populated during eval
	info := &InstanceInfo{
		Id:          stateId,
		Type:        n.Addr.Type,
		uniqueExtra: "destroy",
	}

	// Build the resource for eval
	addr := n.Addr
	resource := &Resource{
		Name:       addr.Name,
		Type:       addr.Type,
		CountIndex: addr.Index,
	}
	if resource.CountIndex < 0 {
		resource.CountIndex = 0
	}

	// Get our state
	rs := n.ResourceState
	if rs == nil {
		rs = &ResourceState{
			Provider: n.ResolvedProvider,
		}
	}

	var diffApply *InstanceDiff
	var provider ResourceProvider
	var state *InstanceState
	var err error
	return &EvalOpFilter{
		Ops: []walkOperation{walkApply, walkDestroy},
		Node: &EvalSequence{
			Nodes: []EvalNode{
				// Get the saved diff for apply
				&EvalReadDiff{
					Name: stateId,
					Diff: &diffApply,
				},

				// Filter the diff so we only get the destroy
				&EvalFilterDiff{
					Diff:    &diffApply,
					Output:  &diffApply,
					Destroy: true,
				},

				// If we're not destroying, then compare diffs
				&EvalIf{
					If: func(ctx EvalContext) (bool, error) {
						if diffApply != nil && diffApply.GetDestroy() {
							return true, nil
						}

						return true, EvalEarlyExitError{}
					},
					Then: EvalNoop{},
				},

				// Load the instance info so we have the module path set
				&EvalInstanceInfo{Info: info},

				&EvalGetProvider{
					Name:   n.ResolvedProvider,
					Output: &provider,
				},
				&EvalReadState{
					Name:   stateId,
					Output: &state,
				},
				&EvalRequireState{
					State: &state,
				},

				// Call pre-apply hook
				&EvalApplyPre{
					Info:  info,
					State: &state,
					Diff:  &diffApply,
				},

				// Run destroy provisioners if not tainted
				&EvalIf{
					If: func(ctx EvalContext) (bool, error) {
						if state != nil && state.Tainted {
							return false, nil
						}

						return true, nil
					},

					Then: &EvalApplyProvisioners{
						Info:           info,
						State:          &state,
						Resource:       n.Config,
						InterpResource: resource,
						Error:          &err,
						When:           config.ProvisionerWhenDestroy,
					},
				},

				// If we have a provisioning error, then we just call
				// the post-apply hook now.
				&EvalIf{
					If: func(ctx EvalContext) (bool, error) {
						return err != nil, nil
					},

					Then: &EvalApplyPost{
						Info:  info,
						State: &state,
						Error: &err,
					},
				},

				// Make sure we handle data sources properly.
				&EvalIf{
					If: func(ctx EvalContext) (bool, error) {
						if n.Addr == nil {
							return false, fmt.Errorf("nil address")
						}

						if n.Addr.Mode == config.DataResourceMode {
							return true, nil
						}

						return false, nil
					},

					Then: &EvalReadDataApply{
						Info:     info,
						Diff:     &diffApply,
						Provider: &provider,
						Output:   &state,
					},
					Else: &EvalApply{
						Info:     info,
						State:    &state,
						Diff:     &diffApply,
						Provider: &provider,
						Output:   &state,
						Error:    &err,
					},
				},
				&EvalWriteState{
					Name:         stateId,
					ResourceType: n.Addr.Type,
					Provider:     n.ResolvedProvider,
					Dependencies: rs.Dependencies,
					State:        &state,
				},
				&EvalApplyPost{
					Info:  info,
					State: &state,
					Error: &err,
				},
				&EvalUpdateStateHook{},
			},
		},
	}
}
