package terraform

import (
	"fmt"
	"log"
	"reflect"
	"strconv"
	"strings"

	"github.com/hashicorp/terraform/config"
	"github.com/hashicorp/terraform/config/module"
	"github.com/hashicorp/terraform/helper/hilmapstructure"
)

// EvalTypeCheckVariable is an EvalNode which ensures that the variable
// values which are assigned as inputs to a module (including the root)
// match the types which are either declared for the variables explicitly
// or inferred from the default values.
//
// In order to achieve this three things are required:
//     - a map of the proposed variable values
//     - the configuration tree of the module in which the variable is
//       declared
//     - the path to the module (so we know which part of the tree to
//       compare the values against).
type EvalTypeCheckVariable struct {
	Variables  map[string]interface{}
	ModulePath []string
	ModuleTree *module.Tree
}

func (n *EvalTypeCheckVariable) Eval(ctx EvalContext) (interface{}, error) {
	currentTree := n.ModuleTree
	for _, pathComponent := range n.ModulePath[1:] {
		currentTree = currentTree.Children()[pathComponent]
	}
	targetConfig := currentTree.Config()

	prototypes := make(map[string]config.VariableType)
	for _, variable := range targetConfig.Variables {
		prototypes[variable.Name] = variable.Type()
	}

	// Only display a module in an error message if we are not in the root module
	modulePathDescription := fmt.Sprintf(" in module %s", strings.Join(n.ModulePath[1:], "."))
	if len(n.ModulePath) == 1 {
		modulePathDescription = ""
	}

	for name, declaredType := range prototypes {
		proposedValue, ok := n.Variables[name]
		if !ok {
			// This means the default value should be used as no overriding value
			// has been set. Therefore we should continue as no check is necessary.
			continue
		}

		if proposedValue == config.UnknownVariableValue {
			continue
		}

		switch declaredType {
		case config.VariableTypeString:
			switch proposedValue.(type) {
			case string:
				continue
			default:
				return nil, fmt.Errorf("variable %s%s should be type %s, got %s",
					name, modulePathDescription, declaredType.Printable(), hclTypeName(proposedValue))
			}
		case config.VariableTypeMap:
			switch proposedValue.(type) {
			case map[string]interface{}:
				continue
			default:
				return nil, fmt.Errorf("variable %s%s should be type %s, got %s",
					name, modulePathDescription, declaredType.Printable(), hclTypeName(proposedValue))
			}
		case config.VariableTypeList:
			switch proposedValue.(type) {
			case []interface{}:
				continue
			default:
				return nil, fmt.Errorf("variable %s%s should be type %s, got %s",
					name, modulePathDescription, declaredType.Printable(), hclTypeName(proposedValue))
			}
		default:
			return nil, fmt.Errorf("variable %s%s should be type %s, got type string",
				name, modulePathDescription, declaredType.Printable())
		}
	}

	return nil, nil
}

// EvalSetVariables is an EvalNode implementation that sets the variables
// explicitly for interpolation later.
type EvalSetVariables struct {
	Module    *string
	Variables map[string]interface{}
}

// TODO: test
func (n *EvalSetVariables) Eval(ctx EvalContext) (interface{}, error) {
	ctx.SetVariables(*n.Module, n.Variables)
	return nil, nil
}

// EvalVariableBlock is an EvalNode implementation that evaluates the
// given configuration, and uses the final values as a way to set the
// mapping.
type EvalVariableBlock struct {
	Config         **ResourceConfig
	VariableValues map[string]interface{}
}

func (n *EvalVariableBlock) Eval(ctx EvalContext) (interface{}, error) {
	// Clear out the existing mapping
	for k, _ := range n.VariableValues {
		delete(n.VariableValues, k)
	}

	// Get our configuration
	rc := *n.Config
	for k, v := range rc.Config {
		vKind := reflect.ValueOf(v).Type().Kind()

		switch vKind {
		case reflect.Slice:
			var vSlice []interface{}
			if err := hilmapstructure.WeakDecode(v, &vSlice); err == nil {
				n.VariableValues[k] = vSlice
				continue
			}
		case reflect.Map:
			var vMap map[string]interface{}
			if err := hilmapstructure.WeakDecode(v, &vMap); err == nil {
				n.VariableValues[k] = vMap
				continue
			}
		default:
			var vString string
			if err := hilmapstructure.WeakDecode(v, &vString); err == nil {
				n.VariableValues[k] = vString
				continue
			}
		}

		return nil, fmt.Errorf("Variable value for %s is not a string, list or map type", k)
	}

	for _, path := range rc.ComputedKeys {
		log.Printf("[DEBUG] Setting Unknown Variable Value for computed key: %s", path)
		err := n.setUnknownVariableValueForPath(path)
		if err != nil {
			return nil, err
		}
	}

	return nil, nil
}

func (n *EvalVariableBlock) setUnknownVariableValueForPath(path string) error {
	pathComponents := strings.Split(path, ".")

	if len(pathComponents) < 1 {
		return fmt.Errorf("No path comoponents in %s", path)
	}

	if len(pathComponents) == 1 {
		// Special case the "top level" since we know the type
		if _, ok := n.VariableValues[pathComponents[0]]; !ok {
			n.VariableValues[pathComponents[0]] = config.UnknownVariableValue
		}
		return nil
	}

	// Otherwise find the correct point in the tree and then set to unknown
	var current interface{} = n.VariableValues[pathComponents[0]]
	for i := 1; i < len(pathComponents); i++ {
		switch tCurrent := current.(type) {
		case []interface{}:
			index, err := strconv.Atoi(pathComponents[i])
			if err != nil {
				return fmt.Errorf("Cannot convert %s to slice index in path %s",
					pathComponents[i], path)
			}
			current = tCurrent[index]
		case []map[string]interface{}:
			index, err := strconv.Atoi(pathComponents[i])
			if err != nil {
				return fmt.Errorf("Cannot convert %s to slice index in path %s",
					pathComponents[i], path)
			}
			current = tCurrent[index]
		case map[string]interface{}:
			if val, hasVal := tCurrent[pathComponents[i]]; hasVal {
				current = val
				continue
			}

			tCurrent[pathComponents[i]] = config.UnknownVariableValue
			break
		}
	}

	return nil
}

// EvalCoerceMapVariable is an EvalNode implementation that recognizes a
// specific ambiguous HCL parsing situation and resolves it. In HCL parsing, a
// bare map literal is indistinguishable from a list of maps w/ one element.
//
// We take all the same inputs as EvalTypeCheckVariable above, since we need
// both the target type and the proposed value in order to properly coerce.
type EvalCoerceMapVariable struct {
	Variables  map[string]interface{}
	ModulePath []string
	ModuleTree *module.Tree
}

// Eval implements the EvalNode interface. See EvalCoerceMapVariable for
// details.
func (n *EvalCoerceMapVariable) Eval(ctx EvalContext) (interface{}, error) {
	currentTree := n.ModuleTree
	for _, pathComponent := range n.ModulePath[1:] {
		currentTree = currentTree.Children()[pathComponent]
	}
	targetConfig := currentTree.Config()

	prototypes := make(map[string]config.VariableType)
	for _, variable := range targetConfig.Variables {
		prototypes[variable.Name] = variable.Type()
	}

	for name, declaredType := range prototypes {
		if declaredType != config.VariableTypeMap {
			continue
		}

		proposedValue, ok := n.Variables[name]
		if !ok {
			continue
		}

		if list, ok := proposedValue.([]interface{}); ok && len(list) == 1 {
			if m, ok := list[0].(map[string]interface{}); ok {
				log.Printf("[DEBUG] EvalCoerceMapVariable: "+
					"Coercing single element list into map: %#v", m)
				n.Variables[name] = m
			}
		}
	}

	return nil, nil
}

// hclTypeName returns the name of the type that would represent this value in
// a config file, or falls back to the Go type name if there's no corresponding
// HCL type. This is used for formatted output, not for comparing types.
func hclTypeName(i interface{}) string {
	switch k := reflect.Indirect(reflect.ValueOf(i)).Kind(); k {
	case reflect.Bool:
		return "boolean"
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
		reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32,
		reflect.Uint64, reflect.Uintptr, reflect.Float32, reflect.Float64:
		return "number"
	case reflect.Array, reflect.Slice:
		return "list"
	case reflect.Map:
		return "map"
	case reflect.String:
		return "string"
	default:
		// fall back to the Go type if there's no match
		return k.String()
	}
}
