package terraform

import (
	version "github.com/hashicorp/go-version"

	"github.com/hashicorp/terraform-plugin-sdk/internal/addrs"
	"github.com/hashicorp/terraform-plugin-sdk/internal/configs"
	"github.com/hashicorp/terraform-plugin-sdk/internal/moduledeps"
	"github.com/hashicorp/terraform-plugin-sdk/internal/plugin/discovery"
	"github.com/hashicorp/terraform-plugin-sdk/internal/states"
)

// ConfigTreeDependencies returns the dependencies of the tree of modules
// described by the given configuration and state.
//
// Both configuration and state are required because there can be resources
// implied by instances in the state that no longer exist in config.
func ConfigTreeDependencies(root *configs.Config, state *states.State) *moduledeps.Module {
	// First we walk the configuration tree to build the overall structure
	// and capture the explicit/implicit/inherited provider dependencies.
	deps := configTreeConfigDependencies(root, nil)

	// Next we walk over the resources in the state to catch any additional
	// dependencies created by existing resources that are no longer in config.
	// Most things we find in state will already be present in 'deps', but
	// we're interested in the rare thing that isn't.
	configTreeMergeStateDependencies(deps, state)

	return deps
}

func configTreeConfigDependencies(root *configs.Config, inheritProviders map[string]*configs.Provider) *moduledeps.Module {
	if root == nil {
		// If no config is provided, we'll make a synthetic root.
		// This isn't necessarily correct if we're called with a nil that
		// *isn't* at the root, but in practice that can never happen.
		return &moduledeps.Module{
			Name:      "root",
			Providers: make(moduledeps.Providers),
		}
	}

	name := "root"
	if len(root.Path) != 0 {
		name = root.Path[len(root.Path)-1]
	}

	ret := &moduledeps.Module{
		Name: name,
	}

	module := root.Module

	// Provider dependencies
	{
		providers := make(moduledeps.Providers)

		// The main way to declare a provider dependency is explicitly inside
		// the "terraform" block, which allows declaring a requirement without
		// also creating a configuration.
		for fullName, constraints := range module.ProviderRequirements {
			inst := moduledeps.ProviderInstance(fullName)

			// The handling here is a bit fiddly because the moduledeps package
			// was designed around the legacy (pre-0.12) configuration model
			// and hasn't yet been revised to handle the new model. As a result,
			// we need to do some translation here.
			// FIXME: Eventually we should adjust the underlying model so we
			// can also retain the source location of each constraint, for
			// more informative output from the "terraform providers" command.
			var rawConstraints version.Constraints
			for _, constraint := range constraints {
				rawConstraints = append(rawConstraints, constraint.Required...)
			}
			discoConstraints := discovery.NewConstraints(rawConstraints)

			providers[inst] = moduledeps.ProviderDependency{
				Constraints: discoConstraints,
				Reason:      moduledeps.ProviderDependencyExplicit,
			}
		}

		// Provider configurations can also include version constraints,
		// allowing for more terse declaration in situations where both a
		// configuration and a constraint are defined in the same module.
		for fullName, pCfg := range module.ProviderConfigs {
			inst := moduledeps.ProviderInstance(fullName)
			discoConstraints := discovery.AllVersions
			if pCfg.Version.Required != nil {
				discoConstraints = discovery.NewConstraints(pCfg.Version.Required)
			}
			if existing, exists := providers[inst]; exists {
				existing.Constraints = existing.Constraints.Append(discoConstraints)
			} else {
				providers[inst] = moduledeps.ProviderDependency{
					Constraints: discoConstraints,
					Reason:      moduledeps.ProviderDependencyExplicit,
				}
			}
		}

		// Each resource in the configuration creates an *implicit* provider
		// dependency, though we'll only record it if there isn't already
		// an explicit dependency on the same provider.
		for _, rc := range module.ManagedResources {
			addr := rc.ProviderConfigAddr()
			inst := moduledeps.ProviderInstance(addr.StringCompact())
			if _, exists := providers[inst]; exists {
				// Explicit dependency already present
				continue
			}

			reason := moduledeps.ProviderDependencyImplicit
			if _, inherited := inheritProviders[addr.StringCompact()]; inherited {
				reason = moduledeps.ProviderDependencyInherited
			}

			providers[inst] = moduledeps.ProviderDependency{
				Constraints: discovery.AllVersions,
				Reason:      reason,
			}
		}
		for _, rc := range module.DataResources {
			addr := rc.ProviderConfigAddr()
			inst := moduledeps.ProviderInstance(addr.StringCompact())
			if _, exists := providers[inst]; exists {
				// Explicit dependency already present
				continue
			}

			reason := moduledeps.ProviderDependencyImplicit
			if _, inherited := inheritProviders[addr.String()]; inherited {
				reason = moduledeps.ProviderDependencyInherited
			}

			providers[inst] = moduledeps.ProviderDependency{
				Constraints: discovery.AllVersions,
				Reason:      reason,
			}
		}

		ret.Providers = providers
	}

	childInherit := make(map[string]*configs.Provider)
	for k, v := range inheritProviders {
		childInherit[k] = v
	}
	for k, v := range module.ProviderConfigs {
		childInherit[k] = v
	}
	for _, c := range root.Children {
		ret.Children = append(ret.Children, configTreeConfigDependencies(c, childInherit))
	}

	return ret
}

func configTreeMergeStateDependencies(root *moduledeps.Module, state *states.State) {
	if state == nil {
		return
	}

	findModule := func(path addrs.ModuleInstance) *moduledeps.Module {
		module := root
		for _, step := range path {
			var next *moduledeps.Module
			for _, cm := range module.Children {
				if cm.Name == step.Name {
					next = cm
					break
				}
			}

			if next == nil {
				// If we didn't find a next node, we'll need to make one
				next = &moduledeps.Module{
					Name:      step.Name,
					Providers: make(moduledeps.Providers),
				}
				module.Children = append(module.Children, next)
			}

			module = next
		}
		return module
	}

	for _, ms := range state.Modules {
		module := findModule(ms.Addr)

		for _, rs := range ms.Resources {
			inst := moduledeps.ProviderInstance(rs.ProviderConfig.ProviderConfig.StringCompact())
			if _, exists := module.Providers[inst]; !exists {
				module.Providers[inst] = moduledeps.ProviderDependency{
					Constraints: discovery.AllVersions,
					Reason:      moduledeps.ProviderDependencyFromState,
				}
			}
		}
	}
}
