package ldevents

import (
	"github.com/launchdarkly/go-sdk-common/v3/ldattr"
	"github.com/launchdarkly/go-sdk-common/v3/ldcontext"
	"github.com/launchdarkly/go-sdk-common/v3/ldvalue"

	"github.com/launchdarkly/go-jsonstream/v3/jwriter"
)

// eventContextFormatter provides the special JSON serialization format that is used when including Context
// data in analytics events. In this format, some attribute values may be redacted based on the SDK's
// events configuration and/or the per-Context setting of ldcontext.Builder.Private().
type eventContextFormatter struct {
	allAttributesPrivate bool
	privateAttributes    map[string]*privateAttrLookupNode
}

type privateAttrLookupNode struct {
	attribute *ldattr.Ref
	children  map[string]*privateAttrLookupNode
}

// newEventContextFormatter creates an eventContextFormatter.
//
// An instance of this type is owned by the eventOutputFormatter that is responsible for writing all
// JSON event data. It is created at SDK initialization time based on the SDK configuration.
func newEventContextFormatter(config EventsConfiguration) eventContextFormatter {
	ret := eventContextFormatter{allAttributesPrivate: config.AllAttributesPrivate}
	if len(config.PrivateAttributes) != 0 {
		// Reformat the list of private attributes into a map structure that will allow
		// for faster lookups.
		ret.privateAttributes = makePrivateAttrLookupData(config.PrivateAttributes)
	}
	return ret
}

func makePrivateAttrLookupData(attrRefList []ldattr.Ref) map[string]*privateAttrLookupNode {
	// This function transforms a list of AttrRefs into a data structure that allows for more efficient
	// implementation of eventContextFormatter.checkGloballyPrivate().
	//
	// For instance, if the original AttrRefs were "/name", "/address/street", and "/address/city",
	// it would produce the following map:
	//
	// "name": {
	//   attribute: NewAttrRef("/name"),
	// },
	// "address": {
	//   children: {
	//     "street": {
	//       attribute: NewAttrRef("/address/street/"),
	//     },
	//     "city": {
	//       attribute: NewAttrRef("/address/city/"),
	//     },
	//   },
	// }
	ret := make(map[string]*privateAttrLookupNode)
	for _, a := range attrRefList {
		parentMap := &ret
		for i := 0; i < a.Depth(); i++ {
			name := a.Component(i)
			if *parentMap == nil {
				*parentMap = make(map[string]*privateAttrLookupNode)
			}
			nextNode := (*parentMap)[name]
			if nextNode == nil {
				nextNode = &privateAttrLookupNode{}
				if i == a.Depth()-1 {
					aa := a
					nextNode.attribute = &aa
				}
				(*parentMap)[name] = nextNode
			}
			parentMap = &nextNode.children
		}
	}
	return ret
}

// WriteContext serializes a Context in the format appropriate for an analytics event, redacting
// private attributes if necessary.
func (f *eventContextFormatter) WriteContext(w *jwriter.Writer, ec *EventInputContext) {
	if ec.preserialized != nil {
		w.Raw(ec.preserialized)
		return
	}
	if ec.context.Err() != nil {
		w.AddError(ec.context.Err())
		return
	}
	if ec.context.Multiple() {
		f.writeContextInternalMulti(w, ec)
	} else {
		f.writeContextInternalSingle(w, &ec.context, true)
	}
}

func (f *eventContextFormatter) writeContextInternalSingle(
	w *jwriter.Writer,
	c *ldcontext.Context,
	includeKind bool,
) {
	obj := w.Object()
	if includeKind {
		obj.Name(ldattr.KindAttr).String(string(c.Kind()))
	}

	obj.Name(ldattr.KeyAttr).String(c.Key())

	optionalAttrNames := make([]string, 0, 50) // arbitrary capacity, expanded if necessary by GetOptionalAttributeNames
	redactedAttrs := make([]string, 0, 20)

	optionalAttrNames = c.GetOptionalAttributeNames(optionalAttrNames)

	for _, key := range optionalAttrNames {
		if value := c.GetValue(key); value.IsDefined() {
			if f.allAttributesPrivate {
				// If allAttributesPrivate is true, then there's no complex filtering or recursing to be done: all of
				// these values are by definition private, so just add their names to the redacted list. Since the
				// redacted list uses the attribute reference syntax, we may need to escape the value if the name of
				// this individual attribute happens to be something like "/a/b"; the easiest way to do that is to
				// call NewLiteralRef and then convert the Ref to an attribute reference string.
				escapedAttrName := ldattr.NewLiteralRef(key).String()
				redactedAttrs = append(redactedAttrs, escapedAttrName)
				continue
			}
			path := make([]string, 0, 10)
			f.writeFilteredAttribute(w, c, &obj, path, key, value, &redactedAttrs)
		}
	}

	if c.Anonymous() {
		obj.Name(ldattr.AnonymousAttr).Bool(true)
	}

	anyRedacted := len(redactedAttrs) != 0
	if anyRedacted {
		metaJSON := obj.Name("_meta").Object()
		privateAttrsJSON := metaJSON.Name("redactedAttributes").Array()
		for _, a := range redactedAttrs {
			privateAttrsJSON.String(a)
		}
		privateAttrsJSON.End()
		metaJSON.End()
	}

	obj.End()
}

func (f *eventContextFormatter) writeContextInternalMulti(w *jwriter.Writer, ec *EventInputContext) {
	obj := w.Object()
	obj.Name(ldattr.KindAttr).String(string(ldcontext.MultiKind))

	for i := 0; i < ec.context.IndividualContextCount(); i++ {
		if ic := ec.context.IndividualContextByIndex(i); ic.IsDefined() {
			obj.Name(string(ic.Kind()))
			f.writeContextInternalSingle(w, &ic, false)
		}
	}

	obj.End()
}

// writeFilteredAttribute checks whether a given value should be considered private, and then
// either writes the attribute to the output JSON object if it is *not* private, or adds the
// corresponding attribute reference to the redactedAttrs list if it is private.
//
// The parentPath parameter indicates where we are in the context data structure. If it is empty,
// we are at the top level and "key" is an attribute name. If it is not empty, we are recursing
// into the properties of an attribute value that is a JSON object: for instance, if parentPath
// is ["billing", "address"] and key is "street", then the top-level attribute is "billing" and
// has a value in the form {"address": {"street": ...}} and we are now deciding whether to
// write the "street" property. See maybeRedact() for the logic involved in that decision.
//
// If allAttributesPrivate is true, this method is never called.
func (f *eventContextFormatter) writeFilteredAttribute(
	w *jwriter.Writer,
	c *ldcontext.Context,
	parentObj *jwriter.ObjectState,
	parentPath []string,
	key string,
	value ldvalue.Value,
	redactedAttrs *[]string,
) {
	path := append(parentPath, key) //nolint:gocritic // purposely not assigning to same slice

	isRedacted, nestedPropertiesAreRedacted := f.maybeRedact(c, path, value.Type(), redactedAttrs)

	if value.Type() != ldvalue.ObjectType {
		// For all value types except object, the question is only "is there a private attribute
		// reference that directly points to this property", since there are no nested properties.
		if !isRedacted {
			parentObj.Name(key)
			value.WriteToJSONWriter(w)
		}
		return
	}

	// If the value is an object, then there are three possible outcomes: 1. this value is
	// completely redacted, so drop it and do not recurse; 2. the value is not redacted, and
	// and neither are any subproperties within it, so output the whole thing as-is; 3. the
	// value itself is not redacted, but some subproperties within it are, so we'll need to
	// recurse through it and filter as we go.
	if isRedacted {
		return // outcome 1
	}
	parentObj.Name(key)
	if !nestedPropertiesAreRedacted {
		value.WriteToJSONWriter(w) // writes the whole value unchanged
		return                     // outcome 2
	}
	subObj := w.Object() // writes the opening brace for the output object

	objectKeys := make([]string, 0, 50) // arbitrary capacity, expanded if necessary by value.Keys()
	for _, subKey := range value.Keys(objectKeys) {
		subValue := value.GetByKey(subKey)
		// recurse to write or not write each property - outcome 3
		f.writeFilteredAttribute(w, c, &subObj, path, subKey, subValue, redactedAttrs)
	}
	subObj.End() // writes the closing brace for the output object
}

// maybeRedact is called by writeFilteredAttribute to decide whether or not a given value (or,
// possibly, properties within it) should be considered private, based on the private attribute
// references in either 1. the eventContextFormatter configuration or 2. this specific Context.
//
// If the value should be private, then the first return value is true, and also the attribute
// reference is added to redactedAttrs.
//
// The second return value indicates whether there are any private attribute references
// designating properties *within* this value. That is, if attrPath is ["address"], and the
// configuration says that "/address/street" is private, then the second return value will be
// true, which tells us that we can't just dump the value of the "address" object directly into
// the output but will need to filter its properties.
//
// Note that even though an AttrRef can contain numeric path components to represent an array
// element lookup, for the purposes of flag evaluations (like "/animals/0" which conceptually
// represents context.animals[0]), those will not work as private attribute references since
// we do not recurse to redact anything within an array value. A reference like "/animals/0"
// would only work if context.animals were an object with a property named "0".
//
// If allAttributesPrivate is true, this method is never called.
func (f *eventContextFormatter) maybeRedact(
	c *ldcontext.Context,
	attrPath []string,
	valueType ldvalue.ValueType,
	redactedAttrs *[]string,
) (bool, bool) {
	// First check against the eventContextFormatter configuration.
	redactedAttrRef, nestedPropertiesAreRedacted := f.checkGlobalPrivateAttrRefs(attrPath)
	if redactedAttrRef != nil {
		*redactedAttrs = append(*redactedAttrs, redactedAttrRef.String())
		return true, false
		// true, false = "this attribute itself is redacted, never mind its children"
	}

	shouldCheckForNestedProperties := valueType == ldvalue.ObjectType

	// Now check the per-Context configuration. Unlike the eventContextFormatter configuration, this
	// does not have a lookup map, just a list of AttrRefs.
	for i := 0; i < c.PrivateAttributeCount(); i++ {
		a, _ := c.PrivateAttributeByIndex(i)
		depth := a.Depth()
		if depth < len(attrPath) {
			// If the attribute reference is shorter than the current path, then it can't possibly be a match,
			// because if it had matched the first part of our path, we wouldn't have recursed this far.
			continue
		}
		if !shouldCheckForNestedProperties && depth > len(attrPath) {
			continue
		}
		match := true
		for j := 0; j < len(attrPath); j++ {
			name := a.Component(j)
			if name != attrPath[j] {
				match = false
				break
			}
		}
		if match {
			if depth == len(attrPath) {
				*redactedAttrs = append(*redactedAttrs, a.String())
				return true, false
				// true, false = "this attribute itself is redacted, never mind its children"
			}
			nestedPropertiesAreRedacted = true
		}
	}
	return false, nestedPropertiesAreRedacted // false = "this attribute itself is not redacted"
}

// Checks whether the given attribute or subproperty matches any AttrRef that was designated as
// private in the SDK options given to newEventContextFormatter.
//
// If attrPath has just one element, it is the name of a top-level attribute. If it has multiple
// elements, it is a path to a property within a custom object attribute: for instance, if you
// represented the overall context as a JSON object, the attrPath ["billing", "address", "street"]
// would refer to the street property within something like {"billing": {"address": {"street": "x"}}}.
//
// The first return value is nil if the attribute does not need to be redacted; otherwise it is the
// specific attribute reference that was matched.
//
// The second return value is true if and only if there's at least one configured private
// attribute reference for *children* of attrPath (and there is not one for attrPath itself, since if
// there was, we would not bother recursing to write the children). See comments on writeFilteredAttribute.
func (f eventContextFormatter) checkGlobalPrivateAttrRefs(attrPath []string) (
	redactedAttrRef *ldattr.Ref, nestedPropertiesAreRedacted bool,
) {
	redactedAttrRef = nil
	nestedPropertiesAreRedacted = false
	lookup := f.privateAttributes
	if lookup == nil {
		return
	}
	for i, pathComponent := range attrPath {
		nextNode := lookup[pathComponent]
		if nextNode == nil {
			break
		}
		if i == len(attrPath)-1 {
			if nextNode.attribute != nil {
				redactedAttrRef = nextNode.attribute
				return
			}
			nestedPropertiesAreRedacted = true
			return
		} else if nextNode.children != nil {
			lookup = nextNode.children
			continue
		}
	}
	return
}
