package objchange

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

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

// NormalizeObjectFromLegacySDK takes an object that may have been generated
// by the legacy Terraform SDK (i.e. returned from a provider with the
// UnsafeToUseLegacyTypeSystem opt-out set) and does its best to normalize it for the
// assumptions we would normally enforce if the provider had not opted out.
//
// In particular, this function guarantees that a value representing a nested
// block will never itself be unknown or null, instead representing that as
// a non-null value that may contain null/unknown values.
//
// The input value must still conform to the implied type of the given schema,
// or else this function may produce garbage results or panic. This is usually
// okay because type consistency is enforced when deserializing the value
// returned from the provider over the RPC wire protocol anyway.
func NormalizeObjectFromLegacySDK(val cty.Value, schema *configschema.Block) cty.Value {
	if val == cty.NilVal || val.IsNull() {
		// This should never happen in reasonable use, but we'll allow it
		// and normalize to a null of the expected type rather than panicking
		// below.
		return cty.NullVal(schema.ImpliedType())
	}

	vals := make(map[string]cty.Value)
	for name := range schema.Attributes {
		// No normalization for attributes, since them being type-conformant
		// is all that we require.
		vals[name] = val.GetAttr(name)
	}
	for name, blockS := range schema.BlockTypes {
		lv := val.GetAttr(name)

		// Legacy SDK never generates dynamically-typed attributes and so our
		// normalization code doesn't deal with them, but we need to make sure
		// we still pass them through properly so that we don't interfere with
		// objects generated by other SDKs.
		if ty := blockS.Block.ImpliedType(); ty.HasDynamicTypes() {
			vals[name] = lv
			continue
		}

		switch blockS.Nesting {
		case configschema.NestingSingle, configschema.NestingGroup:
			if lv.IsKnown() {
				if lv.IsNull() && blockS.Nesting == configschema.NestingGroup {
					vals[name] = blockS.EmptyValue()
				} else {
					vals[name] = NormalizeObjectFromLegacySDK(lv, &blockS.Block)
				}
			} else {
				vals[name] = unknownBlockStub(&blockS.Block)
			}
		case configschema.NestingList:
			switch {
			case !lv.IsKnown():
				vals[name] = cty.ListVal([]cty.Value{unknownBlockStub(&blockS.Block)})
			case lv.IsNull() || lv.LengthInt() == 0:
				vals[name] = cty.ListValEmpty(blockS.Block.ImpliedType())
			default:
				subVals := make([]cty.Value, 0, lv.LengthInt())
				for it := lv.ElementIterator(); it.Next(); {
					_, subVal := it.Element()
					subVals = append(subVals, NormalizeObjectFromLegacySDK(subVal, &blockS.Block))
				}
				vals[name] = cty.ListVal(subVals)
			}
		case configschema.NestingSet:
			switch {
			case !lv.IsKnown():
				vals[name] = cty.SetVal([]cty.Value{unknownBlockStub(&blockS.Block)})
			case lv.IsNull() || lv.LengthInt() == 0:
				vals[name] = cty.SetValEmpty(blockS.Block.ImpliedType())
			default:
				subVals := make([]cty.Value, 0, lv.LengthInt())
				for it := lv.ElementIterator(); it.Next(); {
					_, subVal := it.Element()
					subVals = append(subVals, NormalizeObjectFromLegacySDK(subVal, &blockS.Block))
				}
				vals[name] = cty.SetVal(subVals)
			}
		default:
			// The legacy SDK doesn't support NestingMap, so we just assume
			// maps are always okay. (If not, we would've detected and returned
			// an error to the user before we got here.)
			vals[name] = lv
		}
	}
	return cty.ObjectVal(vals)
}

// unknownBlockStub constructs an object value that approximates an unknown
// block by producing a known block object with all of its leaf attribute
// values set to unknown.
//
// Blocks themselves cannot be unknown, so if the legacy SDK tries to return
// such a thing, we'll use this result instead. This convention mimics how
// the dynamic block feature deals with being asked to iterate over an unknown
// value, because our value-checking functions already accept this convention
// as a special case.
func unknownBlockStub(schema *configschema.Block) cty.Value {
	vals := make(map[string]cty.Value)
	for name, attrS := range schema.Attributes {
		vals[name] = cty.UnknownVal(attrS.Type)
	}
	for name, blockS := range schema.BlockTypes {
		switch blockS.Nesting {
		case configschema.NestingSingle, configschema.NestingGroup:
			vals[name] = unknownBlockStub(&blockS.Block)
		case configschema.NestingList:
			// In principle we may be expected to produce a tuple value here,
			// if there are any dynamically-typed attributes in our nested block,
			// but the legacy SDK doesn't support that, so we just assume it'll
			// never be necessary to normalize those. (Incorrect usage in any
			// other SDK would be caught and returned as an error before we
			// get here.)
			vals[name] = cty.ListVal([]cty.Value{unknownBlockStub(&blockS.Block)})
		case configschema.NestingSet:
			vals[name] = cty.SetVal([]cty.Value{unknownBlockStub(&blockS.Block)})
		case configschema.NestingMap:
			// A nesting map can never be unknown since we then wouldn't know
			// what the keys are. (Legacy SDK doesn't support NestingMap anyway,
			// so this should never arise.)
			vals[name] = cty.MapValEmpty(blockS.Block.ImpliedType())
		}
	}
	return cty.ObjectVal(vals)
}
