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

package typeexpr

import (
	"fmt"

	"github.com/zclconf/go-cty/cty"
	"github.com/zclconf/go-cty/cty/convert"

	"github.com/hashicorp/hcl/v2"
)

const invalidTypeSummary = "Invalid type specification"

// getType is the internal implementation of Type, TypeConstraint, and
// TypeConstraintWithDefaults, using the passed flags to distinguish. When
// `constraint` is true, the "any" keyword can be used in place of a concrete
// type. When `withDefaults` is true, the "optional" call expression supports
// an additional argument describing a default value.
func getType(expr hcl.Expression, constraint, withDefaults bool) (cty.Type, *Defaults, hcl.Diagnostics) {
	// First we'll try for one of our keywords
	kw := hcl.ExprAsKeyword(expr)
	switch kw {
	case "bool":
		return cty.Bool, nil, nil
	case "string":
		return cty.String, nil, nil
	case "number":
		return cty.Number, nil, nil
	case "any":
		if constraint {
			return cty.DynamicPseudoType, nil, nil
		}
		return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
			Severity: hcl.DiagError,
			Summary:  invalidTypeSummary,
			Detail:   fmt.Sprintf("The keyword %q cannot be used in this type specification: an exact type is required.", kw),
			Subject:  expr.Range().Ptr(),
		}}
	case "list", "map", "set":
		return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
			Severity: hcl.DiagError,
			Summary:  invalidTypeSummary,
			Detail:   fmt.Sprintf("The %s type constructor requires one argument specifying the element type.", kw),
			Subject:  expr.Range().Ptr(),
		}}
	case "object":
		return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
			Severity: hcl.DiagError,
			Summary:  invalidTypeSummary,
			Detail:   "The object type constructor requires one argument specifying the attribute types and values as a map.",
			Subject:  expr.Range().Ptr(),
		}}
	case "tuple":
		return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
			Severity: hcl.DiagError,
			Summary:  invalidTypeSummary,
			Detail:   "The tuple type constructor requires one argument specifying the element types as a list.",
			Subject:  expr.Range().Ptr(),
		}}
	case "":
		// okay! we'll fall through and try processing as a call, then.
	default:
		return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
			Severity: hcl.DiagError,
			Summary:  invalidTypeSummary,
			Detail:   fmt.Sprintf("The keyword %q is not a valid type specification.", kw),
			Subject:  expr.Range().Ptr(),
		}}
	}

	// If we get down here then our expression isn't just a keyword, so we'll
	// try to process it as a call instead.
	call, diags := hcl.ExprCall(expr)
	if diags.HasErrors() {
		return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
			Severity: hcl.DiagError,
			Summary:  invalidTypeSummary,
			Detail:   "A type specification is either a primitive type keyword (bool, number, string) or a complex type constructor call, like list(string).",
			Subject:  expr.Range().Ptr(),
		}}
	}

	switch call.Name {
	case "bool", "string", "number":
		return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
			Severity: hcl.DiagError,
			Summary:  invalidTypeSummary,
			Detail:   fmt.Sprintf("Primitive type keyword %q does not expect arguments.", call.Name),
			Subject:  &call.ArgsRange,
		}}
	case "any":
		return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
			Severity: hcl.DiagError,
			Summary:  invalidTypeSummary,
			Detail:   fmt.Sprintf("Type constraint keyword %q does not expect arguments.", call.Name),
			Subject:  &call.ArgsRange,
		}}
	}

	if len(call.Arguments) != 1 {
		contextRange := call.ArgsRange
		subjectRange := call.ArgsRange
		if len(call.Arguments) > 1 {
			// If we have too many arguments (as opposed to too _few_) then
			// we'll highlight the extraneous arguments as the diagnostic
			// subject.
			subjectRange = hcl.RangeBetween(call.Arguments[1].Range(), call.Arguments[len(call.Arguments)-1].Range())
		}

		switch call.Name {
		case "list", "set", "map":
			return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
				Severity: hcl.DiagError,
				Summary:  invalidTypeSummary,
				Detail:   fmt.Sprintf("The %s type constructor requires one argument specifying the element type.", call.Name),
				Subject:  &subjectRange,
				Context:  &contextRange,
			}}
		case "object":
			return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
				Severity: hcl.DiagError,
				Summary:  invalidTypeSummary,
				Detail:   "The object type constructor requires one argument specifying the attribute types and values as a map.",
				Subject:  &subjectRange,
				Context:  &contextRange,
			}}
		case "tuple":
			return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
				Severity: hcl.DiagError,
				Summary:  invalidTypeSummary,
				Detail:   "The tuple type constructor requires one argument specifying the element types as a list.",
				Subject:  &subjectRange,
				Context:  &contextRange,
			}}
		}
	}

	switch call.Name {

	case "list":
		ety, defaults, diags := getType(call.Arguments[0], constraint, withDefaults)
		ty := cty.List(ety)
		return ty, collectionDefaults(ty, defaults), diags
	case "set":
		ety, defaults, diags := getType(call.Arguments[0], constraint, withDefaults)
		ty := cty.Set(ety)
		return ty, collectionDefaults(ty, defaults), diags
	case "map":
		ety, defaults, diags := getType(call.Arguments[0], constraint, withDefaults)
		ty := cty.Map(ety)
		return ty, collectionDefaults(ty, defaults), diags
	case "object":
		attrDefs, diags := hcl.ExprMap(call.Arguments[0])
		if diags.HasErrors() {
			return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
				Severity: hcl.DiagError,
				Summary:  invalidTypeSummary,
				Detail:   "Object type constructor requires a map whose keys are attribute names and whose values are the corresponding attribute types.",
				Subject:  call.Arguments[0].Range().Ptr(),
				Context:  expr.Range().Ptr(),
			}}
		}

		atys := make(map[string]cty.Type)
		defaultValues := make(map[string]cty.Value)
		children := make(map[string]*Defaults)
		var optAttrs []string
		for _, attrDef := range attrDefs {
			attrName := hcl.ExprAsKeyword(attrDef.Key)
			if attrName == "" {
				diags = append(diags, &hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  invalidTypeSummary,
					Detail:   "Object constructor map keys must be attribute names.",
					Subject:  attrDef.Key.Range().Ptr(),
					Context:  expr.Range().Ptr(),
				})
				continue
			}

			if _, exists := atys[attrName]; exists {
				diags = append(diags, &hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  invalidTypeSummary,
					Detail:   "Object constructor map keys must be unique.",
					Subject:  attrDef.Key.Range().Ptr(),
					Context:  expr.Range().Ptr(),
				})
				continue
			}

			atyExpr := attrDef.Value

			// the attribute type expression might be wrapped in the special
			// modifier optional(...) to indicate an optional attribute. If
			// so, we'll unwrap that first and make a note about it being
			// optional for when we construct the type below.
			var defaultExpr hcl.Expression
			if call, callDiags := hcl.ExprCall(atyExpr); !callDiags.HasErrors() {
				if call.Name == "optional" {
					if len(call.Arguments) < 1 {
						diags = append(diags, &hcl.Diagnostic{
							Severity: hcl.DiagError,
							Summary:  invalidTypeSummary,
							Detail:   "Optional attribute modifier requires the attribute type as its argument.",
							Subject:  call.ArgsRange.Ptr(),
							Context:  atyExpr.Range().Ptr(),
						})
						continue
					}
					if constraint {
						if withDefaults {
							switch len(call.Arguments) {
							case 2:
								defaultExpr = call.Arguments[1]
								defaultVal, defaultDiags := defaultExpr.Value(nil)
								diags = append(diags, defaultDiags...)
								if !defaultDiags.HasErrors() {
									optAttrs = append(optAttrs, attrName)
									defaultValues[attrName] = defaultVal
								}
							case 1:
								optAttrs = append(optAttrs, attrName)
							default:
								diags = append(diags, &hcl.Diagnostic{
									Severity: hcl.DiagError,
									Summary:  invalidTypeSummary,
									Detail:   "Optional attribute modifier expects at most two arguments: the attribute type, and a default value.",
									Subject:  call.ArgsRange.Ptr(),
									Context:  atyExpr.Range().Ptr(),
								})
							}
						} else {
							if len(call.Arguments) == 1 {
								optAttrs = append(optAttrs, attrName)
							} else {
								diags = append(diags, &hcl.Diagnostic{
									Severity: hcl.DiagError,
									Summary:  invalidTypeSummary,
									Detail:   "Optional attribute modifier expects only one argument: the attribute type.",
									Subject:  call.ArgsRange.Ptr(),
									Context:  atyExpr.Range().Ptr(),
								})
							}
						}
					} else {
						diags = append(diags, &hcl.Diagnostic{
							Severity: hcl.DiagError,
							Summary:  invalidTypeSummary,
							Detail:   "Optional attribute modifier is only for type constraints, not for exact types.",
							Subject:  call.NameRange.Ptr(),
							Context:  atyExpr.Range().Ptr(),
						})
					}
					atyExpr = call.Arguments[0]
				}
			}

			aty, aDefaults, attrDiags := getType(atyExpr, constraint, withDefaults)
			diags = append(diags, attrDiags...)

			// If a default is set for an optional attribute, verify that it is
			// convertible to the attribute type.
			if defaultVal, ok := defaultValues[attrName]; ok {
				convertedDefaultVal, err := convert.Convert(defaultVal, aty)
				if err != nil {
					diags = append(diags, &hcl.Diagnostic{
						Severity: hcl.DiagError,
						Summary:  "Invalid default value for optional attribute",
						Detail:   fmt.Sprintf("This default value is not compatible with the attribute's type constraint: %s.", err),
						Subject:  defaultExpr.Range().Ptr(),
					})
					delete(defaultValues, attrName)
				} else {
					defaultValues[attrName] = convertedDefaultVal
				}
			}

			atys[attrName] = aty
			if aDefaults != nil {
				children[attrName] = aDefaults
			}
		}
		ty := cty.ObjectWithOptionalAttrs(atys, optAttrs)
		return ty, structuredDefaults(ty, defaultValues, children), diags
	case "tuple":
		elemDefs, diags := hcl.ExprList(call.Arguments[0])
		if diags.HasErrors() {
			return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
				Severity: hcl.DiagError,
				Summary:  invalidTypeSummary,
				Detail:   "Tuple type constructor requires a list of element types.",
				Subject:  call.Arguments[0].Range().Ptr(),
				Context:  expr.Range().Ptr(),
			}}
		}
		etys := make([]cty.Type, len(elemDefs))
		children := make(map[string]*Defaults, len(elemDefs))
		for i, defExpr := range elemDefs {
			ety, elemDefaults, elemDiags := getType(defExpr, constraint, withDefaults)
			diags = append(diags, elemDiags...)
			etys[i] = ety
			if elemDefaults != nil {
				children[fmt.Sprintf("%d", i)] = elemDefaults
			}
		}
		ty := cty.Tuple(etys)
		return ty, structuredDefaults(ty, nil, children), diags
	case "optional":
		return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
			Severity: hcl.DiagError,
			Summary:  invalidTypeSummary,
			Detail:   fmt.Sprintf("Keyword %q is valid only as a modifier for object type attributes.", call.Name),
			Subject:  call.NameRange.Ptr(),
		}}
	default:
		// Can't access call.Arguments in this path because we've not validated
		// that it contains exactly one expression here.
		return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
			Severity: hcl.DiagError,
			Summary:  invalidTypeSummary,
			Detail:   fmt.Sprintf("Keyword %q is not a valid type constructor.", call.Name),
			Subject:  expr.Range().Ptr(),
		}}
	}
}

func collectionDefaults(ty cty.Type, defaults *Defaults) *Defaults {
	if defaults == nil {
		return nil
	}
	return &Defaults{
		Type: ty,
		Children: map[string]*Defaults{
			"": defaults,
		},
	}
}

func structuredDefaults(ty cty.Type, defaultValues map[string]cty.Value, children map[string]*Defaults) *Defaults {
	if len(defaultValues) == 0 && len(children) == 0 {
		return nil
	}

	defaults := &Defaults{
		Type: ty,
	}
	if len(defaultValues) > 0 {
		defaults.DefaultValues = defaultValues
	}
	if len(children) > 0 {
		defaults.Children = children
	}

	return defaults
}
