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

package eval

import (
	"context"

	"github.com/zclconf/go-cty/cty"

	"github.com/opentofu/opentofu/internal/addrs"
	"github.com/opentofu/opentofu/internal/lang/eval/internal/configgraph"
	"github.com/opentofu/opentofu/internal/lang/eval/internal/evalglue"
	"github.com/opentofu/opentofu/internal/lang/grapheval"
	"github.com/opentofu/opentofu/internal/plans/objchange"
	"github.com/opentofu/opentofu/internal/tfdiags"
)

// prepareToPlan implements an extra preparation phase we perform before
// running the main plan phase so we can deal with the "chicken or the egg"
// problem of needing to evaluate configuraton to learn the relationships
// between resources instances and provider instances but needing to already
// know those relationships in order to fully evaluate the configuration.
//
// As a compromise then, we initially perform a more conservative walk that
// just treats all resource instances as having fully-unknown values so that
// we don't need to configure any providers or open any ephemeral resource
// instances, and then ask the resulting configgraph objects to report
// the resource/provider dependencies which we can then use on a subsequent
// re-eval that includes the real provider planning actions.
//
// This approach relies on the idea that an evaluation with more unknown
// values should always produce a superset of the true dependencies that
// would be reported once there are fewer unknown values, and so the
// result of this function should capture _at least_ the dependencies
// required to successfully plan, possibly also including some harmless
// additional dependency relationships that aren't strictly needed once we can
// evaluate using real resource planning results. The planning phase will
// then be able to produce its own tighter version of this information to
// use when building the execution graph for the apply phase.
func (c *ConfigInstance) prepareToPlan(ctx context.Context) (*ResourceRelationships, tfdiags.Diagnostics) {
	// All of our work will be associated with a workgraph worker that serves
	// as the initial worker node in the work graph.
	ctx = grapheval.ContextWithNewWorker(ctx)

	rootModuleInstance, diags := c.precheckedModuleInstance(ctx)
	if diags.HasErrors() {
		return nil, diags
	}
	ret := &ResourceRelationships{
		EphemeralResourceUsers: addrs.MakeMap[addrs.AbsResourceInstance, EphemeralResourceInstanceUsers](),
		ProviderInstanceUsers:  addrs.MakeMap[addrs.AbsProviderInstanceCorrect, ProviderInstanceUsers](),
	}
	for depender := range evalglue.ResourceInstancesDeep(ctx, rootModuleInstance) {
		dependerAddr := depender.Addr
		for dependee := range depender.ResourceInstanceDependencies(ctx) {
			dependeeAddr := dependee.Addr
			if dependeeAddr.Resource.Resource.Mode == addrs.EphemeralResourceMode {
				if !ret.EphemeralResourceUsers.Has(dependeeAddr) {
					ret.EphemeralResourceUsers.Put(dependeeAddr, EphemeralResourceInstanceUsers{
						ResourceInstances: addrs.MakeSet[addrs.AbsResourceInstance](),
						ProviderInstances: addrs.MakeSet[addrs.AbsProviderInstanceCorrect](),
					})
				}
				set := ret.EphemeralResourceUsers.Get(dependeeAddr).ResourceInstances
				set.Add(dependerAddr)
			}
		}
		providerInst, _, _ := depender.ProviderInstance(ctx)
		if providerInst, known := configgraph.GetKnown(providerInst); known {
			providerInstAddr := providerInst.Addr
			if !ret.ProviderInstanceUsers.Has(providerInstAddr) {
				ret.ProviderInstanceUsers.Put(providerInstAddr, ProviderInstanceUsers{
					ResourceInstances: addrs.MakeSet[addrs.AbsResourceInstance](),
				})
			}
			set := ret.ProviderInstanceUsers.Get(providerInstAddr).ResourceInstances
			set.Add(dependerAddr)
		}
	}
	for depender := range evalglue.ProviderInstancesDeep(ctx, rootModuleInstance) {
		dependerAddr := depender.Addr
		for dependee := range depender.ResourceInstanceDependencies(ctx) {
			dependeeAddr := dependee.Addr
			if dependeeAddr.Resource.Resource.Mode == addrs.EphemeralResourceMode {
				if !ret.EphemeralResourceUsers.Has(dependeeAddr) {
					ret.EphemeralResourceUsers.Put(dependeeAddr, EphemeralResourceInstanceUsers{
						ResourceInstances: addrs.MakeSet[addrs.AbsResourceInstance](),
						ProviderInstances: addrs.MakeSet[addrs.AbsProviderInstanceCorrect](),
					})
				}
				set := ret.EphemeralResourceUsers.Get(dependeeAddr).ProviderInstances
				set.Add(dependerAddr)
			}
		}
	}
	return ret, diags
}

type ResourceRelationships struct {
	// EphemeralResourceUsers is a map from ephemeral resource instance
	// addresses to the sets of addresses of other resource instances (of
	// any mode, including other ephemeral ones) which depend on them.
	//
	// A subsequent plan phase can use this to detect when all of the
	// downstream users of an ephemeral resource instance have finished
	// their work and so it's okay to close the ephemeral resource instance.
	//
	// TODO: This should also capture the provider instances that are depending
	// on each ephemeral resource instance.
	EphemeralResourceUsers addrs.Map[addrs.AbsResourceInstance, EphemeralResourceInstanceUsers]

	// EphemeralResourceUsers is a map from provider instance addresses to the
	// sets of addresses of resource instances which a provided by them.
	//
	// A subsequent plan phase can use this to detect when all of the
	// downstream users of a provider instance have finished their work and so
	// it's okay to close the provider instance.
	ProviderInstanceUsers addrs.Map[addrs.AbsProviderInstanceCorrect, ProviderInstanceUsers]
}

type EphemeralResourceInstanceUsers struct {
	ResourceInstances addrs.Set[addrs.AbsResourceInstance]
	ProviderInstances addrs.Set[addrs.AbsProviderInstanceCorrect]
}

type ProviderInstanceUsers struct {
	ResourceInstances addrs.Set[addrs.AbsResourceInstance]
}

// precheckedModuleInstance deals with the common part of both
// [ConfigInstance.prepareToPlan] and [ConfigInstance.Validate], where we
// evaluate the configuration using unknown value placeholders for resource
// instances to discover information about the configuration even when we
// aren't able to configure any providers.
//
// This must be called with a [context.Context] that's associated with a
// [grapheval.Worker].
func (c *ConfigInstance) precheckedModuleInstance(ctx context.Context) (evalglue.CompiledModuleInstance, tfdiags.Diagnostics) {
	var diags tfdiags.Diagnostics

	internalGlue := &preparationGlue{
		providers: c.evalContext.Providers,
	}
	rootModuleInstance, moreDiags := c.newRootModuleInstance(ctx, internalGlue)
	diags = diags.Append(moreDiags)
	if moreDiags.HasErrors() {
		// If we can't even load the root module then we'll bail out early.
		return nil, diags
	}

	// For validation purposes we don't need to do anything other than the
	// full-tree check that would normally run alongside the driving of
	// some other operation.
	moreDiags = checkAll(ctx, rootModuleInstance)
	diags = diags.Append(moreDiags)
	return rootModuleInstance, diags
}

// preparationGlue is the [evaluationGlue] implementation used by
// [ConfigInstance.precheckedModuleInstance].
type preparationGlue struct {
	// preparationGlue uses provider schema information to prepare placeholder
	// "final state" values for resource instances because validation does
	// not use information from the state.
	providers ProvidersSchema
}

// ValidateProviderConfig implements evalglue.Glue.
func (v *preparationGlue) ValidateProviderConfig(ctx context.Context, provider addrs.Provider, configVal cty.Value) tfdiags.Diagnostics {
	// TODO maybe this is why we create validation glue?
	//return v.providers.ValidateProviderConfig(ctx, provider, configVal)
	return nil
}

// ResourceInstanceValue implements evaluationGlue.
func (v *preparationGlue) ResourceInstanceValue(ctx context.Context, ri *configgraph.ResourceInstance, configVal cty.Value, _ configgraph.Maybe[*configgraph.ProviderInstance]) (cty.Value, tfdiags.Diagnostics) {
	schema, diags := v.providers.ResourceTypeSchema(ctx,
		ri.Provider,
		ri.Addr.Resource.Resource.Mode,
		ri.Addr.Resource.Resource.Type,
	)
	if diags.HasErrors() {
		// If we can't get schema then we'll return a fully-unknown value
		// as a placeholder because we don't even know what type we need.
		return cty.DynamicVal, diags
	}

	/* TODO maybe this is why we create validation glue?
	validateDiags := v.providers.ValidateResourceConfig(ctx, ri.Provider, ri.Addr.Resource.Resource.Mode, ri.Addr.Resource.Resource.Type, configVal)
	diags = diags.Append(validateDiags)
	if diags.HasErrors() {
		// Provider indicated an invalid resource configuration
		return cty.DynamicVal, diags
	}*/

	// FIXME: If we have a managed or data resource instance, as opposed to
	// an ephemeral resource instance, then we should check to make sure
	// that ephemeral-marked values only appear in parts of the configVal
	// that correspond to WriteOnly attributes in the schema.

	// We now have enough information to produce a placeholder "planned new
	// state" by placing unknown values in any location that the provider
	// would be allowed to choose a value.
	// NOTE: With the implementation of this function as of commit
	// dd5257d58e27b1af3b8dde97c80daec97f6ca55e this shows as a pretty
	// hot path in CPU profiling, which is not a huge surprise -- we've
	// known it as relatively expensive from its use in "package tofu"
	// already -- but it stands out more in this new implementation because
	// it's not competing with other expensive work like performing transitive
	// reduction on a dag, etc. The main problem seems to be that it allocates
	// a _lot_ of temporary objects, and so there's lots of GC pressure.
	return objchange.ProposedNew(
		schema.Block, cty.NullVal(schema.Block.ImpliedType()), configVal,
	), diags
}
