// 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"
	"log"

	"github.com/opentofu/opentofu/internal/dag"
	"github.com/opentofu/opentofu/internal/tracing/traceattrs"

	"github.com/opentofu/opentofu/internal/addrs"
	"github.com/opentofu/opentofu/internal/plans"
	"github.com/opentofu/opentofu/internal/states"
	"github.com/opentofu/opentofu/internal/tfdiags"
	"github.com/opentofu/opentofu/internal/tracing"
	"github.com/zclconf/go-cty/cty"
)

// NodePlanDestroyableResourceInstance represents a resource that is ready
// to be planned for destruction.
type NodePlanDestroyableResourceInstance struct {
	*NodeAbstractResourceInstance

	// skipRefresh indicates that we should skip refreshing
	skipRefresh bool
}

var (
	_ GraphNodeModuleInstance       = (*NodePlanDestroyableResourceInstance)(nil)
	_ GraphNodeReferenceable        = (*NodePlanDestroyableResourceInstance)(nil)
	_ GraphNodeReferencer           = (*NodePlanDestroyableResourceInstance)(nil)
	_ GraphNodeDestroyer            = (*NodePlanDestroyableResourceInstance)(nil)
	_ GraphNodeConfigResource       = (*NodePlanDestroyableResourceInstance)(nil)
	_ GraphNodeResourceInstance     = (*NodePlanDestroyableResourceInstance)(nil)
	_ GraphNodeAttachResourceConfig = (*NodePlanDestroyableResourceInstance)(nil)
	_ GraphNodeAttachResourceState  = (*NodePlanDestroyableResourceInstance)(nil)
	_ GraphNodeExecutable           = (*NodePlanDestroyableResourceInstance)(nil)
	_ GraphNodeProviderConsumer     = (*NodePlanDestroyableResourceInstance)(nil)
	_ dag.NamedVertex               = (*NodePlanDestroyableResourceInstance)(nil)
)

// dag.NamedVertex
func (n *NodePlanDestroyableResourceInstance) Name() string {
	return n.NodeAbstractResourceInstance.Name() + " (destroy)"
}

// GraphNodeDestroyer
func (n *NodePlanDestroyableResourceInstance) DestroyAddr() *addrs.AbsResourceInstance {
	addr := n.ResourceInstanceAddr()
	return &addr
}

// GraphNodeEvalable
func (n *NodePlanDestroyableResourceInstance) Execute(ctx context.Context, evalCtx EvalContext, op walkOperation) (diags tfdiags.Diagnostics) {
	addr := n.ResourceInstanceAddr()

	ctx, span := tracing.Tracer().Start(
		ctx, traceNamePlanResourceInstance,
		tracing.SpanAttributes(
			traceattrs.String(traceAttrResourceInstanceAddr, addr.String()),
			traceattrs.String(traceAttrResourceType, addr.Resource.Resource.Type),
			traceattrs.Bool(traceAttrPlanRefresh, !n.skipRefresh),
		),
	)
	defer span.End()

	diags = diags.Append(n.resolveProvider(ctx, evalCtx, false, states.NotDeposed))
	if diags.HasErrors() {
		tracing.SetSpanError(span, diags)
		return diags
	}
	span.SetAttributes(
		traceattrs.String(traceAttrProviderInstanceAddr, traceProviderInstanceAddr(n.ResolvedProvider.ProviderConfig, n.ResolvedProviderKey)),
	)

	switch addr.Resource.Resource.Mode {
	case addrs.ManagedResourceMode:
		diags = diags.Append(
			n.managedResourceExecute(ctx, evalCtx, op),
		)
	case addrs.DataResourceMode:
		diags = diags.Append(
			n.dataResourceExecute(ctx, evalCtx, op),
		)
	case addrs.EphemeralResourceMode:
		diags = diags.Append(
			n.ephemeralResourceExecute(ctx, evalCtx, op),
		)
	default:
		panic(fmt.Errorf("unsupported resource mode %s", n.Config.Mode))
	}
	tracing.SetSpanError(span, diags)
	return diags
}

func (n *NodePlanDestroyableResourceInstance) managedResourceExecute(ctx context.Context, evalCtx EvalContext, _ walkOperation) (diags tfdiags.Diagnostics) {
	addr := n.ResourceInstanceAddr()

	// Declare a bunch of variables that are used for state during
	// evaluation. These are written to by address in the EvalNodes we
	// declare below.
	var change *plans.ResourceInstanceChange
	var state *states.ResourceInstanceObject

	state, err := n.readResourceInstanceState(ctx, evalCtx, addr)
	diags = diags.Append(err)
	if diags.HasErrors() {
		return diags
	}

	// If we are in the "skip refresh" mode then we will have skipped over our
	// usual opportunity to update the previous run state and refresh state
	// with the result of any provider schema upgrades, so we'll compensate
	// by doing that here.
	//
	// NOTE: this is coupled with logic in Context.destroyPlan which skips
	// running a normal plan walk when refresh is enabled. These two
	// conditionals must agree (be exactly opposite) in order to get the
	// correct behavior in both cases.
	if n.skipRefresh {
		diags = diags.Append(n.writeResourceInstanceState(ctx, evalCtx, state, prevRunState))
		if diags.HasErrors() {
			return diags
		}
		diags = diags.Append(n.writeResourceInstanceState(ctx, evalCtx, state, refreshState))
		if diags.HasErrors() {
			return diags
		}
	}

	var planDiags tfdiags.Diagnostics
	skipDestroy, skipDiags := n.shouldSkipDestroy()
	diags = diags.Append(skipDiags)
	if diags.HasErrors() {
		return diags
	}
	if skipDestroy {
		log.Printf("[DEBUG] NodePlanDestroyableResourceInstance.managedResourceExecute: %s planning forget instead of destroy due to lifecycle.destroy=false in configuration", addr)
		change = n.planForget(ctx, evalCtx, state, "")
		change.ActionReason = plans.ResourceInstanceForgotBecauseLifecycleDestroyInConfig
	} else if state.SkipDestroy {
		log.Printf("[DEBUG] NodePlanDestroyableResourceInstance.managedResourceExecute: %s planning forget instead of destroy due to lifecycle.destroy=false in state", addr)
		change = n.planForget(ctx, evalCtx, state, "")
		change.ActionReason = plans.ResourceInstanceForgotBecauseLifecycleDestroyInState
	} else {
		change, planDiags = n.planDestroy(ctx, evalCtx, state, "")
	}
	diags = diags.Append(planDiags)
	if diags.HasErrors() {
		return diags
	}

	diags = diags.Append(n.writeChange(ctx, evalCtx, change, ""))
	if diags.HasErrors() {
		return diags
	}
	diags = diags.Append(n.checkPreventDestroy(ctx, evalCtx, change))
	return diags
}

func (n *NodePlanDestroyableResourceInstance) dataResourceExecute(ctx context.Context, evalCtx EvalContext, _ walkOperation) (diags tfdiags.Diagnostics) {

	// We may not be able to read a prior data source from the state if the
	// schema was upgraded and we are destroying before ever refreshing that
	// data source. Regardless, a data source  "destroy" is simply writing a
	// null state, which we can do with a null prior state too.
	change := &plans.ResourceInstanceChange{
		Addr:        n.ResourceInstanceAddr(),
		PrevRunAddr: n.prevRunAddr(evalCtx),
		Change: plans.Change{
			Action: plans.Delete,
			Before: cty.NullVal(cty.DynamicPseudoType),
			After:  cty.NullVal(cty.DynamicPseudoType),
		},
		ProviderAddr: n.ResolvedProvider.ProviderConfig,
	}
	return diags.Append(n.writeChange(ctx, evalCtx, change, ""))
}

func (n *NodePlanDestroyableResourceInstance) ephemeralResourceExecute(_ context.Context, _ EvalContext, _ walkOperation) (diags tfdiags.Diagnostics) {
	log.Printf("[TRACE] NodePlanDestroyableResourceInstance: called for ephemeral resource %s", n.Addr)
	return diags.Append(tfdiags.Sourceless(
		tfdiags.Error,
		"An ephemeral resource planned for destroy",
		fmt.Sprintf("A destroy operation has been planned for the ephemeral resource %q. This is an OpenTofu error. Please report this.", n.Addr),
	))
}
