// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package schema

import (
	"fmt"

	"github.com/hashicorp/go-cty/cty"

	"github.com/hashicorp/terraform-plugin-sdk/v2/internal/configs/configschema"
)

// StringKind represents the format a string is in.
type StringKind configschema.StringKind

const (
	// StringPlain indicates a string is plain-text and requires no processing for display.
	StringPlain = StringKind(configschema.StringPlain)

	// StringMarkdown indicates a string is in markdown format and may
	// require additional processing to display.
	StringMarkdown = StringKind(configschema.StringMarkdown)
)

var (
	// DescriptionKind is the default StringKind of descriptions in this provider.
	// It defaults to StringPlain but can be globally switched to StringMarkdown.
	DescriptionKind = StringPlain

	// SchemaDescriptionBuilder converts helper/schema.Schema Descriptions to configschema.Attribute
	// and Block Descriptions. This method can be used to modify the description text prior to it
	// being returned in the schema.
	SchemaDescriptionBuilder = func(s *Schema) string {
		return s.Description
	}

	// ResourceDescriptionBuilder converts helper/schema.Resource Descriptions to configschema.Block
	// Descriptions at the resource top level. This method can be used to modify the description prior
	// to it being returned in the schema.
	ResourceDescriptionBuilder = func(r *Resource) string {
		return r.Description
	}
)

// The functions and methods in this file are concerned with the conversion
// of this package's schema model into the slightly-lower-level schema model
// used by Terraform core for configuration parsing.

// CoreConfigSchema lowers the receiver to the schema model expected by
// Terraform core.
//
// This lower-level model has fewer features than the schema in this package,
// describing only the basic structure of configuration and state values we
// expect. The full schemaMap from this package is still required for full
// validation, handling of default values, etc.
//
// This method presumes a schema that passes InternalValidate, and so may
// panic or produce an invalid result if given an invalid schemaMap.
func (m schemaMap) CoreConfigSchema() *configschema.Block {
	if len(m) == 0 {
		// We return an actual (empty) object here, rather than a nil,
		// because a nil result would mean that we don't have a schema at
		// all, rather than that we have an empty one.
		return &configschema.Block{}
	}

	ret := &configschema.Block{
		Attributes: map[string]*configschema.Attribute{},
		BlockTypes: map[string]*configschema.NestedBlock{},
	}

	for name, schema := range m {
		if schema.Elem == nil {
			ret.Attributes[name] = schema.coreConfigSchemaAttribute()
			continue
		}
		if schema.Type == TypeMap {
			// For TypeMap in particular, it isn't valid for Elem to be a
			// *Resource (since that would be ambiguous in flatmap) and
			// so Elem is treated as a TypeString schema if so. This matches
			// how the field readers treat this situation, for compatibility
			// with configurations targeting Terraform 0.11 and earlier.
			if _, isResource := schema.Elem.(*Resource); isResource {
				sch := *schema // shallow copy
				sch.Elem = &Schema{
					Type: TypeString,
				}
				ret.Attributes[name] = sch.coreConfigSchemaAttribute()
				continue
			}
		}
		switch schema.ConfigMode {
		case SchemaConfigModeAttr:
			ret.Attributes[name] = schema.coreConfigSchemaAttribute()
		case SchemaConfigModeBlock:
			ret.BlockTypes[name] = schema.coreConfigSchemaBlock()
		default: // SchemaConfigModeAuto, or any other invalid value
			if schema.Computed && !schema.Optional {
				// Computed-only schemas are always handled as attributes,
				// because they never appear in configuration.
				ret.Attributes[name] = schema.coreConfigSchemaAttribute()
				continue
			}
			switch schema.Elem.(type) {
			case *Schema, ValueType:
				ret.Attributes[name] = schema.coreConfigSchemaAttribute()
			case *Resource:
				ret.BlockTypes[name] = schema.coreConfigSchemaBlock()
			default:
				// Should never happen for a valid schema
				panic(fmt.Errorf("invalid Schema.Elem %#v; need *Schema or *Resource", schema.Elem))
			}
		}
	}

	return ret
}

// coreConfigSchemaAttribute prepares a configschema.Attribute representation
// of a schema. This is appropriate only for primitives or collections whose
// Elem is an instance of Schema. Use coreConfigSchemaBlock for collections
// whose elem is a whole resource.
func (s *Schema) coreConfigSchemaAttribute() *configschema.Attribute {
	// The Schema.DefaultFunc capability adds some extra weirdness here since
	// it can be combined with "Required: true" to create a sitution where
	// required-ness is conditional. Terraform Core doesn't share this concept,
	// so we must sniff for this possibility here and conditionally turn
	// off the "Required" flag if it looks like the DefaultFunc is going
	// to provide a value.
	// This is not 100% true to the original interface of DefaultFunc but
	// works well enough for the EnvDefaultFunc and MultiEnvDefaultFunc
	// situations, which are the main cases we care about.
	//
	// Note that this also has a consequence for commands that return schema
	// information for documentation purposes: running those for certain
	// providers will produce different results depending on which environment
	// variables are set. We accept that weirdness in order to keep this
	// interface to core otherwise simple.
	reqd := s.Required
	opt := s.Optional
	if reqd && s.DefaultFunc != nil {
		v, err := s.DefaultFunc()
		// We can't report errors from here, so we'll instead just force
		// "Required" to false and let the provider try calling its
		// DefaultFunc again during the validate step, where it can then
		// return the error.
		if err != nil || (err == nil && v != nil) {
			reqd = false
			opt = true
		}
	}

	desc := SchemaDescriptionBuilder(s)
	descKind := configschema.StringKind(DescriptionKind)
	if desc == "" {
		// fallback to plain text if empty
		descKind = configschema.StringPlain
	}

	return &configschema.Attribute{
		Type:            s.coreConfigSchemaType(),
		Optional:        opt,
		Required:        reqd,
		Computed:        s.Computed,
		Sensitive:       s.Sensitive,
		Description:     desc,
		DescriptionKind: descKind,
		Deprecated:      s.Deprecated != "",
	}
}

// coreConfigSchemaBlock prepares a configschema.NestedBlock representation of
// a schema. This is appropriate only for collections whose Elem is an instance
// of Resource, and will panic otherwise.
func (s *Schema) coreConfigSchemaBlock() *configschema.NestedBlock {
	ret := &configschema.NestedBlock{}
	if nested := s.Elem.(*Resource).coreConfigSchema(); nested != nil {
		ret.Block = *nested

		desc := SchemaDescriptionBuilder(s)
		descKind := configschema.StringKind(DescriptionKind)
		if desc == "" {
			// fallback to plain text if empty
			descKind = configschema.StringPlain
		}
		// set these on the block from the attribute Schema
		ret.Block.Description = desc
		ret.Block.DescriptionKind = descKind
		ret.Block.Deprecated = s.Deprecated != ""
	}
	switch s.Type {
	case TypeList:
		ret.Nesting = configschema.NestingList
	case TypeSet:
		ret.Nesting = configschema.NestingSet
	case TypeMap:
		ret.Nesting = configschema.NestingMap
	default:
		// Should never happen for a valid schema
		panic(fmt.Errorf("invalid s.Type %s for s.Elem being resource", s.Type))
	}

	ret.MinItems = s.MinItems
	ret.MaxItems = s.MaxItems

	if s.Required && s.MinItems == 0 {
		// configschema doesn't have a "required" representation for nested
		// blocks, but we can fake it by requiring at least one item.
		ret.MinItems = 1
	}
	if s.Optional && s.MinItems > 0 {
		// Historically helper/schema would ignore MinItems if Optional were
		// set, so we must mimic this behavior here to ensure that providers
		// relying on that undocumented behavior can continue to operate as
		// they did before.
		ret.MinItems = 0
	}
	if s.Computed && !s.Optional {
		// MinItems/MaxItems are meaningless for computed nested blocks, since
		// they are never set by the user anyway. This ensures that we'll never
		// generate weird errors about them.
		ret.MinItems = 0
		ret.MaxItems = 0
	}

	return ret
}

// coreConfigSchemaType determines the core config schema type that corresponds
// to a particular schema's type.
func (s *Schema) coreConfigSchemaType() cty.Type {
	switch s.Type {
	case TypeString:
		return cty.String
	case TypeBool:
		return cty.Bool
	case TypeInt, TypeFloat:
		// configschema doesn't distinguish int and float, so helper/schema
		// will deal with this as an additional validation step after
		// configuration has been parsed and decoded.
		return cty.Number
	case TypeList, TypeSet, TypeMap:
		var elemType cty.Type
		switch set := s.Elem.(type) {
		case *Schema:
			elemType = set.coreConfigSchemaType()
		case ValueType:
			// This represents a mistake in the provider code, but it's a
			// common one so we'll just shim it.
			elemType = (&Schema{Type: set}).coreConfigSchemaType()
		case *Resource:
			// By default we construct a NestedBlock in this case, but this
			// behavior is selected either for computed-only schemas or
			// when ConfigMode is explicitly SchemaConfigModeBlock.
			// See schemaMap.CoreConfigSchema for the exact rules.
			elemType = set.coreConfigSchema().ImpliedType()
		default:
			if set != nil {
				// Should never happen for a valid schema
				panic(fmt.Errorf("invalid Schema.Elem %#v; need *Schema or *Resource", s.Elem))
			}
			// Some pre-existing schemas assume string as default, so we need
			// to be compatible with them.
			elemType = cty.String
		}
		switch s.Type {
		case TypeList:
			return cty.List(elemType)
		case TypeSet:
			return cty.Set(elemType)
		case TypeMap:
			return cty.Map(elemType)
		default:
			// can never get here in practice, due to the case we're inside
			panic("invalid collection type")
		}
	default:
		// should never happen for a valid schema
		panic(fmt.Errorf("invalid Schema.Type %s", s.Type))
	}
}

// CoreConfigSchema is a convenient shortcut for calling CoreConfigSchema on
// the resource's schema. CoreConfigSchema adds the implicitly required "id"
// attribute for top level resources if it doesn't exist.
func (r *Resource) CoreConfigSchema() *configschema.Block {
	block := r.coreConfigSchema()

	desc := ResourceDescriptionBuilder(r)
	descKind := configschema.StringKind(DescriptionKind)
	if desc == "" {
		// fallback to plain text if empty
		descKind = configschema.StringPlain
	}

	// Only apply Resource Description, Kind, Deprecation at top level
	block.Description = desc
	block.DescriptionKind = descKind
	block.Deprecated = r.DeprecationMessage != ""

	if block.Attributes == nil {
		block.Attributes = map[string]*configschema.Attribute{}
	}

	// Add the implicitly required "id" field if it doesn't exist
	if block.Attributes["id"] == nil {
		block.Attributes["id"] = &configschema.Attribute{
			Type:     cty.String,
			Optional: true,
			Computed: true,
		}
	}

	_, timeoutsAttr := block.Attributes[TimeoutsConfigKey]
	_, timeoutsBlock := block.BlockTypes[TimeoutsConfigKey]

	// Insert configured timeout values into the schema, as long as the schema
	// didn't define anything else by that name.
	if r.Timeouts != nil && !timeoutsAttr && !timeoutsBlock {
		timeouts := configschema.Block{
			Attributes: map[string]*configschema.Attribute{},
		}

		if r.Timeouts.Create != nil {
			timeouts.Attributes[TimeoutCreate] = &configschema.Attribute{
				Type:     cty.String,
				Optional: true,
			}
		}

		if r.Timeouts.Read != nil {
			timeouts.Attributes[TimeoutRead] = &configschema.Attribute{
				Type:     cty.String,
				Optional: true,
			}
		}

		if r.Timeouts.Update != nil {
			timeouts.Attributes[TimeoutUpdate] = &configschema.Attribute{
				Type:     cty.String,
				Optional: true,
			}
		}

		if r.Timeouts.Delete != nil {
			timeouts.Attributes[TimeoutDelete] = &configschema.Attribute{
				Type:     cty.String,
				Optional: true,
			}
		}

		if r.Timeouts.Default != nil {
			timeouts.Attributes[TimeoutDefault] = &configschema.Attribute{
				Type:     cty.String,
				Optional: true,
			}
		}

		block.BlockTypes[TimeoutsConfigKey] = &configschema.NestedBlock{
			Nesting: configschema.NestingSingle,
			Block:   timeouts,
		}
	}

	return block
}

func (r *Resource) coreConfigSchema() *configschema.Block {
	return schemaMap(r.SchemaMap()).CoreConfigSchema()
}
