// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package terraform

import (
	"fmt"

	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/hcl/v2/ext/typeexpr"
	"github.com/hashicorp/hcl/v2/gohcl"
	"github.com/hashicorp/hcl/v2/hclsyntax"
	"github.com/terraform-linters/tflint-plugin-sdk/hclext"
	"github.com/terraform-linters/tflint/terraform/tfdiags"
	"github.com/zclconf/go-cty/cty"
	"github.com/zclconf/go-cty/cty/convert"
)

type Variable struct {
	Name    string
	Default cty.Value

	Type           cty.Type
	ConstraintType cty.Type
	TypeDefaults   *typeexpr.Defaults

	DeclRange hcl.Range

	ParsingMode VariableParsingMode
	Sensitive   bool
	Ephemeral   bool
	Nullable    bool
}

func decodeVariableBlock(block *hclext.Block) (*Variable, hcl.Diagnostics) {
	v := &Variable{
		Name:           block.Labels[0],
		Type:           cty.DynamicPseudoType,
		ConstraintType: cty.DynamicPseudoType,
		ParsingMode:    VariableParseLiteral,
		DeclRange:      block.DefRange,
	}
	diags := hcl.Diagnostics{}

	if attr, exists := block.Body.Attributes["type"]; exists {
		ty, tyDefaults, parseMode, tyDiags := decodeVariableType(attr.Expr)
		diags = diags.Extend(tyDiags)
		v.ConstraintType = ty
		v.TypeDefaults = tyDefaults
		v.Type = ty.WithoutOptionalAttributesDeep()
		v.ParsingMode = parseMode
	}

	if attr, exists := block.Body.Attributes["sensitive"]; exists {
		valDiags := gohcl.DecodeExpression(attr.Expr, nil, &v.Sensitive)
		diags = diags.Extend(valDiags)
	}

	if attr, exists := block.Body.Attributes["ephemeral"]; exists {
		valDiags := gohcl.DecodeExpression(attr.Expr, nil, &v.Ephemeral)
		diags = diags.Extend(valDiags)
	}

	if attr, exists := block.Body.Attributes["nullable"]; exists {
		valDiags := gohcl.DecodeExpression(attr.Expr, nil, &v.Nullable)
		diags = append(diags, valDiags...)
	} else {
		// The current default is true, which is subject to change in a future
		// language edition.
		v.Nullable = true
	}

	if attr, exists := block.Body.Attributes["default"]; exists {
		val, valDiags := attr.Expr.Value(nil)
		diags = diags.Extend(valDiags)

		if v.ConstraintType != cty.NilType {
			var err error
			// defaults to the variable default value before type conversion,
			// unless the default value is null. Null is excluded from the
			// type default application process as a special case, to allow
			// nullable variables to have a null default value.
			if v.TypeDefaults != nil && !val.IsNull() {
				val = v.TypeDefaults.Apply(val)
			}
			val, err = convert.Convert(val, v.ConstraintType)
			if err != nil {
				diags = append(diags, &hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  "Invalid default value for variable",
					Detail: fmt.Sprintf(
						"This default value is not compatible with the variable's type constraint: %s.",
						tfdiags.FormatError(err),
					),
					Subject: attr.Expr.Range().Ptr(),
				})
				val = cty.DynamicVal
			}
		}

		v.Default = val
	}

	return v, diags
}

func decodeVariableType(expr hcl.Expression) (cty.Type, *typeexpr.Defaults, VariableParsingMode, hcl.Diagnostics) {
	if exprIsNativeQuotedString(expr) {
		// If a user provides the pre-0.12 form of variable type argument where
		// the string values "string", "list" and "map" are accepted, we
		// provide an error to point the user towards using the type system
		// correctly has a hint.
		// Only the native syntax ends up in this codepath; we handle the
		// JSON syntax (which is, of course, quoted within the type system)
		// in the normal codepath below.
		val, diags := expr.Value(nil)
		if diags.HasErrors() {
			return cty.DynamicPseudoType, nil, VariableParseHCL, diags
		}
		str := val.AsString()
		switch str {
		case "string":
			diags = append(diags, &hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Invalid quoted type constraints",
				Detail:   "Terraform 0.11 and earlier required type constraints to be given in quotes, but that form is now deprecated and will be removed in a future version of Terraform. Remove the quotes around \"string\".",
				Subject:  expr.Range().Ptr(),
			})
			return cty.DynamicPseudoType, nil, VariableParseLiteral, diags
		case "list":
			diags = append(diags, &hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Invalid quoted type constraints",
				Detail:   "Terraform 0.11 and earlier required type constraints to be given in quotes, but that form is now deprecated and will be removed in a future version of Terraform. Remove the quotes around \"list\" and write list(string) instead to explicitly indicate that the list elements are strings.",
				Subject:  expr.Range().Ptr(),
			})
			return cty.DynamicPseudoType, nil, VariableParseHCL, diags
		case "map":
			diags = append(diags, &hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Invalid quoted type constraints",
				Detail:   "Terraform 0.11 and earlier required type constraints to be given in quotes, but that form is now deprecated and will be removed in a future version of Terraform. Remove the quotes around \"map\" and write map(string) instead to explicitly indicate that the map elements are strings.",
				Subject:  expr.Range().Ptr(),
			})
			return cty.DynamicPseudoType, nil, VariableParseHCL, diags
		default:
			return cty.DynamicPseudoType, nil, VariableParseHCL, hcl.Diagnostics{{
				Severity: hcl.DiagError,
				Summary:  "Invalid legacy variable type hint",
				Detail:   `To provide a full type expression, remove the surrounding quotes and give the type expression directly.`,
				Subject:  expr.Range().Ptr(),
			}}
		}
	}

	// First we'll deal with some shorthand forms that the HCL-level type
	// expression parser doesn't include. These both emulate pre-0.12 behavior
	// of allowing a list or map of any element type as long as all of the
	// elements are consistent. This is the same as list(any) or map(any).
	switch hcl.ExprAsKeyword(expr) {
	case "list":
		return cty.List(cty.DynamicPseudoType), nil, VariableParseHCL, nil
	case "map":
		return cty.Map(cty.DynamicPseudoType), nil, VariableParseHCL, nil
	}

	ty, typeDefaults, diags := typeexpr.TypeConstraintWithDefaults(expr)
	if diags.HasErrors() {
		return cty.DynamicPseudoType, nil, VariableParseHCL, diags
	}

	switch {
	case ty.IsPrimitiveType():
		// Primitive types use literal parsing.
		return ty, typeDefaults, VariableParseLiteral, diags
	default:
		// Everything else uses HCL parsing
		return ty, typeDefaults, VariableParseHCL, diags
	}
}

// VariableParsingMode defines how values of a particular variable given by
// text-only mechanisms (command line arguments and environment variables)
// should be parsed to produce the final value.
type VariableParsingMode rune

// VariableParseLiteral is a variable parsing mode that just takes the given
// string directly as a cty.String value.
const VariableParseLiteral VariableParsingMode = 'L'

// VariableParseHCL is a variable parsing mode that attempts to parse the given
// string as an HCL expression and returns the result.
const VariableParseHCL VariableParsingMode = 'H'

// Parse uses the receiving parsing mode to process the given variable value
// string, returning the result along with any diagnostics.
//
// A VariableParsingMode does not know the expected type of the corresponding
// variable, so it's the caller's responsibility to attempt to convert the
// result to the appropriate type and return to the user any diagnostics that
// conversion may produce.
//
// The given name is used to create a synthetic filename in case any diagnostics
// must be generated about the given string value. This should be the name
// of the root module variable whose value will be populated from the given
// string.
//
// If the returned diagnostics has errors, the returned value may not be
// valid.
func (m VariableParsingMode) Parse(name, value string) (cty.Value, hcl.Diagnostics) {
	switch m {
	case VariableParseLiteral:
		return cty.StringVal(value), nil
	case VariableParseHCL:
		fakeFilename := fmt.Sprintf("<value for var.%s>", name)
		expr, diags := hclsyntax.ParseExpression([]byte(value), fakeFilename, hcl.Pos{Line: 1, Column: 1})
		if diags.HasErrors() {
			return cty.DynamicVal, diags
		}
		val, valDiags := expr.Value(nil)
		diags = append(diags, valDiags...)
		return val, diags
	default:
		// Should never happen
		panic(fmt.Errorf("Parse called on invalid VariableParsingMode %#v", m))
	}
}

func exprIsNativeQuotedString(expr hcl.Expression) bool {
	_, ok := expr.(*hclsyntax.TemplateExpr)
	return ok
}

var variableBlockSchema = &hclext.BodySchema{
	Attributes: []hclext.AttributeSchema{
		{
			Name: "default",
		},
		{
			Name: "type",
		},
		{
			Name: "sensitive",
		},
		{
			Name: "ephemeral",
		},
		{
			Name: "nullable",
		},
	},
}
