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

package objchange

import (
	"fmt"

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

	"github.com/opentofu/opentofu/internal/configs/configschema"
)

// AssertPlanValid checks checks whether a planned new state returned by a
// provider's PlanResourceChange method is suitable to achieve a change
// from priorState to config. It returns a slice with nonzero length if
// any problems are detected. Because problems here indicate bugs in the
// provider that generated the plannedState, they are written with provider
// developers as an audience, rather than end-users.
//
// All of the given values must have the same type and must conform to the
// implied type of the given schema, or this function may panic or produce
// garbage results.
//
// During planning, a provider may only make changes to attributes that are
// null (unset) in the configuration and are marked as "computed" in the
// resource type schema, in order to insert any default values the provider
// may know about. If the default value cannot be determined until apply time,
// the provider can return an unknown value. Providers are forbidden from
// planning a change that disagrees with any non-null argument in the
// configuration.
//
// As a special exception, providers _are_ allowed to provide attribute values
// conflicting with configuration if and only if the planned value exactly
// matches the corresponding attribute value in the prior state. The provider
// can use this to signal that the new value is functionally equivalent to
// the old and thus no change is required.
func AssertPlanValid(schema *configschema.Block, priorState, config, plannedState cty.Value) []error {
	return assertPlanValid(schema, priorState, config, plannedState, nil)
}

func assertPlanValid(schema *configschema.Block, priorState, config, plannedState cty.Value, path cty.Path) []error {
	var errs []error
	if plannedState.IsNull() && !config.IsNull() {
		errs = append(errs, path.NewErrorf("planned for absence but config wants existence"))
		return errs
	}
	if config.IsNull() && !plannedState.IsNull() {
		errs = append(errs, path.NewErrorf("planned for existence but config wants absence"))
		return errs
	}
	if plannedState.IsNull() {
		// No further checks possible if the planned value is null
		return errs
	}

	impTy := schema.ImpliedType()

	// verify attributes
	moreErrs := assertPlannedAttrsValid(schema.Attributes, priorState, config, plannedState, path)
	errs = append(errs, moreErrs...)

	for name, blockS := range schema.BlockTypes {
		path := append(path, cty.GetAttrStep{Name: name})
		plannedV := plannedState.GetAttr(name)
		configV := config.GetAttr(name)
		priorV := cty.NullVal(impTy.AttributeType(name))
		if !priorState.IsNull() {
			priorV = priorState.GetAttr(name)
		}
		if plannedV.RawEquals(configV) {
			// Easy path: nothing has changed at all
			continue
		}

		if !configV.IsKnown() {
			// An unknown config block represents a dynamic block where the
			// for_each value is unknown, and therefor cannot be altered by the
			// provider.
			errs = append(errs, path.NewErrorf("planned value %#v for unknown dynamic block", plannedV))
			continue
		}

		if !plannedV.IsKnown() {
			// Only dynamic configuration can set blocks to unknown, so this is
			// not allowed from the provider. This means that either the config
			// and plan should match, or we have an error where the plan
			// changed the config value, both of which have been checked.
			errs = append(errs, path.NewErrorf("attribute representing nested block must not be unknown itself; set nested attribute values to unknown instead"))
			continue
		}

		switch blockS.Nesting {
		case configschema.NestingSingle, configschema.NestingGroup:
			moreErrs := assertPlanValid(&blockS.Block, priorV, configV, plannedV, path)
			errs = append(errs, moreErrs...)
		case configschema.NestingList:
			// A NestingList might either be a list or a tuple, depending on
			// whether there are dynamically-typed attributes inside. However,
			// both support a similar-enough API that we can treat them the
			// same for our purposes here.
			if plannedV.IsNull() {
				errs = append(errs, path.NewErrorf("attribute representing a list of nested blocks must be empty to indicate no blocks, not null"))
				continue
			}

			if configV.IsNull() {
				// Configuration cannot decode a block into a null value, but
				// we could be dealing with a null returned by a legacy
				// provider and inserted via ignore_changes. Fix the value in
				// place so the length can still be compared.
				configV = cty.ListValEmpty(configV.Type().ElementType())
			}

			plannedL := plannedV.LengthInt()
			configL := configV.LengthInt()
			if plannedL != configL {
				errs = append(errs, path.NewErrorf("block count in plan (%d) disagrees with count in config (%d)", plannedL, configL))
				continue
			}

			for it := plannedV.ElementIterator(); it.Next(); {
				idx, plannedEV := it.Element()
				path := append(path, cty.IndexStep{Key: idx})
				if !plannedEV.IsKnown() {
					errs = append(errs, path.NewErrorf("element representing nested block must not be unknown itself; set nested attribute values to unknown instead"))
					continue
				}
				if !configV.HasIndex(idx).True() {
					continue // should never happen since we checked the lengths above
				}
				configEV := configV.Index(idx)
				priorEV := cty.NullVal(blockS.ImpliedType())
				if !priorV.IsNull() && priorV.HasIndex(idx).True() {
					priorEV = priorV.Index(idx)
				}

				moreErrs := assertPlanValid(&blockS.Block, priorEV, configEV, plannedEV, path)
				errs = append(errs, moreErrs...)
			}
		case configschema.NestingMap:
			if plannedV.IsNull() {
				errs = append(errs, path.NewErrorf("attribute representing a map of nested blocks must be empty to indicate no blocks, not null"))
				continue
			}

			// A NestingMap might either be a map or an object, depending on
			// whether there are dynamically-typed attributes inside, but
			// that's decided statically and so all values will have the same
			// kind.
			if plannedV.Type().IsObjectType() {
				plannedAtys := plannedV.Type().AttributeTypes()
				configAtys := configV.Type().AttributeTypes()
				for k := range plannedAtys {
					if _, ok := configAtys[k]; !ok {
						errs = append(errs, path.NewErrorf("block key %q from plan is not present in config", k))
						continue
					}
					path := append(path, cty.GetAttrStep{Name: k})

					plannedEV := plannedV.GetAttr(k)
					if !plannedEV.IsKnown() {
						errs = append(errs, path.NewErrorf("element representing nested block must not be unknown itself; set nested attribute values to unknown instead"))
						continue
					}
					configEV := configV.GetAttr(k)
					priorEV := cty.NullVal(blockS.ImpliedType())
					if !priorV.IsNull() && priorV.Type().HasAttribute(k) {
						priorEV = priorV.GetAttr(k)
					}
					moreErrs := assertPlanValid(&blockS.Block, priorEV, configEV, plannedEV, path)
					errs = append(errs, moreErrs...)
				}
				for k := range configAtys {
					if _, ok := plannedAtys[k]; !ok {
						errs = append(errs, path.NewErrorf("block key %q from config is not present in plan", k))
						continue
					}
				}
			} else {
				plannedL := plannedV.LengthInt()
				configL := configV.LengthInt()
				if plannedL != configL {
					errs = append(errs, path.NewErrorf("block count in plan (%d) disagrees with count in config (%d)", plannedL, configL))
					continue
				}
				for it := plannedV.ElementIterator(); it.Next(); {
					idx, plannedEV := it.Element()
					path := append(path, cty.IndexStep{Key: idx})
					if !plannedEV.IsKnown() {
						errs = append(errs, path.NewErrorf("element representing nested block must not be unknown itself; set nested attribute values to unknown instead"))
						continue
					}
					k := idx.AsString()
					if !configV.HasIndex(idx).True() {
						errs = append(errs, path.NewErrorf("block key %q from plan is not present in config", k))
						continue
					}
					configEV := configV.Index(idx)
					priorEV := cty.NullVal(blockS.ImpliedType())
					if !priorV.IsNull() && priorV.HasIndex(idx).True() {
						priorEV = priorV.Index(idx)
					}
					moreErrs := assertPlanValid(&blockS.Block, priorEV, configEV, plannedEV, path)
					errs = append(errs, moreErrs...)
				}
				for it := configV.ElementIterator(); it.Next(); {
					idx, _ := it.Element()
					if !plannedV.HasIndex(idx).True() {
						errs = append(errs, path.NewErrorf("block key %q from config is not present in plan", idx.AsString()))
						continue
					}
				}
			}
		case configschema.NestingSet:
			if plannedV.IsNull() {
				errs = append(errs, path.NewErrorf("attribute representing a set of nested blocks must be empty to indicate no blocks, not null"))
				continue
			}

			// Because set elements have no identifier with which to correlate
			// them, we can't robustly validate the plan for a nested block
			// backed by a set, and so unfortunately we need to just trust the
			// provider to do the right thing. :(
			//
			// (In principle we could correlate elements by matching the
			// subset of attributes explicitly set in config, except for the
			// special diff suppression rule which allows for there to be a
			// planned value that is constructed by mixing part of a prior
			// value with part of a config value, creating an entirely new
			// element that is not present in either prior nor config.)
			for it := plannedV.ElementIterator(); it.Next(); {
				idx, plannedEV := it.Element()
				path := append(path, cty.IndexStep{Key: idx})
				if !plannedEV.IsKnown() {
					errs = append(errs, path.NewErrorf("element representing nested block must not be unknown itself; set nested attribute values to unknown instead"))
					continue
				}
			}

		default:
			panic(fmt.Sprintf("unsupported nesting mode %s", blockS.Nesting))
		}
	}

	return errs
}

func assertPlannedAttrsValid(schema map[string]*configschema.Attribute, priorState, config, plannedState cty.Value, path cty.Path) []error {
	var errs []error
	for name, attrS := range schema {
		moreErrs := assertPlannedAttrValid(name, attrS, priorState, config, plannedState, path)
		errs = append(errs, moreErrs...)
	}
	return errs
}

func assertPlannedAttrValid(name string, attrS *configschema.Attribute, priorState, config, plannedState cty.Value, path cty.Path) []error {
	plannedV := plannedState.GetAttr(name)
	configV := config.GetAttr(name)
	priorV := cty.NullVal(attrS.Type)
	if !priorState.IsNull() {
		priorV = priorState.GetAttr(name)
	}
	path = append(path, cty.GetAttrStep{Name: name})

	return assertPlannedValueValid(attrS, priorV, configV, plannedV, path)
}

func assertPlannedValueValid(attrS *configschema.Attribute, priorV, configV, plannedV cty.Value, path cty.Path) []error {

	var errs []error
	if unrefinedValue(plannedV).RawEquals(unrefinedValue(configV)) {
		// This is the easy path: provider didn't change anything at all.
		return errs
	}
	if unrefinedValue(plannedV).RawEquals(unrefinedValue(priorV)) && !priorV.IsNull() && !configV.IsNull() {
		// Also pretty easy: there is a prior value and the provider has
		// returned it unchanged. This indicates that configV and plannedV
		// are functionally equivalent and so the provider wishes to disregard
		// the configuration value in favor of the prior.
		return errs
	}

	switch {
	// The provider can plan any value for a computed-only attribute. There may
	// be a config value here in the case where a user used `ignore_changes` on
	// a computed attribute and ignored the warning, or we failed to validate
	// computed attributes in the config, but regardless it's not a plan error
	// caused by the provider.
	case attrS.Computed && !attrS.Optional:
		return errs

	// The provider is allowed to insert optional values when the config is
	// null, but only if the attribute is computed.
	case configV.IsNull() && attrS.Computed:
		return errs

	case configV.IsNull() && !plannedV.IsNull():
		// if the attribute is not computed, then any planned value is incorrect
		if attrS.Sensitive {
			errs = append(errs, path.NewErrorf("sensitive planned value for a non-computed attribute"))
		} else {
			errs = append(errs, path.NewErrorf("planned value %#v for a non-computed attribute", plannedV))
		}
		return errs
	}

	// If there is a config value but there is nothing planned, we allow this in case of write only attributes.
	// This validation covers all the cases:
	//  - root resource attributes
	//  - nested attributes object attributes
	//  - nested blocks attributes
	if !configV.IsNull() && plannedV.IsNull() && attrS.WriteOnly {
		return errs
	}

	// If this attribute has a NestedType, validate the nested object
	if attrS.NestedType != nil {
		return assertPlannedObjectValid(attrS.NestedType, priorV, configV, plannedV, path)
	}

	// If none of the above conditions match, the provider has made an invalid
	// change to this attribute.
	if priorV.IsNull() {
		if attrS.Sensitive {
			errs = append(errs, path.NewErrorf("sensitive planned value does not match config value"))
		} else {
			errs = append(errs, path.NewErrorf("planned value %#v does not match config value %#v", plannedV, configV))
		}
		return errs
	}

	if attrS.Sensitive {
		errs = append(errs, path.NewErrorf("sensitive planned value does not match config value nor prior value"))
	} else {
		errs = append(errs, path.NewErrorf("planned value %#v does not match config value %#v nor prior value %#v", plannedV, configV, priorV))
	}

	return errs
}

func assertPlannedObjectValid(schema *configschema.Object, prior, config, planned cty.Value, path cty.Path) []error {
	var errs []error

	if planned.IsNull() && !config.IsNull() {
		errs = append(errs, path.NewErrorf("planned for absence but config wants existence"))
		return errs
	}
	if config.IsNull() && !planned.IsNull() {
		errs = append(errs, path.NewErrorf("planned for existence but config wants absence"))
		return errs
	}
	if !config.IsNull() && !planned.IsKnown() {
		errs = append(errs, path.NewErrorf("planned unknown for configured value"))
		return errs
	}

	if planned.IsNull() {
		// No further checks possible if the planned value is null
		return errs
	}

	switch schema.Nesting {
	case configschema.NestingSingle, configschema.NestingGroup:
		moreErrs := assertPlannedAttrsValid(schema.Attributes, prior, config, planned, path)
		errs = append(errs, moreErrs...)

	case configschema.NestingList:
		// A NestingList might either be a list or a tuple, depending on
		// whether there are dynamically-typed attributes inside. However,
		// both support a similar-enough API that we can treat them the
		// same for our purposes here.

		plannedL := planned.Length()
		configL := config.Length()

		// config wasn't known, then planned should be unknown too
		if !plannedL.IsKnown() && !configL.IsKnown() {
			return errs
		}

		lenEqual := plannedL.Equals(configL)
		if !lenEqual.IsKnown() || lenEqual.False() {
			errs = append(errs, path.NewErrorf("count in plan (%#v) disagrees with count in config (%#v)", plannedL, configL))
			return errs
		}
		for it := planned.ElementIterator(); it.Next(); {
			idx, plannedEV := it.Element()
			path := append(path, cty.IndexStep{Key: idx})
			if !config.HasIndex(idx).True() {
				continue // should never happen since we checked the lengths above
			}
			configEV := config.Index(idx)
			priorEV := cty.NullVal(schema.ImpliedType())
			if !prior.IsNull() && prior.HasIndex(idx).True() {
				priorEV = prior.Index(idx)
			}

			moreErrs := assertPlannedAttrsValid(schema.Attributes, priorEV, configEV, plannedEV, path)
			errs = append(errs, moreErrs...)
		}

	case configschema.NestingMap:
		// A NestingMap might either be a map or an object, depending on
		// whether there are dynamically-typed attributes inside, so we will
		// break these down to maps to handle them both in the same manner.
		plannedVals := map[string]cty.Value{}
		configVals := map[string]cty.Value{}
		priorVals := map[string]cty.Value{}

		plannedL := planned.Length()
		configL := config.Length()

		// config wasn't known, then planned should be unknown too
		if !plannedL.IsKnown() && !configL.IsKnown() {
			return errs
		}

		lenEqual := plannedL.Equals(configL)
		if !lenEqual.IsKnown() || lenEqual.False() {
			errs = append(errs, path.NewErrorf("count in plan (%#v) disagrees with count in config (%#v)", plannedL, configL))
			return errs
		}

		if !planned.IsNull() {
			plannedVals = planned.AsValueMap()
		}
		if !config.IsNull() {
			configVals = config.AsValueMap()
		}
		if !prior.IsNull() {
			priorVals = prior.AsValueMap()
		}

		for k, plannedEV := range plannedVals {
			configEV, ok := configVals[k]
			if !ok {
				errs = append(errs, path.NewErrorf("map key %q from plan is not present in config", k))
				continue
			}
			path := append(path, cty.GetAttrStep{Name: k})

			priorEV, ok := priorVals[k]
			if !ok {
				priorEV = cty.NullVal(schema.ImpliedType())
			}
			moreErrs := assertPlannedAttrsValid(schema.Attributes, priorEV, configEV, plannedEV, path)
			errs = append(errs, moreErrs...)
		}
		for k := range configVals {
			if _, ok := plannedVals[k]; !ok {
				errs = append(errs, path.NewErrorf("map key %q from config is not present in plan", k))
				continue
			}
		}

	case configschema.NestingSet:
		plannedL := planned.Length()
		configL := config.Length()

		if ok := plannedL.Range().Includes(configL); ok.IsKnown() && ok.False() {
			errs = append(errs, path.NewErrorf("count in plan (%#v) disagrees with count in config (%#v)", plannedL, configL))
			return errs
		}
		// Because set elements have no identifier with which to correlate
		// them, we can't robustly validate the plan for a nested object
		// backed by a set, and so unfortunately we need to just trust the
		// provider to do the right thing.
	}

	return errs
}

// unrefinedValue returns the given value with any unknown value refinements
// stripped away, making it a basic unknown value with only a type constraint.
//
// This function also considers unknown values nested inside a known container
// such as a collection, which unfortunately makes it relatively expensive
// for large data structures. Over time we should transition away from using
// this trick and prefer to use cty's Equals and value range APIs instead of
// of using Value.RawEquals, which is primarily intended for unit test code
// rather than real application use.
func unrefinedValue(v cty.Value) cty.Value {
	ret, _ := cty.Transform(v, func(p cty.Path, v cty.Value) (cty.Value, error) {
		if !v.IsKnown() {
			return cty.UnknownVal(v.Type()), nil
		}
		return v, nil
	})
	return ret
}
