package resource

import (
	"bytes"
	"errors"
	"fmt"
	"strconv"
	"strings"

	"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
	"github.com/pulumi/pulumi/sdk/v3/go/common/util/logging"
)

// PropertyPath represents a path to a nested property. The path may be composed of strings (which access properties
// in ObjectProperty values) and integers (which access elements of ArrayProperty values).
type PropertyPath []interface{}

// ParsePropertyPath parses a property path into a PropertyPath value.
//
// A property path string is essentially a Javascript property access expression in which all elements are literals.
// Valid property paths obey the following EBNF-ish grammar:
//
//	propertyName := [a-zA-Z_$] { [a-zA-Z0-9_$] }
//	quotedPropertyName := '"' ( '\' '"' | [^"] ) { ( '\' '"' | [^"] ) } '"'
//	arrayIndex := { [0-9] }
//
//	propertyIndex := '[' ( quotedPropertyName | arrayIndex ) ']'
//	rootProperty := ( propertyName | propertyIndex )
//	propertyAccessor := ( ( '.' propertyName ) |  propertyIndex )
//	path := rootProperty { propertyAccessor }
//
// Examples of valid paths:
// - root
// - root.nested
// - root["nested"]
// - root.double.nest
// - root["double"].nest
// - root["double"]["nest"]
// - root.array[0]
// - root.array[100]
// - root.array[0].nested
// - root.array[0][1].nested
// - root.nested.array[0].double[1]
// - root["key with \"escaped\" quotes"]
// - root["key with a ."]
// - ["root key with \"escaped\" quotes"].nested
// - ["root key with a ."][100]
// - root.array[*].field
// - root.array["*"].field
func ParsePropertyPath(path string) (PropertyPath, error) {
	// We interpret the grammar above a little loosely in order to keep things simple. Specifically, we will accept
	// something close to the following:
	// pathElement := { '.' } [a-zA-Z_$][a-zA-Z0-9_$]
	// pathIndex := '[' ( [0-9]+ | '"' ('\' '"' | [^"] )+ '"' ']'
	// path := { pathElement | pathIndex }
	var elements []interface{}
	if len(path) > 0 && path[0] == '.' {
		return nil, errors.New("expected property path to start with a name or index")
	}
	for len(path) > 0 {
		switch path[0] {
		case '.':
			path = path[1:]
			if len(path) == 0 {
				return nil, errors.New("expected property path to end with a name or index")
			}
			if path[0] == '[' {
				// We tolerate a '.' followed by a '[', which is not strictly legal, but is common from old providers.
				logging.V(10).Infof("property path '%s' contains a '.' followed by a '['; this is not strictly legal", path)
			}
		case '[':
			// If the character following the '[' is a '"', parse a string key.
			var pathElement interface{}
			if len(path) > 1 && path[1] == '"' {
				var propertyKey []byte
				var i int
				for i = 2; ; {
					if i >= len(path) {
						return nil, errors.New("missing closing quote in property name")
					} else if path[i] == '"' {
						i++
						break
					} else if path[i] == '\\' && i+1 < len(path) && path[i+1] == '"' {
						propertyKey = append(propertyKey, '"')
						i += 2
					} else {
						propertyKey = append(propertyKey, path[i])
						i++
					}
				}
				if i >= len(path) || path[i] != ']' {
					return nil, errors.New("missing closing bracket in property access")
				}
				pathElement, path = string(propertyKey), path[i:]
			} else {
				// Look for a closing ']'
				rbracket := strings.IndexRune(path, ']')
				if rbracket == -1 {
					return nil, errors.New("missing closing bracket in array index")
				}

				segment := path[1:rbracket]
				if segment == "*" {
					pathElement, path = "*", path[rbracket:]
				} else {
					index, err := strconv.ParseInt(segment, 10, 0)
					if err != nil {
						return nil, fmt.Errorf("invalid array index: %w", err)
					}
					pathElement, path = int(index), path[rbracket:]
				}
			}
			elements, path = append(elements, pathElement), path[1:]
		default:
			for i := 0; ; i++ {
				if i == len(path) || path[i] == '.' || path[i] == '[' {
					elements, path = append(elements, path[:i]), path[i:]
					break
				}
			}
		}
	}
	return PropertyPath(elements), nil
}

// Get attempts to get the value located by the PropertyPath inside the given PropertyValue. If any component of the
// path does not exist, this function will return (NullPropertyValue, false).
func (p PropertyPath) Get(v PropertyValue) (PropertyValue, bool) {
	for _, key := range p {
		switch {
		case v.IsArray():
			index, ok := key.(int)
			if !ok || index < 0 || index >= len(v.ArrayValue()) {
				return PropertyValue{}, false
			}
			v = v.ArrayValue()[index]
		case v.IsObject():
			k, ok := key.(string)
			if !ok {
				return PropertyValue{}, false
			}
			v, ok = v.ObjectValue()[PropertyKey(k)]
			if !ok {
				return PropertyValue{}, false
			}
		default:
			return PropertyValue{}, false
		}
	}
	return v, true
}

// Set attempts to set the location inside a PropertyValue indicated by the PropertyPath to the given value. If any
// component of the path besides the last component does not exist, this function will return false.
func (p PropertyPath) Set(dest, v PropertyValue) bool {
	if len(p) == 0 {
		return false
	}

	dest, ok := p[:len(p)-1].Get(dest)
	if !ok {
		return false
	}

	key := p[len(p)-1]
	switch {
	case dest.IsArray():
		index, ok := key.(int)
		if !ok || index < 0 || index >= len(dest.ArrayValue()) {
			return false
		}
		dest.ArrayValue()[index] = v
	case dest.IsObject():
		k, ok := key.(string)
		if !ok {
			return false
		}
		dest.ObjectValue()[PropertyKey(k)] = v
	default:
		return false
	}
	return true
}

// Add sets the location inside a PropertyValue indicated by the PropertyPath to the given value. Any components
// referred to by the path that do not exist will be created. If there is a mismatch between the type of an existing
// component and a key that traverses that component, this function will return false. If the destination is a null
// property value, this function will create and return a new property value.
func (p PropertyPath) Add(dest, v PropertyValue) (PropertyValue, bool) {
	if len(p) == 0 {
		return PropertyValue{}, false
	}

	// set sets the destination referred to by the last element of the path to the given value.
	rv := dest
	set := func(v PropertyValue) {
		dest, rv = v, v
	}
	for _, key := range p {
		switch key := key.(type) {
		case int:
			// This key is an int, so we expect an array.
			switch {
			case dest.IsNull():
				// If the destination array does not exist, create a new array with enough room to store the value at
				// the requested index.
				dest = NewArrayProperty(make([]PropertyValue, key+1))
				set(dest)
			case dest.IsArray():
				// If the destination array does exist, ensure that it is large enough to accommodate the requested
				// index.
				if arr := dest.ArrayValue(); key >= len(arr) {
					dest = NewArrayProperty(append(make([]PropertyValue, key+1-len(arr)), arr...))
					set(dest)
				}
			default:
				return PropertyValue{}, false
			}
			destV := dest.ArrayValue()
			set = func(v PropertyValue) {
				destV[key] = v
			}
			dest = destV[key]
		case string:
			// This key is a string, so we expect an object.
			switch {
			case dest.IsNull():
				// If the destination does not exist, create a new object.
				dest = NewObjectProperty(PropertyMap{})
				set(dest)
			case dest.IsObject():
				// OK
			default:
				return PropertyValue{}, false
			}
			destV := dest.ObjectValue()
			set = func(v PropertyValue) {
				destV[PropertyKey(key)] = v
			}
			dest = destV[PropertyKey(key)]
		default:
			return PropertyValue{}, false
		}
	}

	set(v)
	return rv, true
}

// Delete attempts to delete the value located by the PropertyPath inside the given PropertyValue. If any component
// of the path does not exist, this function will return false.
func (p PropertyPath) Delete(dest PropertyValue) bool {
	if len(p) == 0 {
		return false
	}

	dest, ok := p[:len(p)-1].Get(dest)
	if !ok {
		return false
	}

	key := p[len(p)-1]
	switch {
	case dest.IsArray():
		index, ok := key.(int)
		if !ok || index < 0 || index >= len(dest.ArrayValue()) {
			return false
		}
		dest.ArrayValue()[index] = PropertyValue{}
	case dest.IsObject():
		k, ok := key.(string)
		if !ok {
			return false
		}
		delete(dest.ObjectValue(), PropertyKey(k))
	default:
		return false
	}
	return true
}

// Contains returns true if the receiver property path contains the other property path.
// For example, the path `foo["bar"][1]` contains the path `foo.bar[1].baz`.  The key `"*"`
// is a wildcard which matches any string or int index at that same nesting level.  So for example,
// the path `foo.*.baz` contains `foo.bar.baz.bam`, and the path `*` contains any path.
func (p PropertyPath) Contains(other PropertyPath) bool {
	if len(other) < len(p) {
		return false
	}

	for i := range p {
		pp := p[i]
		otherp := other[i]

		switch pp := pp.(type) {
		case int:
			if otherpi, ok := otherp.(int); !ok || otherpi != pp {
				return false
			}
		case string:
			if pp == "*" {
				continue
			}
			if otherps, ok := otherp.(string); !ok || otherps != pp {
				return false
			}
		default:
			// Invalid path, return false
			return false
		}
	}

	return true
}

// unwrapSecrets recursively unwraps any secrets from the given PropertyValue returning true if any secrets were
// unwrapped.
func unwrapSecrets(v PropertyValue) (PropertyValue, bool) {
	if v.IsSecret() {
		inner, _ := unwrapSecrets(v.SecretValue().Element)
		return inner, true
	}
	return v, false
}

func (p PropertyPath) reset(old, new PropertyValue, oldIsSecret, newIsSecret bool) bool {
	if len(p) == 0 {
		return false
	}

	// Unwrap any secrets from old & new, we can just go through them for this traversal.
	old, isSecret := unwrapSecrets(old)
	oldIsSecret = oldIsSecret || isSecret
	new, isSecret = unwrapSecrets(new)
	newIsSecret = newIsSecret || isSecret

	// If this is the last component we want to do the reset, else we want to search for the next component.
	key := p[0]
	switch key := key.(type) {
	case int:
		// An index < 0 is always a path error, even for empty arrays or objects
		if key < 0 {
			return false
		}

		// This is a leaf path element, so we want to reset the value at this index in new to the value at this index from old
		if len(p) == 1 {
			if !old.IsArray() && !new.IsArray() {
				// Neither old nor new are arrays, so we can't reset this index
				return true
			} else if !old.IsArray() || !new.IsArray() {
				// One of old or new is an array but the other isn't, so this is a path error
				return false
			}

			// If neither array contains this index then this is a _same_ and so ok, e.g. given old:[1, 2] and
			// new:[1] and a path of [3] we can return true because new at [3] is the same as old at [3], it
			// doesn't exist.
			if key >= len(old.ArrayValue()) && key >= len(new.ArrayValue()) {
				return true
			}
			// If one array has this index but the other doesn't this is a path failure because we can't
			// remove a location from an array.
			if key >= len(old.ArrayValue()) || key >= len(new.ArrayValue()) {
				return false
			}
			// Otherwise both arrays contain this index and we can reset the value of it in new to what is in
			// old.
			v := old.ArrayValue()[key]
			// If this was a secret value in old, but new isn't currently a secret context then we need to mark this
			// reset value as secret.
			if oldIsSecret && !newIsSecret {
				v = MakeSecret(v)
			}
			new.ArrayValue()[key] = v
			return true
		}

		if !old.IsArray() || !new.IsArray() {
			// At least one of old or new is not an array, so we can't keep searching along this path but
			// we only return an error if both are not arrays.
			return !old.IsArray() && !new.IsArray()
		}

		// If this index is out of bounds in either array then this is a path failure because we can't
		// continue the search of this path down each PropertyValue.
		if key >= len(old.ArrayValue()) || key >= len(new.ArrayValue()) {
			return false
		}
		old = old.ArrayValue()[key]
		new = new.ArrayValue()[key]
		return p[1:].reset(old, new, oldIsSecret, newIsSecret)

	case string:
		if key == "*" {
			if len(p) == 1 {
				if new.IsObject() {
					if old.IsObject() {
						for k := range old.ObjectValue() {
							v := old.ObjectValue()[k]
							// If this was a secret value in old, but new isn't currently a secret context then we need
							// to mark this reset value as secret.
							if oldIsSecret && !newIsSecret {
								v = MakeSecret(v)
							}
							new.ObjectValue()[k] = v
						}
						for k := range new.ObjectValue() {
							if _, has := old.ObjectValue()[k]; !has {
								delete(new.ObjectValue(), k)
							}
						}
					}
					return true
				} else if new.IsArray() {
					if old.IsArray() {
						for i := range old.ArrayValue() {
							v := old.ArrayValue()[i]
							// If this was a secret value in old, but new isn't currently a secret context then we need
							// to mark this reset value as secret.
							if oldIsSecret && !newIsSecret {
								v = MakeSecret(v)
							}
							new.ArrayValue()[i] = v
						}
					}
					return true
				}
				return false
			}

			if old.IsObject() && new.IsObject() {
				oldObject := old.ObjectValue()
				newObject := new.ObjectValue()

				for k := range oldObject {
					var hasOld, hasNew bool
					oldValue, hasOld := oldObject[k]
					newValue, hasNew := newObject[k]
					if !hasOld || !hasNew {
						return false
					}

					if !p[1:].reset(oldValue, newValue, oldIsSecret, newIsSecret) {
						return false
					}
				}
				return true
			} else if old.IsArray() && new.IsArray() {
				oldArray := old.ArrayValue()
				newArray := new.ArrayValue()

				for i := range oldArray {
					if !p[1:].reset(oldArray[i], newArray[i], oldIsSecret, newIsSecret) {
						return false
					}
				}
				return true
			}
			return false
		} else {
			pkey := PropertyKey(key)

			if len(p) == 1 {
				// This is the leaf path entry, so we want to reset this property in new to it's value in old.

				// Firstly if old doesn't have this key (either because it isn't an object or because it
				// doesn't have the property) then we want to delete this from new.
				var v PropertyValue
				var has bool
				if old.IsObject() {
					v, has = old.ObjectValue()[pkey]
				}

				if has {
					// If this path exists in old but new isn't an object than return a path error
					if !new.IsObject() {
						return false
					}
					// Else simply overwrite the value in new with the value from old, if this was a secret value in
					// old, but new isn't currently a secret context then we need to mark this reset value as secret.
					if oldIsSecret && !newIsSecret {
						v = MakeSecret(v)
					}
					new.ObjectValue()[pkey] = v
				} else {
					// If the path doesn't exist in old then we want to delete it from new, but if new isn't
					// an object then we can just do nothing we don't consider this a path error. e.g. given
					// old:{} and new:1 and a path of "a" we can return true because ["a"] in both is the
					// same (it doesn't exist).
					if new.IsObject() {
						delete(new.ObjectValue(), pkey)
					}
				}
				return true
			}

			if !old.IsObject() || !new.IsObject() {
				// At least one of old or new is not an object, so we can't keep searching along this path but
				// we only return an error if both are not objects.
				return !old.IsObject() && !new.IsObject()
			}

			new, hasNew := new.ObjectValue()[pkey]
			old, hasOld := old.ObjectValue()[pkey]

			if hasOld && !hasNew {
				// Old has this key but new doesn't, but we still searching for the leaf item to set so this
				// is a path error.
				return false
			}
			if !hasOld && !hasNew {
				// Neither value contain this path, so we're done.
				return true
			}

			return p[1:].reset(old, new, oldIsSecret, newIsSecret)
		}
	}

	contract.Failf("Invalid property path component type: %T", key)
	return true
}

// Reset attempts to reset the values located by the PropertyPath inside the given new PropertyMap to the
// values from the same location in the old PropertyMap. Reset behaves likes Set in that it will not create
// intermediate locations, it also won't create or delete array locations (because that would change the size
// of the array).
func (p PropertyPath) Reset(old, new PropertyMap) bool {
	return p.reset(NewObjectProperty(old), NewObjectProperty(new), false, false)
}

func requiresQuote(c rune) bool {
	return !(c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z' || c >= '0' && c <= '9' || c == '_')
}

func (p PropertyPath) String() string {
	var buf bytes.Buffer
	for i, k := range p {
		switch k := k.(type) {
		case string:
			var keyBuf bytes.Buffer
			quoted := false
			for _, c := range k {
				if requiresQuote(c) {
					quoted = true
					if c == '"' {
						keyBuf.WriteByte('\\')
					}
				}
				keyBuf.WriteRune(c)
			}
			if !quoted {
				if i == 0 {
					fmt.Fprintf(&buf, "%s", keyBuf.String())
				} else {
					fmt.Fprintf(&buf, ".%s", keyBuf.String())
				}
			} else {
				fmt.Fprintf(&buf, `["%s"]`, keyBuf.String())
			}
		case int:
			fmt.Fprintf(&buf, "[%d]", k)
		}
	}
	return buf.String()
}