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

	"github.com/opentofu/opentofu/internal/addrs"
	"github.com/opentofu/opentofu/internal/configs"
	"github.com/opentofu/opentofu/internal/dag"
)

type checkTransformer struct {
	// Config for the entire module.
	Config *configs.Config

	// Operation is the current operation this node will be part of.
	Operation walkOperation
}

var _ GraphTransformer = (*checkTransformer)(nil)

func (t *checkTransformer) Transform(graph *Graph) error {
	return t.transform(graph, t.Config, graph.Vertices())
}

func (t *checkTransformer) transform(g *Graph, cfg *configs.Config, allNodes []dag.Vertex) error {

	if t.Operation == walkDestroy || t.Operation == walkPlanDestroy {
		// Don't include anything about checks during destroy operations.
		//
		// For other plan and normal apply operations we do everything, for
		// destroy operations we do nothing. For any other operations we still
		// include the check nodes, but we don't actually execute the checks
		// instead we still validate their references and make sure their
		// conditions make sense etc.
		return nil
	}

	moduleAddr := cfg.Path

	for _, check := range cfg.Module.Checks {
		configAddr := check.Addr().InModule(moduleAddr)

		// We want to create a node for each check block. This node will execute
		// after anything it references, and will update the checks object
		// embedded in the plan and/or state.

		log.Printf("[TRACE] checkTransformer: Nodes and edges for %s", configAddr)
		expand := &nodeExpandCheck{
			addr:   configAddr,
			config: check,
			makeInstance: func(addr addrs.AbsCheck, cfg *configs.Check) dag.Vertex {
				return &nodeCheckAssert{
					addr:          addr,
					config:        cfg,
					executeChecks: t.ExecuteChecks(),
				}
			},
		}
		g.Add(expand)

		// We also need to report the checks we are going to execute before we
		// try and execute them.
		if t.ReportChecks() {
			report := &nodeReportCheck{
				addr: configAddr,
			}
			g.Add(report)

			// Make sure we report our checks before we start executing the
			// actual checks.
			g.Connect(dag.BasicEdge(expand, report))

			if check.DataResource != nil {
				// If we have a nested data source, we need to make sure we
				// also report the check before the data source executes.
				//
				// We loop through all the nodes in the graph to find the one
				// that contains our data source and connect it.
				for _, other := range allNodes {
					if resource, isResource := other.(GraphNodeConfigResource); isResource {
						resourceAddr := resource.ResourceAddr()
						if !resourceAddr.Module.Equal(moduleAddr) {
							// This resource isn't in the same module as our check
							// so skip it.
							continue
						}

						resourceCfg := cfg.Module.ResourceByAddr(resourceAddr.Resource)
						if resourceCfg != nil && resourceCfg.Container != nil && resourceCfg.Container.Accessible(check.Addr()) {
							// Make sure we report our checks before we execute any
							// embedded data resource.
							g.Connect(dag.BasicEdge(other, report))

							// There's at most one embedded data source, and
							// we've found it so stop looking.
							break
						}
					}
				}
			}
		}
	}

	for _, child := range cfg.Children {
		if err := t.transform(g, child, allNodes); err != nil {
			return err
		}
	}

	return nil
}

// ReportChecks returns true if this operation should report any check blocks
// that it is about to execute.
//
// This is true for planning operations, as apply operations recreate the
// expected checks from the plan.
//
// We'll also report the checks during an import operation. We still execute
// our check blocks during an import operation so they need to be reported
// first.
func (t *checkTransformer) ReportChecks() bool {
	return t.Operation == walkPlan || t.Operation == walkImport
}

// ExecuteChecks returns true if this operation should actually execute any
// check blocks in the config.
//
// If this returns false we will still create and execute check nodes in the
// graph, but they will only validate things like references and syntax.
func (t *checkTransformer) ExecuteChecks() bool {
	switch t.Operation {
	case walkPlan, walkApply, walkImport:
		// We only actually execute the checks for plan and apply operations.
		return true
	default:
		// For everything else, we still want to validate the checks make sense
		// logically and syntactically, but we won't actually resolve the check
		// conditions.
		return false
	}
}
