package terraform

import (
	"fmt"
	"log"

	"github.com/hashicorp/terraform/dag"

	"github.com/hashicorp/terraform/addrs"

	"github.com/hashicorp/terraform/config/configschema"
)

// GraphNodeAttachResourceSchema is an interface implemented by node types
// that need a resource schema attached.
type GraphNodeAttachResourceSchema interface {
	GraphNodeResource
	GraphNodeProviderConsumer

	AttachResourceSchema(*configschema.Block)
}

// GraphNodeAttachProviderConfigSchema is an interface implemented by node types
// that need a provider configuration schema attached.
type GraphNodeAttachProviderConfigSchema interface {
	GraphNodeProvider

	AttachProviderConfigSchema(*configschema.Block)
}

// AttachSchemaTransformer finds nodes that implement either
// GraphNodeAttachResourceSchema or GraphNodeAttachProviderConfigSchema, looks up
// the schema for each, and then passes it to a method implemented by the
// node.
type AttachSchemaTransformer struct {
	Components contextComponentFactory
}

func (t *AttachSchemaTransformer) Transform(g *Graph) error {

	// First we'll figure out which provider types we need to fetch schemas for.
	needProviders := make(map[string]struct{})
	for _, v := range g.Vertices() {
		switch tv := v.(type) {
		case GraphNodeAttachResourceSchema:
			providerAddr, _ := tv.ProvidedBy()
			needProviders[providerAddr.ProviderConfig.Type] = struct{}{}
		case GraphNodeAttachProviderConfigSchema:
			providerAddr := tv.ProviderAddr()
			needProviders[providerAddr.ProviderConfig.Type] = struct{}{}
		}
	}

	// Now we'll fetch each one. This requires us to temporarily instantiate
	// them, though this is not a full bootstrap since we don't yet have
	// configuration information; the providers will be re-instantiated and
	// properly configured during the graph walk.
	schemas := make(map[string]*ProviderSchema)
	for typeName := range needProviders {
		log.Printf("[TRACE] AttachSchemaTransformer: retrieving schema for provider type %q", typeName)
		provider, err := t.Components.ResourceProvider(typeName, "early/"+typeName)
		if err != nil {
			return fmt.Errorf("failed to instantiate provider %q to obtain schema: %s", typeName, err)
		}

		// FIXME: The provider interface is currently awkward in that it
		// requires us to tell the provider which resources types and data
		// sources we need. In future this will change to just return
		// everything available, but for now we'll fake that by fetching all
		// of the available names and then requesting them.
		resourceTypes := provider.Resources()
		dataSources := provider.DataSources()
		resourceTypeNames := make([]string, len(resourceTypes))
		for i, o := range resourceTypes {
			resourceTypeNames[i] = o.Name
		}
		dataSourceNames := make([]string, len(dataSources))
		for i, o := range dataSources {
			dataSourceNames[i] = o.Name
		}

		schema, err := provider.GetSchema(&ProviderSchemaRequest{
			ResourceTypes: resourceTypeNames,
			DataSources:   dataSourceNames,
		})
		if err != nil {
			return fmt.Errorf("failed to retrieve schema from provider %q: %s", typeName, err)
		}

		schemas[typeName] = schema

		if closer, ok := provider.(ResourceProviderCloser); ok {
			closer.Close()
		}
	}

	// Finally we'll once again visit all of the vertices and attach to
	// them the schemas we found for them.
	for _, v := range g.Vertices() {
		switch tv := v.(type) {
		case GraphNodeAttachResourceSchema:
			addr := tv.ResourceAddr()
			mode := addr.Resource.Mode
			typeName := addr.Resource.Type
			providerAddr, _ := tv.ProvidedBy()
			var schema *configschema.Block
			providerSchema := schemas[providerAddr.ProviderConfig.Type]
			if providerSchema == nil {
				log.Printf("[ERROR] AttachSchemaTransformer: No schema available for %s because provider schema for %q is missing", addr, providerAddr.ProviderConfig.Type)
				continue
			}
			switch mode {
			case addrs.ManagedResourceMode:
				schema = providerSchema.ResourceTypes[typeName]
			case addrs.DataResourceMode:
				schema = providerSchema.DataSources[typeName]
			}
			if schema != nil {
				log.Printf("[TRACE] AttachSchemaTransformer: attaching schema to %s", dag.VertexName(v))
				tv.AttachResourceSchema(schema)
			} else {
				log.Printf("[ERROR] AttachSchemaTransformer: No schema available for %s", addr)
			}
		case GraphNodeAttachProviderConfigSchema:
			providerAddr := tv.ProviderAddr()
			providerSchema := schemas[providerAddr.ProviderConfig.Type]
			if providerSchema == nil {
				log.Printf("[ERROR] AttachSchemaTransformer: No schema available for %s because the whole provider schema is missing", providerAddr)
				continue
			}

			schema := schemas[providerAddr.ProviderConfig.Type].Provider

			if schema != nil {
				log.Printf("[TRACE] AttachSchemaTransformer: attaching schema to %s", dag.VertexName(v))
				tv.AttachProviderConfigSchema(schema)
			} else {
				log.Printf("[ERROR] AttachSchemaTransformer: No schema available for %s", providerAddr)
			}
		}
	}

	return nil
}