package terraform

import (
	"fmt"
	"log"
	"sort"

	"github.com/hashicorp/terraform-plugin-sdk/internal/addrs"
	"github.com/hashicorp/terraform-plugin-sdk/internal/configs"
	"github.com/hashicorp/terraform-plugin-sdk/internal/configs/configschema"
	"github.com/hashicorp/terraform-plugin-sdk/internal/dag"
	"github.com/hashicorp/terraform-plugin-sdk/internal/lang"
	"github.com/hashicorp/terraform-plugin-sdk/internal/states"
	"github.com/hashicorp/terraform-plugin-sdk/tfdiags"
)

// ConcreteResourceNodeFunc is a callback type used to convert an
// abstract resource to a concrete one of some type.
type ConcreteResourceNodeFunc func(*NodeAbstractResource) dag.Vertex

// GraphNodeResource is implemented by any nodes that represent a resource.
// The type of operation cannot be assumed, only that this node represents
// the given resource.
type GraphNodeResource interface {
	ResourceAddr() addrs.AbsResource
}

// ConcreteResourceInstanceNodeFunc is a callback type used to convert an
// abstract resource instance to a concrete one of some type.
type ConcreteResourceInstanceNodeFunc func(*NodeAbstractResourceInstance) dag.Vertex

// GraphNodeResourceInstance is implemented by any nodes that represent
// a resource instance. A single resource may have multiple instances if,
// for example, the "count" or "for_each" argument is used for it in
// configuration.
type GraphNodeResourceInstance interface {
	ResourceInstanceAddr() addrs.AbsResourceInstance
}

// NodeAbstractResource represents a resource that has no associated
// operations. It registers all the interfaces for a resource that common
// across multiple operation types.
type NodeAbstractResource struct {
	Addr addrs.AbsResource // Addr is the address for this resource

	// The fields below will be automatically set using the Attach
	// interfaces if you're running those transforms, but also be explicitly
	// set if you already have that information.

	Schema        *configschema.Block // Schema for processing the configuration body
	SchemaVersion uint64              // Schema version of "Schema", as decided by the provider
	Config        *configs.Resource   // Config is the resource in the config

	ProvisionerSchemas map[string]*configschema.Block

	Targets []addrs.Targetable // Set from GraphNodeTargetable

	// The address of the provider this resource will use
	ResolvedProvider addrs.AbsProviderConfig
}

var (
	_ GraphNodeSubPath                 = (*NodeAbstractResource)(nil)
	_ GraphNodeReferenceable           = (*NodeAbstractResource)(nil)
	_ GraphNodeReferencer              = (*NodeAbstractResource)(nil)
	_ GraphNodeProviderConsumer        = (*NodeAbstractResource)(nil)
	_ GraphNodeProvisionerConsumer     = (*NodeAbstractResource)(nil)
	_ GraphNodeResource                = (*NodeAbstractResource)(nil)
	_ GraphNodeAttachResourceConfig    = (*NodeAbstractResource)(nil)
	_ GraphNodeAttachResourceSchema    = (*NodeAbstractResource)(nil)
	_ GraphNodeAttachProvisionerSchema = (*NodeAbstractResource)(nil)
	_ GraphNodeTargetable              = (*NodeAbstractResource)(nil)
	_ dag.GraphNodeDotter              = (*NodeAbstractResource)(nil)
)

// NewNodeAbstractResource creates an abstract resource graph node for
// the given absolute resource address.
func NewNodeAbstractResource(addr addrs.AbsResource) *NodeAbstractResource {
	return &NodeAbstractResource{
		Addr: addr,
	}
}

// NodeAbstractResourceInstance represents a resource instance with no
// associated operations. It embeds NodeAbstractResource but additionally
// contains an instance key, used to identify one of potentially many
// instances that were created from a resource in configuration, e.g. using
// the "count" or "for_each" arguments.
type NodeAbstractResourceInstance struct {
	NodeAbstractResource
	InstanceKey addrs.InstanceKey

	// The fields below will be automatically set using the Attach
	// interfaces if you're running those transforms, but also be explicitly
	// set if you already have that information.

	ResourceState *states.Resource
}

var (
	_ GraphNodeSubPath                 = (*NodeAbstractResourceInstance)(nil)
	_ GraphNodeReferenceable           = (*NodeAbstractResourceInstance)(nil)
	_ GraphNodeReferencer              = (*NodeAbstractResourceInstance)(nil)
	_ GraphNodeProviderConsumer        = (*NodeAbstractResourceInstance)(nil)
	_ GraphNodeProvisionerConsumer     = (*NodeAbstractResourceInstance)(nil)
	_ GraphNodeResource                = (*NodeAbstractResourceInstance)(nil)
	_ GraphNodeResourceInstance        = (*NodeAbstractResourceInstance)(nil)
	_ GraphNodeAttachResourceState     = (*NodeAbstractResourceInstance)(nil)
	_ GraphNodeAttachResourceConfig    = (*NodeAbstractResourceInstance)(nil)
	_ GraphNodeAttachResourceSchema    = (*NodeAbstractResourceInstance)(nil)
	_ GraphNodeAttachProvisionerSchema = (*NodeAbstractResourceInstance)(nil)
	_ GraphNodeTargetable              = (*NodeAbstractResourceInstance)(nil)
	_ dag.GraphNodeDotter              = (*NodeAbstractResourceInstance)(nil)
)

// NewNodeAbstractResourceInstance creates an abstract resource instance graph
// node for the given absolute resource instance address.
func NewNodeAbstractResourceInstance(addr addrs.AbsResourceInstance) *NodeAbstractResourceInstance {
	// Due to the fact that we embed NodeAbstractResource, the given address
	// actually ends up split between the resource address in the embedded
	// object and the InstanceKey field in our own struct. The
	// ResourceInstanceAddr method will stick these back together again on
	// request.
	return &NodeAbstractResourceInstance{
		NodeAbstractResource: NodeAbstractResource{
			Addr: addr.ContainingResource(),
		},
		InstanceKey: addr.Resource.Key,
	}
}

func (n *NodeAbstractResource) Name() string {
	return n.ResourceAddr().String()
}

func (n *NodeAbstractResourceInstance) Name() string {
	return n.ResourceInstanceAddr().String()
}

// GraphNodeSubPath
func (n *NodeAbstractResource) Path() addrs.ModuleInstance {
	return n.Addr.Module
}

// GraphNodeReferenceable
func (n *NodeAbstractResource) ReferenceableAddrs() []addrs.Referenceable {
	return []addrs.Referenceable{n.Addr.Resource}
}

// GraphNodeReferenceable
func (n *NodeAbstractResourceInstance) ReferenceableAddrs() []addrs.Referenceable {
	addr := n.ResourceInstanceAddr()
	return []addrs.Referenceable{
		addr.Resource,

		// A resource instance can also be referenced by the address of its
		// containing resource, so that e.g. a reference to aws_instance.foo
		// would match both aws_instance.foo[0] and aws_instance.foo[1].
		addr.ContainingResource().Resource,
	}
}

// GraphNodeReferencer
func (n *NodeAbstractResource) References() []*addrs.Reference {
	// If we have a config then we prefer to use that.
	if c := n.Config; c != nil {
		var result []*addrs.Reference

		for _, traversal := range c.DependsOn {
			ref, err := addrs.ParseRef(traversal)
			if err != nil {
				// We ignore this here, because this isn't a suitable place to return
				// errors. This situation should be caught and rejected during
				// validation.
				log.Printf("[ERROR] Can't parse %#v from depends_on as reference: %s", traversal, err)
				continue
			}

			result = append(result, ref)
		}

		if n.Schema == nil {
			// Should never happens, but we'll log if it does so that we can
			// see this easily when debugging.
			log.Printf("[WARN] no schema is attached to %s, so config references cannot be detected", n.Name())
		}

		refs, _ := lang.ReferencesInExpr(c.Count)
		result = append(result, refs...)
		refs, _ = lang.ReferencesInExpr(c.ForEach)
		result = append(result, refs...)
		refs, _ = lang.ReferencesInBlock(c.Config, n.Schema)
		result = append(result, refs...)
		if c.Managed != nil {
			for _, p := range c.Managed.Provisioners {
				if p.When != configs.ProvisionerWhenCreate {
					continue
				}
				if p.Connection != nil {
					refs, _ = lang.ReferencesInBlock(p.Connection.Config, connectionBlockSupersetSchema)
					result = append(result, refs...)
				}

				schema := n.ProvisionerSchemas[p.Type]
				if schema == nil {
					log.Printf("[WARN] no schema for provisioner %q is attached to %s, so provisioner block references cannot be detected", p.Type, n.Name())
				}
				refs, _ = lang.ReferencesInBlock(p.Config, schema)
				result = append(result, refs...)
			}
		}
		return result
	}

	// Otherwise, we have no references.
	return nil
}

// GraphNodeReferencer
func (n *NodeAbstractResourceInstance) References() []*addrs.Reference {
	// If we have a configuration attached then we'll delegate to our
	// embedded abstract resource, which knows how to extract dependencies
	// from configuration.
	if n.Config != nil {
		if n.Schema == nil {
			// We'll produce a log message about this out here so that
			// we can include the full instance address, since the equivalent
			// message in NodeAbstractResource.References cannot see it.
			log.Printf("[WARN] no schema is attached to %s, so config references cannot be detected", n.Name())
			return nil
		}
		return n.NodeAbstractResource.References()
	}

	// Otherwise, if we have state then we'll use the values stored in state
	// as a fallback.
	if rs := n.ResourceState; rs != nil {
		if s := rs.Instance(n.InstanceKey); s != nil {
			// State is still storing dependencies as old-style strings, so we'll
			// need to do a little work here to massage this to the form we now
			// want.
			var result []*addrs.Reference

			// It is (apparently) possible for s.Current to be nil. This proved
			// difficult to reproduce, so we will fix the symptom here and hope
			// to find the root cause another time.
			//
			// https://github.com/hashicorp/terraform-plugin-sdk/issues/21407
			if s.Current == nil {
				log.Printf("[WARN] no current state found for %s", n.Name())
			} else {
				for _, addr := range s.Current.Dependencies {
					if addr == nil {
						// Should never happen; indicates a bug in the state loader
						panic(fmt.Sprintf("dependencies for current object on %s contains nil address", n.ResourceInstanceAddr()))
					}

					// This is a little weird: we need to manufacture an addrs.Reference
					// with a fake range here because the state isn't something we can
					// make source references into.
					result = append(result, &addrs.Reference{
						Subject: addr,
						SourceRange: tfdiags.SourceRange{
							Filename: "(state file)",
						},
					})
				}
			}
			return result
		}
	}

	// If we have neither config nor state then we have no references.
	return nil
}

// StateReferences returns the dependencies to put into the state for
// this resource.
func (n *NodeAbstractResourceInstance) StateReferences() []addrs.Referenceable {
	selfAddrs := n.ReferenceableAddrs()

	// Since we don't include the source location references in our
	// results from this method, we'll also filter out duplicates:
	// there's no point in listing the same object twice without
	// that additional context.
	seen := map[string]struct{}{}

	// Pretend that we've already "seen" all of our own addresses so that we
	// won't record self-references in the state. This can arise if, for
	// example, a provisioner for a resource refers to the resource itself,
	// which is valid (since provisioners always run after apply) but should
	// not create an explicit dependency edge.
	for _, selfAddr := range selfAddrs {
		seen[selfAddr.String()] = struct{}{}
		if riAddr, ok := selfAddr.(addrs.ResourceInstance); ok {
			seen[riAddr.ContainingResource().String()] = struct{}{}
		}
	}

	depsRaw := n.References()
	deps := make([]addrs.Referenceable, 0, len(depsRaw))
	for _, d := range depsRaw {
		subj := d.Subject
		if mco, isOutput := subj.(addrs.ModuleCallOutput); isOutput {
			// For state dependencies, we simplify outputs to just refer
			// to the module as a whole. It's not really clear why we do this,
			// but this logic is preserved from before the 0.12 rewrite of
			// this function.
			subj = mco.Call
		}

		k := subj.String()
		if _, exists := seen[k]; exists {
			continue
		}
		seen[k] = struct{}{}
		switch tr := subj.(type) {
		case addrs.ResourceInstance:
			deps = append(deps, tr)
		case addrs.Resource:
			deps = append(deps, tr)
		case addrs.ModuleCallInstance:
			deps = append(deps, tr)
		default:
			// No other reference types are recorded in the state.
		}
	}

	// We'll also sort them, since that'll avoid creating changes in the
	// serialized state that make no semantic difference.
	sort.Slice(deps, func(i, j int) bool {
		// Simple string-based sort because we just care about consistency,
		// not user-friendliness.
		return deps[i].String() < deps[j].String()
	})

	return deps
}

func (n *NodeAbstractResource) SetProvider(p addrs.AbsProviderConfig) {
	n.ResolvedProvider = p
}

// GraphNodeProviderConsumer
func (n *NodeAbstractResource) ProvidedBy() (addrs.AbsProviderConfig, bool) {
	// If we have a config we prefer that above all else
	if n.Config != nil {
		relAddr := n.Config.ProviderConfigAddr()
		return relAddr.Absolute(n.Path()), false
	}

	// Use our type and containing module path to guess a provider configuration address
	return n.Addr.Resource.DefaultProviderConfig().Absolute(n.Addr.Module), false
}

// GraphNodeProviderConsumer
func (n *NodeAbstractResourceInstance) ProvidedBy() (addrs.AbsProviderConfig, bool) {
	// If we have a config we prefer that above all else
	if n.Config != nil {
		relAddr := n.Config.ProviderConfigAddr()
		return relAddr.Absolute(n.Path()), false
	}

	// If we have state, then we will use the provider from there
	if n.ResourceState != nil {
		// An address from the state must match exactly, since we must ensure
		// we refresh/destroy a resource with the same provider configuration
		// that created it.
		return n.ResourceState.ProviderConfig, true
	}

	// Use our type and containing module path to guess a provider configuration address
	return n.Addr.Resource.DefaultProviderConfig().Absolute(n.Path()), false
}

// GraphNodeProvisionerConsumer
func (n *NodeAbstractResource) ProvisionedBy() []string {
	// If we have no configuration, then we have no provisioners
	if n.Config == nil || n.Config.Managed == nil {
		return nil
	}

	// Build the list of provisioners we need based on the configuration.
	// It is okay to have duplicates here.
	result := make([]string, len(n.Config.Managed.Provisioners))
	for i, p := range n.Config.Managed.Provisioners {
		result[i] = p.Type
	}

	return result
}

// GraphNodeProvisionerConsumer
func (n *NodeAbstractResource) AttachProvisionerSchema(name string, schema *configschema.Block) {
	if n.ProvisionerSchemas == nil {
		n.ProvisionerSchemas = make(map[string]*configschema.Block)
	}
	n.ProvisionerSchemas[name] = schema
}

// GraphNodeResource
func (n *NodeAbstractResource) ResourceAddr() addrs.AbsResource {
	return n.Addr
}

// GraphNodeResourceInstance
func (n *NodeAbstractResourceInstance) ResourceInstanceAddr() addrs.AbsResourceInstance {
	return n.NodeAbstractResource.Addr.Instance(n.InstanceKey)
}

// GraphNodeAddressable, TODO: remove, used by target, should unify
func (n *NodeAbstractResource) ResourceAddress() *ResourceAddress {
	return NewLegacyResourceAddress(n.Addr)
}

// GraphNodeTargetable
func (n *NodeAbstractResource) SetTargets(targets []addrs.Targetable) {
	n.Targets = targets
}

// GraphNodeAttachResourceState
func (n *NodeAbstractResourceInstance) AttachResourceState(s *states.Resource) {
	n.ResourceState = s
}

// GraphNodeAttachResourceConfig
func (n *NodeAbstractResource) AttachResourceConfig(c *configs.Resource) {
	n.Config = c
}

// GraphNodeAttachResourceSchema impl
func (n *NodeAbstractResource) AttachResourceSchema(schema *configschema.Block, version uint64) {
	n.Schema = schema
	n.SchemaVersion = version
}

// GraphNodeDotter impl.
func (n *NodeAbstractResource) DotNode(name string, opts *dag.DotOpts) *dag.DotNode {
	return &dag.DotNode{
		Name: name,
		Attrs: map[string]string{
			"label": n.Name(),
			"shape": "box",
		},
	}
}
