package terraform

import (
	"log"

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

// GraphNodeTargetable is an interface for graph nodes to implement when they
// need to be told about incoming targets. This is useful for nodes that need
// to respect targets as they dynamically expand. Note that the list of targets
// provided will contain every target provided, and each implementing graph
// node must filter this list to targets considered relevant.
type GraphNodeTargetable interface {
	SetTargets([]addrs.Targetable)
}

// GraphNodeTargetDownstream is an interface for graph nodes that need to
// be remain present under targeting if any of their dependencies are targeted.
// TargetDownstream is called with the set of vertices that are direct
// dependencies for the node, and it should return true if the node must remain
// in the graph in support of those dependencies.
//
// This is used in situations where the dependency edges are representing an
// ordering relationship but the dependency must still be visited if its
// dependencies are visited. This is true for outputs, for example, since
// they must get updated if any of their dependent resources get updated,
// which would not normally be true if one of their dependencies were targeted.
type GraphNodeTargetDownstream interface {
	TargetDownstream(targeted, untargeted *dag.Set) bool
}

// TargetsTransformer is a GraphTransformer that, when the user specifies a
// list of resources to target, limits the graph to only those resources and
// their dependencies.
type TargetsTransformer struct {
	// List of targeted resource names specified by the user
	Targets []addrs.Targetable

	// If set, the index portions of resource addresses will be ignored
	// for comparison. This is used when transforming a graph where
	// counted resources have not yet been expanded, since otherwise
	// the unexpanded nodes (which never have indices) would not match.
	IgnoreIndices bool

	// Set to true when we're in a `terraform destroy` or a
	// `terraform plan -destroy`
	Destroy bool
}

func (t *TargetsTransformer) Transform(g *Graph) error {
	if len(t.Targets) > 0 {
		targetedNodes, err := t.selectTargetedNodes(g, t.Targets)
		if err != nil {
			return err
		}

		for _, v := range g.Vertices() {
			removable := false
			if _, ok := v.(GraphNodeResource); ok {
				removable = true
			}

			if vr, ok := v.(RemovableIfNotTargeted); ok {
				removable = vr.RemoveIfNotTargeted()
			}

			if removable && !targetedNodes.Include(v) {
				log.Printf("[DEBUG] Removing %q, filtered by targeting.", dag.VertexName(v))
				g.Remove(v)
			}
		}
	}

	return nil
}

// Returns a set of targeted nodes. A targeted node is either addressed
// directly, address indirectly via its container, or it's a dependency of a
// targeted node. Destroy mode keeps dependents instead of dependencies.
func (t *TargetsTransformer) selectTargetedNodes(g *Graph, addrs []addrs.Targetable) (*dag.Set, error) {
	targetedNodes := new(dag.Set)

	vertices := g.Vertices()

	for _, v := range vertices {
		if t.nodeIsTarget(v, addrs) {
			targetedNodes.Add(v)

			// We inform nodes that ask about the list of targets - helps for nodes
			// that need to dynamically expand. Note that this only occurs for nodes
			// that are already directly targeted.
			if tn, ok := v.(GraphNodeTargetable); ok {
				tn.SetTargets(addrs)
			}

			var deps *dag.Set
			var err error
			if t.Destroy {
				deps, err = g.Descendents(v)
			} else {
				deps, err = g.Ancestors(v)
			}
			if err != nil {
				return nil, err
			}

			for _, d := range deps.List() {
				targetedNodes.Add(d)
			}
		}
	}
	return t.addDependencies(targetedNodes, g)
}

func (t *TargetsTransformer) addDependencies(targetedNodes *dag.Set, g *Graph) (*dag.Set, error) {
	// Handle nodes that need to be included if their dependencies are included.
	// This requires multiple passes since we need to catch transitive
	// dependencies if and only if they are via other nodes that also
	// support TargetDownstream. For example:
	// output -> output -> targeted-resource: both outputs need to be targeted
	// output -> non-targeted-resource -> targeted-resource: output not targeted
	//
	// We'll keep looping until we stop targeting more nodes.
	queue := targetedNodes.List()
	for len(queue) > 0 {
		vertices := queue
		queue = nil // ready to append for next iteration if neccessary
		for _, v := range vertices {
			// providers don't cause transitive dependencies, so don't target
			// downstream from them.
			if _, ok := v.(GraphNodeProvider); ok {
				continue
			}

			dependers := g.UpEdges(v)
			if dependers == nil {
				// indicates that there are no up edges for this node, so
				// we have nothing to do here.
				continue
			}

			dependers = dependers.Filter(func(dv interface{}) bool {
				_, ok := dv.(GraphNodeTargetDownstream)
				return ok
			})

			if dependers.Len() == 0 {
				continue
			}

			for _, dv := range dependers.List() {
				if targetedNodes.Include(dv) {
					// Already present, so nothing to do
					continue
				}

				// We'll give the node some information about what it's
				// depending on in case that informs its decision about whether
				// it is safe to be targeted.
				deps := g.DownEdges(v)

				depsTargeted := deps.Intersection(targetedNodes)
				depsUntargeted := deps.Difference(depsTargeted)

				if dv.(GraphNodeTargetDownstream).TargetDownstream(depsTargeted, depsUntargeted) {
					targetedNodes.Add(dv)
					// Need to visit this node on the next pass to see if it
					// has any transitive dependers.
					queue = append(queue, dv)
				}
			}
		}
	}

	return targetedNodes.Filter(func(dv interface{}) bool {
		return filterPartialOutputs(dv, targetedNodes, g)
	}), nil
}

// Outputs may have been included transitively, but if any of their
// dependencies have been pruned they won't be resolvable.
// If nothing depends on the output, and the output is missing any
// dependencies, remove it from the graph.
// This essentially maintains the previous behavior where interpolation in
// outputs would fail silently, but can now surface errors where the output
// is required.
func filterPartialOutputs(v interface{}, targetedNodes *dag.Set, g *Graph) bool {
	// should this just be done with TargetDownstream?
	if _, ok := v.(*NodeApplyableOutput); !ok {
		return true
	}

	dependers := g.UpEdges(v)
	for _, d := range dependers.List() {
		if _, ok := d.(*NodeCountBoundary); ok {
			continue
		}

		if !targetedNodes.Include(d) {
			// this one is going to be removed, so it doesn't count
			continue
		}

		// as soon as we see a real dependency, we mark this as
		// non-removable
		return true
	}

	depends := g.DownEdges(v)

	for _, d := range depends.List() {
		if !targetedNodes.Include(d) {
			log.Printf("[WARN] %s missing targeted dependency %s, removing from the graph",
				dag.VertexName(v), dag.VertexName(d))
			return false
		}
	}
	return true
}

func (t *TargetsTransformer) nodeIsTarget(v dag.Vertex, targets []addrs.Targetable) bool {
	var vertexAddr addrs.Targetable
	switch r := v.(type) {
	case GraphNodeResourceInstance:
		vertexAddr = r.ResourceInstanceAddr()
	case GraphNodeResource:
		vertexAddr = r.ResourceAddr()
	default:
		// Only resource and resource instance nodes can be targeted.
		return false
	}
	_, ok := v.(GraphNodeResource)
	if !ok {
		return false
	}

	for _, targetAddr := range targets {
		if t.IgnoreIndices {
			// If we're ignoring indices then we'll convert any resource instance
			// addresses into resource addresses. We don't need to convert
			// vertexAddr because instance addresses are contained within
			// their associated resources, and so .TargetContains will take
			// care of this for us.
			if instance, isInstance := targetAddr.(addrs.AbsResourceInstance); isInstance {
				targetAddr = instance.ContainingResource()
			}
		}
		if targetAddr.TargetContains(vertexAddr) {
			return true
		}
	}

	return false
}

// RemovableIfNotTargeted is a special interface for graph nodes that
// aren't directly addressable, but need to be removed from the graph when they
// are not targeted. (Nodes that are not directly targeted end up in the set of
// targeted nodes because something that _is_ targeted depends on them.) The
// initial use case for this interface is GraphNodeConfigVariable, which was
// having trouble interpolating for module variables in targeted scenarios that
// filtered out the resource node being referenced.
type RemovableIfNotTargeted interface {
	RemoveIfNotTargeted() bool
}
