package configupgrade

import (
	"log"

	hcl2 "github.com/hashicorp/hcl/v2"
	hcl2syntax "github.com/hashicorp/hcl/v2/hclsyntax"
	"github.com/zclconf/go-cty/cty"

	"github.com/hashicorp/terraform/addrs"
	"github.com/hashicorp/terraform/lang"
	"github.com/hashicorp/terraform/tfdiags"
)

// InferExpressionType attempts to determine a result type for the given
// expression source code, which should already have been upgraded to new
// expression syntax.
//
// If self is non-nil, it will determine the meaning of the special "self"
// reference.
//
// If such an inference isn't possible, either because of limitations of
// static analysis or because of errors in the expression, the result is
// cty.DynamicPseudoType indicating "unknown".
func (an *analysis) InferExpressionType(src []byte, self addrs.Referenceable) cty.Type {
	expr, diags := hcl2syntax.ParseExpression(src, "", hcl2.Pos{Line: 1, Column: 1})
	if diags.HasErrors() {
		// If there's a syntax error then analysis is impossible.
		return cty.DynamicPseudoType
	}

	data := analysisData{an}
	scope := &lang.Scope{
		Data:     data,
		SelfAddr: self,
		PureOnly: false,
		BaseDir:  ".",
	}
	val, _ := scope.EvalExpr(expr, cty.DynamicPseudoType)

	// Value will be cty.DynamicVal if either inference was impossible or
	// if there was an error, leading to cty.DynamicPseudoType here.
	return val.Type()
}

// analysisData is an implementation of lang.Data that returns unknown values
// of suitable types in order to achieve approximate dynamic analysis of
// expression result types, which we need for some upgrade rules.
//
// Unlike a usual implementation of this interface, this one never returns
// errors and will instead just return cty.DynamicVal if it can't produce
// an exact type for any reason. This can then allow partial upgrading to
// proceed and the caller can emit warning comments for ambiguous situations.
//
// N.B.: Source ranges in the data methods are meaningless, since they are
// just relative to the byte array passed to InferExpressionType, not to
// any real input file.
type analysisData struct {
	an *analysis
}

var _ lang.Data = (*analysisData)(nil)

func (d analysisData) StaticValidateReferences(refs []*addrs.Reference, self addrs.Referenceable) tfdiags.Diagnostics {
	// This implementation doesn't do any static validation.
	return nil
}

func (d analysisData) GetCountAttr(addr addrs.CountAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
	// All valid count attributes are numbers
	return cty.UnknownVal(cty.Number), nil
}

func (d analysisData) GetForEachAttr(addr addrs.ForEachAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
	return cty.DynamicVal, nil
}

func (d analysisData) GetResourceInstance(instAddr addrs.ResourceInstance, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
	log.Printf("[TRACE] configupgrade: Determining type for %s", instAddr)
	addr := instAddr.Resource

	// Our analysis pass should've found a suitable schema for every resource
	// type in the module.
	providerType, ok := d.an.ResourceProviderType[addr]
	if !ok {
		// Should not be possible, since analysis visits every resource block.
		log.Printf("[TRACE] configupgrade: analysis.GetResourceInstance doesn't have a provider type for %s", addr)
		return cty.DynamicVal, nil
	}
	providerSchema, ok := d.an.ProviderSchemas[providerType]
	if !ok {
		// Should not be possible, since analysis loads schema for every provider.
		log.Printf("[TRACE] configupgrade: analysis.GetResourceInstance doesn't have a provider schema for for %q", providerType)
		return cty.DynamicVal, nil
	}
	schema, _ := providerSchema.SchemaForResourceAddr(addr)
	if schema == nil {
		// Should not be possible, since analysis loads schema for every provider.
		log.Printf("[TRACE] configupgrade: analysis.GetResourceInstance doesn't have a schema for for %s", addr)
		return cty.DynamicVal, nil
	}

	objTy := schema.ImpliedType()

	// We'll emulate the normal evaluator's behavor of deciding whether to
	// return a list or a single object type depending on whether count is
	// set and whether an instance key is given in the address.
	if d.an.ResourceHasCount[addr] {
		if instAddr.Key == addrs.NoKey {
			log.Printf("[TRACE] configupgrade: %s refers to counted instance without a key, so result is a list of %#v", instAddr, objTy)
			return cty.UnknownVal(cty.List(objTy)), nil
		}
		log.Printf("[TRACE] configupgrade: %s refers to counted instance with a key, so result is single object", instAddr)
		return cty.UnknownVal(objTy), nil
	}

	if instAddr.Key != addrs.NoKey {
		log.Printf("[TRACE] configupgrade: %s refers to non-counted instance with a key, which is invalid", instAddr)
		return cty.DynamicVal, nil
	}
	log.Printf("[TRACE] configupgrade: %s refers to non-counted instance without a key, so result is single object", instAddr)
	return cty.UnknownVal(objTy), nil
}

func (d analysisData) GetLocalValue(addrs.LocalValue, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
	// We can only predict these in general by recursively evaluating their
	// expressions, which creates some undesirable complexity here so for
	// now we'll just skip analyses with locals and see if this complexity
	// is warranted with real-world testing.
	return cty.DynamicVal, nil
}

func (d analysisData) GetModuleInstance(addrs.ModuleCallInstance, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
	// We only work on one module at a time during upgrading, so we have no
	// information about the outputs of a child module.
	return cty.DynamicVal, nil
}

func (d analysisData) GetModuleInstanceOutput(addrs.ModuleCallOutput, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
	// We only work on one module at a time during upgrading, so we have no
	// information about the outputs of a child module.
	return cty.DynamicVal, nil
}

func (d analysisData) GetPathAttr(addrs.PathAttr, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
	// All valid path attributes are strings
	return cty.UnknownVal(cty.String), nil
}

func (d analysisData) GetTerraformAttr(addrs.TerraformAttr, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
	// All valid "terraform" attributes are strings
	return cty.UnknownVal(cty.String), nil
}

func (d analysisData) GetInputVariable(addr addrs.InputVariable, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
	// TODO: Collect shallow type information (list vs. map vs. string vs. unknown)
	// in analysis and then return a similarly-approximate type here.
	log.Printf("[TRACE] configupgrade: Determining type for %s", addr)
	name := addr.Name
	typeName := d.an.VariableTypes[name]
	switch typeName {
	case "list":
		return cty.UnknownVal(cty.List(cty.DynamicPseudoType)), nil
	case "map":
		return cty.UnknownVal(cty.Map(cty.DynamicPseudoType)), nil
	case "string":
		return cty.UnknownVal(cty.String), nil
	default:
		return cty.DynamicVal, nil
	}
}
