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

package loadschemas

import (
	"fmt"
	"log"

	"github.com/hashicorp/hcl/v2/hclsyntax"
	"github.com/hashicorp/terraform/internal/addrs"
	"github.com/hashicorp/terraform/internal/configs/configschema"
	"github.com/hashicorp/terraform/internal/providers"
	"github.com/hashicorp/terraform/internal/provisioners"
)

// Plugins represents a library of available plugins for which it's safe
// to cache certain information for performance reasons.
type Plugins struct {
	providerFactories    map[addrs.Provider]providers.Factory
	provisionerFactories map[string]provisioners.Factory

	preloadedProviderSchemas map[addrs.Provider]providers.ProviderSchema
}

func NewPlugins(
	providerFactories map[addrs.Provider]providers.Factory,
	provisionerFactories map[string]provisioners.Factory,
	preloadedProviderSchemas map[addrs.Provider]providers.ProviderSchema,
) *Plugins {
	ret := &Plugins{
		providerFactories:        providerFactories,
		provisionerFactories:     provisionerFactories,
		preloadedProviderSchemas: preloadedProviderSchemas,
	}
	return ret
}

// ProviderFactories returns a map of all of the registered provider factories.
//
// Callers must not modify the returned map and must not access it concurrently
// with any other method of this type.
func (cp *Plugins) ProviderFactories() map[addrs.Provider]providers.Factory {
	return cp.providerFactories
}

func (cp *Plugins) HasProvider(addr addrs.Provider) bool {
	_, ok := cp.providerFactories[addr]
	return ok
}

func (cp *Plugins) HasPreloadedSchemaForProvider(addr addrs.Provider) bool {
	_, ok := cp.preloadedProviderSchemas[addr]
	return ok
}

func (cp *Plugins) NewProviderInstance(addr addrs.Provider) (providers.Interface, error) {
	f, ok := cp.providerFactories[addr]
	if !ok {
		return nil, fmt.Errorf("unavailable provider %q", addr.String())
	}

	return f()
}

// ProvisionerFactories returns a map of all of the registered provisioner
// factories.
//
// Callers must not modify the returned map and must not access it concurrently
// with any other method of this type.
func (cp *Plugins) ProvisionerFactories() map[string]provisioners.Factory {
	return cp.provisionerFactories
}

func (cp *Plugins) HasProvisioner(typ string) bool {
	_, ok := cp.provisionerFactories[typ]
	return ok
}

func (cp *Plugins) NewProvisionerInstance(typ string) (provisioners.Interface, error) {
	f, ok := cp.provisionerFactories[typ]
	if !ok {
		return nil, fmt.Errorf("unavailable provisioner %q", typ)
	}

	return f()
}

// ProviderSchema uses a temporary instance of the provider with the given
// address to obtain the full schema for all aspects of that provider.
//
// ProviderSchema memoizes results by unique provider address, so it's fine
// to repeatedly call this method with the same address if various different
// parts of Terraform all need the same schema information.
func (cp *Plugins) ProviderSchema(addr addrs.Provider) (providers.ProviderSchema, error) {
	// Check the global schema cache first.
	// This cache is only written by the provider client, and transparently
	// used by GetProviderSchema, but we check it here because at this point we
	// may be able to avoid spinning up the provider instance at all.
	// We skip this if we have preloaded schemas because that suggests that
	// our caller is not Terraform CLI and therefore it's probably inappropriate
	// to assume that provider schemas are unique process-wide.
	schemas, ok := providers.SchemaCache.Get(addr)
	if ok {
		log.Printf("[TRACE] terraform.contextPlugins: Schema for provider %q is in the global cache", addr)
		return schemas, nil
	}

	// We might have a non-global preloaded copy of this provider's schema.
	if schema, ok := cp.preloadedProviderSchemas[addr]; ok {
		log.Printf("[TRACE] terraform.contextPlugins: Provider %q has a preloaded schema", addr)
		return schema, nil
	}

	log.Printf("[TRACE] terraform.contextPlugins: Initializing provider %q to read its schema", addr)
	provider, err := cp.NewProviderInstance(addr)
	if err != nil {
		return schemas, fmt.Errorf("failed to instantiate provider %q to obtain schema: %s", addr, err)
	}
	defer provider.Close()

	resp := provider.GetProviderSchema()
	if resp.Diagnostics.HasErrors() {
		return resp, fmt.Errorf("failed to retrieve schema from provider %q: %s", addr, resp.Diagnostics.Err())
	}

	if resp.Provider.Version < 0 {
		// We're not using the version numbers here yet, but we'll check
		// for validity anyway in case we start using them in future.
		return resp, fmt.Errorf("provider %s has invalid negative schema version for its configuration blocks,which is a bug in the provider ", addr)
	}

	for t, r := range resp.ResourceTypes {
		if err := r.Body.InternalValidate(); err != nil {
			return resp, fmt.Errorf("provider %s has invalid schema for managed resource type %q, which is a bug in the provider: %q", addr, t, err)
		}
		if r.Version < 0 {
			return resp, fmt.Errorf("provider %s has invalid negative schema version for managed resource type %q, which is a bug in the provider", addr, t)
		}

		// Validate resource identity schema if the resource has one
		if r.Identity != nil {
			if err := r.Identity.InternalValidate(); err != nil {
				return resp, fmt.Errorf("provider %s has invalid identity schema for managed resource type %q, which is a bug in the provider: %q", addr, t, err)
			}
			if r.IdentityVersion < 0 {
				return resp, fmt.Errorf("provider %s has invalid negative identity schema version for managed resource type %q, which is a bug in the provider", addr, t)
			}

			for attrName, attrTy := range r.Identity.ImpliedType().AttributeTypes() {
				if attrTy.MapElementType() != nil {
					return resp, fmt.Errorf("provider %s has invalid schema for managed resource type %q, attribute %q is a map, which is not allowed in identity schemas", addr, t, attrName)
				}

				if attrTy.SetElementType() != nil {
					return resp, fmt.Errorf("provider %s has invalid schema for managed resource type %q, attribute %q is a set, which is not allowed in identity schemas", addr, t, attrName)
				}

				if attrTy.IsObjectType() {
					return resp, fmt.Errorf("provider %s has invalid schema for managed resource type %q, attribute %q is an object, which is not allowed in identity schemas", addr, t, attrName)
				}
			}
		}
	}

	for t, d := range resp.DataSources {
		if err := d.Body.InternalValidate(); err != nil {
			return resp, fmt.Errorf("provider %s has invalid schema for data resource type %q, which is a bug in the provider: %q", addr, t, err)
		}
		if d.Version < 0 {
			// We're not using the version numbers here yet, but we'll check
			// for validity anyway in case we start using them in future.
			return resp, fmt.Errorf("provider %s has invalid negative schema version for data resource type %q, which is a bug in the provider", addr, t)
		}
	}

	for t, r := range resp.EphemeralResourceTypes {
		if err := r.Body.InternalValidate(); err != nil {
			return resp, fmt.Errorf("provider %s has invalid schema for ephemeral resource type %q, which is a bug in the provider: %q", addr, t, err)
		}
	}

	for t, r := range resp.ListResourceTypes {
		if err := r.Body.InternalValidate(); err != nil {
			return resp, fmt.Errorf("provider %s has invalid schema for list resource type %q, which is a bug in the provider: %q", addr, t, err)
		}
	}

	for n, f := range resp.Functions {
		if !hclsyntax.ValidIdentifier(n) {
			return resp, fmt.Errorf("provider %s declares function with invalid name %q", addr, n)
		}
		// We'll also do some enforcement of parameter names, even though they
		// are only for docs/UI for now, to leave room for us to potentially
		// use them for other purposes later.
		seenParams := make(map[string]int, len(f.Parameters))
		for i, p := range f.Parameters {
			if !hclsyntax.ValidIdentifier(p.Name) {
				return resp, fmt.Errorf("provider %s function %q declares invalid name %q for parameter %d", addr, n, p.Name, i)
			}
			if prevIdx, exists := seenParams[p.Name]; exists {
				return resp, fmt.Errorf("provider %s function %q reuses name %q for both parameters %d and %d", addr, n, p.Name, prevIdx, i)
			}
			seenParams[p.Name] = i
		}
		if p := f.VariadicParameter; p != nil {
			if !hclsyntax.ValidIdentifier(p.Name) {
				return resp, fmt.Errorf("provider %s function %q declares invalid name %q for its variadic parameter", addr, n, p.Name)
			}
			if prevIdx, exists := seenParams[p.Name]; exists {
				return resp, fmt.Errorf("provider %s function %q reuses name %q for both parameter %d and its variadic parameter", addr, n, p.Name, prevIdx)
			}
		}
	}

	return resp, nil
}

// ProviderConfigSchema is a helper wrapper around ProviderSchema which first
// reads the full schema of the given provider and then extracts just the
// provider's configuration schema, which defines what's expected in a
// "provider" block in the configuration when configuring this provider.
func (cp *Plugins) ProviderConfigSchema(providerAddr addrs.Provider) (*configschema.Block, error) {
	providerSchema, err := cp.ProviderSchema(providerAddr)
	if err != nil {
		return nil, err
	}

	return providerSchema.Provider.Body, nil
}

// ResourceTypeSchema is a helper wrapper around ProviderSchema which first
// reads the schema of the given provider and then tries to find the schema
// for the resource type of the given resource mode in that provider.
//
// ResourceTypeSchema will return an error if the provider schema lookup
// fails, but will return an empty schema if the provider schema lookup
// succeeds but then the provider doesn't have a resource of the requested type.
func (cp *Plugins) ResourceTypeSchema(providerAddr addrs.Provider, resourceMode addrs.ResourceMode, resourceType string) (providers.Schema, error) {
	providerSchema, err := cp.ProviderSchema(providerAddr)
	if err != nil {
		return providers.Schema{}, err
	}

	return providerSchema.SchemaForResourceType(resourceMode, resourceType), nil
}

// ProvisionerSchema uses a temporary instance of the provisioner with the
// given type name to obtain the schema for that provisioner's configuration.
//
// ProvisionerSchema memoizes results by provisioner type name, so it's fine
// to repeatedly call this method with the same name if various different
// parts of Terraform all need the same schema information.
func (cp *Plugins) ProvisionerSchema(typ string) (*configschema.Block, error) {
	log.Printf("[TRACE] terraform.contextPlugins: Initializing provisioner %q to read its schema", typ)
	provisioner, err := cp.NewProvisionerInstance(typ)
	if err != nil {
		return nil, fmt.Errorf("failed to instantiate provisioner %q to obtain schema: %s", typ, err)
	}
	defer provisioner.Close()

	resp := provisioner.GetSchema()
	if resp.Diagnostics.HasErrors() {
		return nil, fmt.Errorf("failed to retrieve schema from provisioner %q: %s", typ, resp.Diagnostics.Err())
	}

	return resp.Provisioner, nil
}

func (cp *Plugins) ActionTypeSchema(providerAddr addrs.Provider, actionType string) (*providers.ActionSchema, error) {
	providerSchema, err := cp.ProviderSchema(providerAddr)
	if err != nil {
		return nil, err
	}

	actionSchema, ok := providerSchema.Actions[actionType]
	if !ok {
		return nil, fmt.Errorf("provider %s does not have an action schema for %s", providerAddr, actionType)
	}

	return &actionSchema, nil
}

// ProviderFunctionDecls is a helper wrapper around ProviderSchema which first
// reads the schema of the given provider and then returns all of the
// functions it declares, if any.
//
// ProviderFunctionDecl will return an error if the provider schema lookup
// fails, but will return an empty set of functions if a successful response
// returns no functions, or if the provider is using an older protocol version
// which has no support for provider-contributed functions.
func (cp *Plugins) ProviderFunctionDecls(providerAddr addrs.Provider) (map[string]providers.FunctionDecl, error) {
	providerSchema, err := cp.ProviderSchema(providerAddr)
	if err != nil {
		return nil, err
	}

	return providerSchema.Functions, nil
}
