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

package terraform

import (
	"fmt"
	"log"

	"github.com/hashicorp/terraform/internal/configs"
	"github.com/hashicorp/terraform/internal/configs/configschema"
	"github.com/hashicorp/terraform/internal/dag"
	"github.com/hashicorp/terraform/internal/providers"
)

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

	AttachResourceSchema(schema *providers.Schema)
}

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

	AttachProviderConfigSchema(*configschema.Block)
}

// GraphNodeAttachProvisionerSchema is an interface implemented by node types
// that need one or more provisioner schemas attached.
type GraphNodeAttachProvisionerSchema interface {
	ProvisionedBy() []string

	// SetProvisionerSchema is called during transform for each provisioner
	// type returned from ProvisionedBy, providing the configuration schema
	// for each provisioner in turn. The implementer should save these for
	// later use in evaluating provisioner configuration blocks.
	AttachProvisionerSchema(name string, schema *configschema.Block)
}

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

	AttachActionSchema(schema *providers.ActionSchema)
}

// AttachSchemaTransformer finds nodes that implement
// GraphNodeAttachResourceSchema, GraphNodeAttachProviderConfigSchema, or
// GraphNodeAttachProvisionerSchema, looks up the needed schemas for each
// and then passes them to a method implemented by the node.
type AttachSchemaTransformer struct {
	Plugins *contextPlugins
	Config  *configs.Config
}

func (t *AttachSchemaTransformer) Transform(g *Graph) error {
	if t.Plugins == nil {
		// Should never happen with a reasonable caller, but we'll return a
		// proper error here anyway so that we'll fail gracefully.
		return fmt.Errorf("AttachSchemaTransformer used with nil Plugins")
	}

	for _, v := range g.Vertices() {

		if tv, ok := v.(GraphNodeAttachResourceSchema); ok {
			addr := tv.ResourceAddr()
			mode := addr.Resource.Mode
			typeName := addr.Resource.Type
			providerFqn := tv.Provider().FQN()

			schema, err := t.Plugins.ResourceTypeSchema(providerFqn, mode, typeName)
			if err != nil {
				return fmt.Errorf("failed to read schema for %s in %s: %s", addr, providerFqn, err)
			}
			if schema.Body == nil {
				log.Printf("[ERROR] AttachSchemaTransformer: No resource schema available for %s", addr)
				continue
			}
			log.Printf("[TRACE] AttachSchemaTransformer: attaching resource schema to %s", dag.VertexName(v))
			tv.AttachResourceSchema(&schema)
		}

		if tv, ok := v.(GraphNodeAttachProviderConfigSchema); ok {
			providerAddr := tv.ProviderAddr()
			schema, err := t.Plugins.ProviderConfigSchema(providerAddr.Provider)
			if err != nil {
				return fmt.Errorf("failed to read provider configuration schema for %s: %s", providerAddr.Provider, err)
			}
			if schema == nil {
				log.Printf("[ERROR] AttachSchemaTransformer: No provider config schema available for %s", providerAddr)
				continue
			}
			log.Printf("[TRACE] AttachSchemaTransformer: attaching provider config schema to %s", dag.VertexName(v))
			tv.AttachProviderConfigSchema(schema)
		}

		if tv, ok := v.(GraphNodeAttachProvisionerSchema); ok {
			names := tv.ProvisionedBy()
			for _, name := range names {
				schema, err := t.Plugins.ProvisionerSchema(name)
				if err != nil {
					return fmt.Errorf("failed to read provisioner configuration schema for %q: %s", name, err)
				}
				if schema == nil {
					log.Printf("[ERROR] AttachSchemaTransformer: No schema available for provisioner %q on %q", name, dag.VertexName(v))
					continue
				}
				log.Printf("[TRACE] AttachSchemaTransformer: attaching provisioner %q config schema to %s", name, dag.VertexName(v))
				tv.AttachProvisionerSchema(name, schema)
			}
		}

		if tv, ok := v.(GraphNodeAttachActionSchema); ok {
			addr := tv.ActionAddr()
			providerFqn := tv.Provider().FQN()
			schema, err := t.Plugins.ActionTypeSchema(providerFqn, addr.Action.Type)
			if err != nil {
				return fmt.Errorf("failed to read schema for %s in %s: %s", addr, providerFqn, err)
			}
			if schema.ConfigSchema == nil {
				log.Printf("[ERROR] AttachSchemaTransformer: No action schema available for %s", addr)
				continue
			}
			log.Printf("[TRACE] AttachSchemaTransformer: attaching action schema to %s", dag.VertexName(v))
			tv.AttachActionSchema(schema)
		}
	}

	return nil
}
