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

package planning

import (
	"context"
	"fmt"

	"github.com/opentofu/opentofu/internal/engine/plugins"
	"github.com/opentofu/opentofu/internal/lang/eval"
	"github.com/opentofu/opentofu/internal/lang/grapheval"
	"github.com/opentofu/opentofu/internal/plans"
	"github.com/opentofu/opentofu/internal/states"
	"github.com/opentofu/opentofu/internal/tfdiags"
)

// PlanChanges is the main entry point, taking a state snapshot from the end
// of the previous plan/apply round and an instantiated configuration (bound
// to some input variable definitions) and returning a plan containing a set of
// proposed actions.
//
// This is currently really just a placeholder to demonstrate the role that the
// functionality in lang/eval might play in a planning process and what other
// work would need to happen in a planning engine that is beyond the scope
// of lang/eval. For now then it's just using our existing models of state
// and plan, but our larger ambitions also involve some other changes that
// would likely cause the signature here to change significantly:
//
//   - We're considering changing the apply phase implementation to just be a
//     walk of an execution graph calculated during the planning phase, which
//     implies that the "plan" model would need to change significantly to
//     be able to directly represent that graph, whereas the current model only
//     _implies_ that graph at a high level while expecting the apply phase
//     itself to construct the finalized graph.
//   - We're considering switching from a "state snapshot" model to a more
//     granular model where we request individual objects from the state storage
//     as needed, in which case we'd likely change our usage pattern so that
//     the planning phase is able to create a "provider-like" live object that
//     offers an API for fetching items from the state as needed, rather than
//     the current pure-data snapshot representation.
//
// Therefore readers of this code should focus mainly on the inner
// implementation of how it decides what to plan and how to plan it, and less
// on where it gets the information to make those decisions and how it
// represents those decisions in its return value.
func PlanChanges(ctx context.Context, prevRoundState *states.State, configInst *eval.ConfigInstance, providers plugins.Providers) (*plans.Plan, tfdiags.Diagnostics) {
	var diags tfdiags.Diagnostics

	planCtx := newPlanContext(configInst.EvalContext(), prevRoundState, providers)

	// This configInst.DrivePlanning call blocks until the evaluator has
	// visited all expressions in the configuration and calls
	// [planContext.PlanDesiredResourceInstance] on the [planGlue] object for
	// each resource instance it discovers so that we can produce a planned
	// action and result value for each one.
	//
	// It also calls the various "Plan*Orphans" methods at different levels
	// of granularity once it's determined the full set of objects under
	// a given prefix, which planGlue uses to notice when there are
	// prevRoundState resource instances that are no longer in the desired
	// state and so plan to delete or forget them.
	evalResult, moreDiags := configInst.DrivePlanning(ctx, func(oracle *eval.PlanningOracle) eval.PlanGlue {
		return &planGlue{
			planCtx: planCtx,
			oracle:  oracle,
		}
	})
	diags = diags.Append(moreDiags)
	if moreDiags.HasErrors() {
		// If we encountered errors during the eval-based phase then we'll halt
		// here but we'll still produce a best-effort [plans.Plan] describing
		// the situation because that often gives useful information for debugging
		// what caused the errors.
		plan := planCtx.Close()
		plan.Errored = true
		return plan, diags
	}
	if evalResult == nil {
		// This should not happen: we should always have an evalResult if
		// there weren't any errors.
		panic(fmt.Sprintf("%T.DrivePlanning returned nil result without any error diagnostics", configInst))
	}

	// We also need to deal with any "deposed" resource instances that were
	// in the previous round state. We do this separately afterwards because
	// these have no direct representation in the configuration at all and
	// so are not in scope for the config eval system. It's also relatively
	// rare for a previous round state to include deposed instances, since it
	// can happen only if the "delete" leg of a create-before-destroy replace
	// failed in the previous round.
	//
	// The provider instance manager should've planned ahead and arranged for
	// any providers we need for these to still be open, waiting for the
	// completion reports generated by our planning calls in this loop.
	ctx = grapheval.ContextWithNewWorker(ctx)
	planGlue := evalResult.Glue.(*planGlue)
	for _, moduleState := range prevRoundState.Modules {
		for _, resourceState := range moduleState.Resources {
			for instKey, instState := range resourceState.Instances {
				instAddr := resourceState.Addr.Instance(instKey)
				for dk := range instState.Deposed {
					// We currently have a schism where we do all of the
					// discovery work using the traditional state model but
					// we then switch to using our new-style "full" object model
					// to act on what we've discovered. This is hopefully just
					// a temporary situation while we're operating in a mixed
					// world where most of the system doesn't know about the
					// new runtime yet.
					objState := prevRoundState.SyncWrapper().ResourceInstanceObjectFull(instAddr, dk)
					if objState == nil {
						// If we get here then there's a bug in the
						// ResourceInstanceObjectFull function, because we
						// should only be here if instAddr and dk correspond.
						// to an actual deposed object.
						panic(fmt.Sprintf("state has %s deposed object %q, but ResourceInstanceObjectFull didn't return it", instAddr, dk))
					}
					diags = diags.Append(
						planGlue.planDeposedResourceInstanceObject(ctx, instAddr, dk, objState),
					)
				}
			}
		}
	}

	return planCtx.Close(), diags
}
