package terraform

import (
	"log"
	"sync"

	"github.com/hashicorp/terraform-plugin-sdk/internal/addrs"
	"github.com/hashicorp/terraform-plugin-sdk/internal/configs"
	"github.com/hashicorp/terraform-plugin-sdk/internal/dag"
)

// ConfigTransformer is a GraphTransformer that adds all the resources
// from the configuration to the graph.
//
// The module used to configure this transformer must be the root module.
//
// Only resources are added to the graph. Variables, outputs, and
// providers must be added via other transforms.
//
// Unlike ConfigTransformerOld, this transformer creates a graph with
// all resources including module resources, rather than creating module
// nodes that are then "flattened".
type ConfigTransformer struct {
	Concrete ConcreteResourceNodeFunc

	// Module is the module to add resources from.
	Config *configs.Config

	// Unique will only add resources that aren't already present in the graph.
	Unique bool

	// Mode will only add resources that match the given mode
	ModeFilter bool
	Mode       addrs.ResourceMode

	l         sync.Mutex
	uniqueMap map[string]struct{}
}

func (t *ConfigTransformer) Transform(g *Graph) error {
	// Lock since we use some internal state
	t.l.Lock()
	defer t.l.Unlock()

	// If no configuration is available, we don't do anything
	if t.Config == nil {
		return nil
	}

	// Reset the uniqueness map. If we're tracking uniques, then populate
	// it with addresses.
	t.uniqueMap = make(map[string]struct{})
	defer func() { t.uniqueMap = nil }()
	if t.Unique {
		for _, v := range g.Vertices() {
			if rn, ok := v.(GraphNodeResource); ok {
				t.uniqueMap[rn.ResourceAddr().String()] = struct{}{}
			}
		}
	}

	// Start the transformation process
	return t.transform(g, t.Config)
}

func (t *ConfigTransformer) transform(g *Graph, config *configs.Config) error {
	// If no config, do nothing
	if config == nil {
		return nil
	}

	// Add our resources
	if err := t.transformSingle(g, config); err != nil {
		return err
	}

	// Transform all the children.
	for _, c := range config.Children {
		if err := t.transform(g, c); err != nil {
			return err
		}
	}

	return nil
}

func (t *ConfigTransformer) transformSingle(g *Graph, config *configs.Config) error {
	path := config.Path
	module := config.Module
	log.Printf("[TRACE] ConfigTransformer: Starting for path: %v", path)

	// For now we assume that each module call produces only one module
	// instance with no key, since we don't yet support "count" and "for_each"
	// on modules.
	// FIXME: As part of supporting "count" and "for_each" on modules, rework
	// this so that we'll "expand" the module call first and then create graph
	// nodes for each module instance separately.
	instPath := path.UnkeyedInstanceShim()

	allResources := make([]*configs.Resource, 0, len(module.ManagedResources)+len(module.DataResources))
	for _, r := range module.ManagedResources {
		allResources = append(allResources, r)
	}
	for _, r := range module.DataResources {
		allResources = append(allResources, r)
	}

	for _, r := range allResources {
		relAddr := r.Addr()

		if t.ModeFilter && relAddr.Mode != t.Mode {
			// Skip non-matching modes
			continue
		}

		addr := relAddr.Absolute(instPath)
		if _, ok := t.uniqueMap[addr.String()]; ok {
			// We've already seen a resource with this address. This should
			// never happen, because we enforce uniqueness in the config loader.
			continue
		}

		abstract := &NodeAbstractResource{Addr: addr}
		var node dag.Vertex = abstract
		if f := t.Concrete; f != nil {
			node = f(abstract)
		}

		g.Add(node)
	}

	return nil
}
