// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1

package addrs

import (
	"bytes"
	"fmt"
	"sort"

	"github.com/hashicorp/terraform/internal/dag"
)

// DirectedGraph represents a directed graph whose nodes are addresses of
// type T.
//
// This graph type supports directed edges between pairs of addresses, and
// because Terraform most commonly uses graphs to represent dependency
// relationships it uses "dependency" and "dependent" as the names of the
// endpoints of an edge, even though technically this data structure could
// be used to represent other kinds of directed relationships if needed.
// When used as an operation dependency graph, the "dependency" must be visited
// before the "dependent".
//
// This data structure is not concurrency-safe for writes and so callers must
// supply suitable synchronization primitives if modifying a graph concurrently
// with readers or other writers. Concurrent reads of an already-constructed
// graph are safe.
type DirectedGraph[T UniqueKeyer] struct {
	// Our dag.AcyclicGraph implementation is a little quirky but also
	// well-tested and stable, so we'll use that for the underlying
	// graph operations and just wrap a slightly nicer address-oriented
	// API around it.
	// Reusing this does mean that some of our operations end up allocating
	// more than they would need to otherwise, so perhaps we'll revisit this
	// in future if it seems to be causing performance problems.
	g *dag.AcyclicGraph

	// dag.AcyclicGraph can only support node types that are either
	// comparable using == or that implement a special "hashable"
	// interface, so we'll use our UniqueKeyer technique to produce
	// suitable node values but we need a sidecar structure to remember
	// which real address value belongs to each node value.
	nodes map[UniqueKey]T
}

func NewDirectedGraph[T UniqueKeyer]() DirectedGraph[T] {
	return DirectedGraph[T]{
		g:     &dag.AcyclicGraph{},
		nodes: map[UniqueKey]T{},
	}
}

func (g DirectedGraph[T]) Add(addr T) {
	k := addr.UniqueKey()
	g.nodes[k] = addr
	g.g.Add(k)
}

func (g DirectedGraph[T]) Has(addr T) bool {
	k := addr.UniqueKey()
	_, ok := g.nodes[k]
	return ok
}

func (g DirectedGraph[T]) Remove(addr T) {
	k := addr.UniqueKey()
	g.g.Remove(k)
	delete(g.nodes, k)
}

func (g DirectedGraph[T]) AllNodes() Set[T] {
	ret := make(Set[T], len(g.nodes))
	for _, addr := range g.nodes {
		ret.Add(addr)
	}
	return ret
}

// AddDependency records that the first address depends on the second.
//
// If either address is not already in the graph then it will be implicitly
// added as part of this operation.
func (g DirectedGraph[T]) AddDependency(dependent, dependency T) {
	g.Add(dependent)
	g.Add(dependency)
	g.g.Connect(dag.BasicEdge(dependent.UniqueKey(), dependency.UniqueKey()))
}

// DirectDependenciesOf returns only the direct dependencies of the given
// dependent address.
func (g DirectedGraph[T]) DirectDependenciesOf(addr T) Set[T] {
	k := addr.UniqueKey()
	ret := MakeSet[T]()
	raw := g.g.DownEdges(k)
	for otherKI := range raw {
		ret.Add(g.nodes[otherKI.(UniqueKey)])
	}
	return ret
}

// TransitiveDependenciesOf returns both direct and indirect dependencies of the
// given dependent address.
//
// This operation is valid only for an acyclic graph, and will panic if
// the graph contains cycles.
func (g DirectedGraph[T]) TransitiveDependenciesOf(addr T) Set[T] {
	k := addr.UniqueKey()
	ret := MakeSet[T]()
	for otherKI := range g.g.Ancestors(k) {
		ret.Add(g.nodes[otherKI.(UniqueKey)])
	}
	return ret
}

// DirectDependentsOf returns only the direct dependents of the given
// dependency address.
func (g DirectedGraph[T]) DirectDependentsOf(addr T) Set[T] {
	k := addr.UniqueKey()
	ret := MakeSet[T]()
	raw := g.g.UpEdges(k)
	for otherKI := range raw {
		ret.Add(g.nodes[otherKI.(UniqueKey)])
	}
	return ret
}

// TransitiveDependentsOf returns both direct and indirect dependents of the
// given dependency address.
//
// This operation is valid only for an acyclic graph, and will panic if
// the graph contains cycles.
func (g DirectedGraph[T]) TransitiveDependentsOf(addr T) Set[T] {
	k := addr.UniqueKey()
	ret := MakeSet[T]()
	for otherKI := range g.g.Descendants(k) {
		ret.Add(g.nodes[otherKI.(UniqueKey)])
	}
	return ret
}

// TopologicalOrder returns one possible topological sort of the addresses
// in the graph.
//
// There are often multiple possible sort orders that preserve the required
// dependency ordering. The exact order returned by this function is undefined
// and may vary between calls against the same graph.
func (g DirectedGraph[T]) TopologicalOrder() []T {
	raw := g.g.TopologicalOrder()
	if len(raw) == 0 {
		return nil
	}
	ret := make([]T, len(raw))
	for i, k := range raw {
		ret[i] = g.nodes[k.(UniqueKey)]
	}
	return ret
}

// StringForComparison outputs a string representing the topology of the
// graph in a form intended for convenient equality testing by string comparison.
//
// For best results all possible dynamic types of T should implement
// fmt.Stringer.
func (g DirectedGraph[T]) StringForComparison() string {
	var buf bytes.Buffer

	stringRepr := func(v any) string {
		switch v := v.(type) {
		case fmt.Stringer:
			return v.String()
		default:
			return fmt.Sprintf("%#v", v)
		}
	}

	// We want the addresses in a consistent order but it doesn't really
	// matter what that order is, so we'll just do it lexically by each
	// type's standard string representation.
	nodeKeys := make([]UniqueKey, 0, len(g.nodes))
	for k := range g.nodes {
		nodeKeys = append(nodeKeys, k)
	}
	sort.Slice(nodeKeys, func(i, j int) bool {
		iStr := stringRepr(g.nodes[nodeKeys[i]])
		jStr := stringRepr(g.nodes[nodeKeys[j]])
		return iStr < jStr
	})

	for _, k := range nodeKeys {
		addr := g.nodes[k]
		fmt.Fprintf(&buf, "%s\n", stringRepr(addr))

		deps := g.DirectDependenciesOf(addr)
		if len(deps) == 0 {
			continue
		}

		depKeys := make([]UniqueKey, 0, len(deps))
		for k := range deps {
			depKeys = append(depKeys, k)
		}
		sort.Slice(depKeys, func(i, j int) bool {
			iStr := stringRepr(g.nodes[depKeys[i]])
			jStr := stringRepr(g.nodes[depKeys[j]])
			return iStr < jStr
		})
		for _, k := range depKeys {
			fmt.Fprintf(&buf, "  %s\n", stringRepr(g.nodes[k]))
		}
	}

	return buf.String()
}
