package ldmodel

import (
	"github.com/launchdarkly/go-jsonstream/v3/jwriter"
	"github.com/launchdarkly/go-sdk-common/v3/ldattr"
	"github.com/launchdarkly/go-sdk-common/v3/ldcontext"
)

// For backward compatibility, we are only allowed to drop out properties that have default values if
// Go SDK 4.x would also have done so (since some SDKs are not tolerant of missing properties in
// general). This is true of all properties that have OptionalInt-like semantics (having either a
// numeric value or "undefined"), and properties that could be either a JSON object or null (like
// VariationOrRollout.Rollout), and the BucketBy property which has optional-string-like behavior.
// Array properties should not be dropped even if nil.
//
// Properties that did not exist prior to Go SDK v5 are always safe to drop if they have default
// values, since older SDKs will never look for them. These are:
// - FeatureFlag.ClientSideAvailability
// - Segment.Unbounded

func marshalFeatureFlag(flag FeatureFlag) ([]byte, error) {
	w := jwriter.NewWriter()
	marshalFeatureFlagToWriter(flag, &w)
	return w.Bytes(), w.Error()
}

func marshalFeatureFlagToWriter(flag FeatureFlag, w *jwriter.Writer) {
	obj := w.Object()

	obj.Name("key").String(flag.Key)

	obj.Name("on").Bool(flag.On)

	prereqsArr := obj.Name("prerequisites").Array()
	for _, p := range flag.Prerequisites {
		prereqObj := prereqsArr.Object()
		prereqObj.Name("key").String(p.Key)
		prereqObj.Name("variation").Int(p.Variation)
		prereqObj.End()
	}
	prereqsArr.End()

	writeTargets(&obj, flag.Targets, "targets")
	writeTargets(&obj, flag.ContextTargets, "contextTargets")

	rulesArr := obj.Name("rules").Array()
	for _, r := range flag.Rules {
		ruleObj := rulesArr.Object()
		writeVariationOrRolloutProperties(&ruleObj, r.VariationOrRollout)
		ruleObj.Maybe("id", r.ID != "").String(r.ID)
		writeClauses(w, &ruleObj, r.Clauses)
		ruleObj.Name("trackEvents").Bool(r.TrackEvents)
		ruleObj.End()
	}
	rulesArr.End()

	fallthroughObj := obj.Name("fallthrough").Object()
	writeVariationOrRolloutProperties(&fallthroughObj, flag.Fallthrough)
	fallthroughObj.End()

	flag.OffVariation.WriteToJSONWriter(obj.Name("offVariation"))

	variationsArr := obj.Name("variations").Array()
	for _, v := range flag.Variations {
		v.WriteToJSONWriter(w)
	}
	variationsArr.End()

	// In the older JSON schema, ClientSideAvailability.UsingEnvironmentID was in "clientSide", and
	// ClientSideAvailability.UsingMobileKey was assumed to be true. In the newer schema, those are
	// both in a "clientSideAvailability" object.
	//
	// If ClientSideAvailability.Explicit is true, then this flag used the newer schema and should be
	// reserialized the same way. If it is false, we will reserialize with the old schema, which
	// does not include UsingMobileKey; note that in that case UsingMobileKey is assumed to be true.
	//
	// For backward compatibility with older SDKs that might be reading a flag that was serialized by
	// this SDK, we always include the older "clientSide" property if it would be true.
	if flag.ClientSideAvailability.Explicit {
		csaObj := obj.Name("clientSideAvailability").Object()
		csaObj.Name("usingMobileKey").Bool(flag.ClientSideAvailability.UsingMobileKey)
		csaObj.Name("usingEnvironmentId").Bool(flag.ClientSideAvailability.UsingEnvironmentID)
		csaObj.End()
	}
	obj.Name("clientSide").Bool(flag.ClientSideAvailability.UsingEnvironmentID)

	obj.Name("salt").String(flag.Salt)

	obj.Name("trackEvents").Bool(flag.TrackEvents)
	obj.Name("trackEventsFallthrough").Bool(flag.TrackEventsFallthrough)

	obj.Name("debugEventsUntilDate").Float64OrNull(flag.DebugEventsUntilDate != 0, float64(flag.DebugEventsUntilDate))

	obj.Name("version").Int(flag.Version)

	obj.Name("deleted").Bool(flag.Deleted)

	obj.End()
}

func writeTargets(obj *jwriter.ObjectState, targets []Target, name string) {
	targetsArr := obj.Name(name).Array()
	for _, t := range targets {
		targetObj := targetsArr.Object()
		if t.ContextKind != "" {
			targetObj.Name("contextKind").String(string(t.ContextKind))
		}
		targetObj.Name("variation").Int(t.Variation)
		writeStringArray(&targetObj, "values", t.Values)
		targetObj.End()
	}
	targetsArr.End()
}

func marshalSegment(segment Segment) ([]byte, error) {
	w := jwriter.NewWriter()
	marshalSegmentToWriter(segment, &w)
	return w.Bytes(), w.Error()
}

func marshalSegmentToWriter(segment Segment, w *jwriter.Writer) {
	obj := w.Object()

	obj.Name("key").String(segment.Key)
	writeStringArray(&obj, "included", segment.Included)
	writeStringArray(&obj, "excluded", segment.Excluded)
	writeSegmentTargets(&obj, segment.IncludedContexts, "includedContexts")
	writeSegmentTargets(&obj, segment.ExcludedContexts, "excludedContexts")
	obj.Name("salt").String(segment.Salt)

	rulesArr := obj.Name("rules").Array()
	for _, r := range segment.Rules {
		ruleObj := rulesArr.Object()
		ruleObj.Name("id").String(r.ID)
		writeClauses(w, &ruleObj, r.Clauses)
		ruleObj.Maybe("weight", r.Weight.IsDefined()).Int(r.Weight.IntValue())
		writeAttrRef(ruleObj.Maybe("bucketBy", r.BucketBy.IsDefined()), &r.BucketBy, r.RolloutContextKind)
		ruleObj.Maybe("rolloutContextKind", r.RolloutContextKind != "").String(string(r.RolloutContextKind))
		ruleObj.End()
	}
	rulesArr.End()

	obj.Maybe("unbounded", segment.Unbounded).Bool(segment.Unbounded)
	obj.Maybe("unboundedContextKind", segment.UnboundedContextKind != "").String(string(segment.UnboundedContextKind))

	obj.Name("version").Int(segment.Version)
	segment.Generation.WriteToJSONWriter(obj.Name("generation"))
	obj.Name("deleted").Bool(segment.Deleted)

	obj.End()
}

func writeSegmentTargets(obj *jwriter.ObjectState, targets []SegmentTarget, name string) {
	targetsArr := obj.Name(name).Array()
	for _, t := range targets {
		targetObj := targetsArr.Object()
		if t.ContextKind != "" {
			targetObj.Name("contextKind").String(string(t.ContextKind))
		}
		writeStringArray(&targetObj, "values", t.Values)
		targetObj.End()
	}
	targetsArr.End()
}

func writeStringArray(obj *jwriter.ObjectState, name string, values []string) {
	arr := obj.Name(name).Array()
	for _, v := range values {
		arr.String(v)
	}
	arr.End()
}

func writeVariationOrRolloutProperties(obj *jwriter.ObjectState, vr VariationOrRollout) {
	obj.Maybe("variation", vr.Variation.IsDefined()).Int(vr.Variation.IntValue())
	if len(vr.Rollout.Variations) > 0 {
		rolloutObj := obj.Name("rollout").Object()
		rolloutObj.Maybe("kind", vr.Rollout.Kind != "").String(string(vr.Rollout.Kind))
		rolloutObj.Maybe("contextKind", vr.Rollout.ContextKind != "").String(string(vr.Rollout.ContextKind))
		variationsArr := rolloutObj.Name("variations").Array()
		for _, wv := range vr.Rollout.Variations {
			variationObj := variationsArr.Object()
			variationObj.Name("variation").Int(wv.Variation)
			variationObj.Name("weight").Int(wv.Weight)
			variationObj.Maybe("untracked", wv.Untracked).Bool(wv.Untracked)
			variationObj.End()
		}
		variationsArr.End()
		rolloutObj.Maybe("seed", vr.Rollout.Seed.IsDefined()).Int(vr.Rollout.Seed.IntValue())
		writeAttrRef(rolloutObj.Maybe("bucketBy", vr.Rollout.BucketBy.IsDefined()),
			&vr.Rollout.BucketBy, vr.Rollout.ContextKind)
		rolloutObj.End()
	}
}

func writeClauses(w *jwriter.Writer, obj *jwriter.ObjectState, clauses []Clause) {
	clausesArr := obj.Name("clauses").Array()
	for _, c := range clauses {
		clauseObj := clausesArr.Object()
		if c.ContextKind != "" {
			clauseObj.Name("contextKind").String(string(c.ContextKind))
		}

		clauseObj.Name("attribute")
		if !c.Attribute.IsDefined() {
			// See comments in unmarshaling logic - for consistency with LD service behavior, we serialize
			// this as an empty string even if it was undefined
			w.String("")
		} else {
			writeAttrRef(w, &c.Attribute, c.ContextKind)
		}

		clauseObj.Name("op").String(string(c.Op))
		valuesArr := clauseObj.Name("values").Array()
		for _, v := range c.Values {
			v.WriteToJSONWriter(w)
		}
		valuesArr.End()
		clauseObj.Name("negate").Bool(c.Negate)
		clauseObj.End()
	}
	clausesArr.End()
}

func writeAttrRef(w *jwriter.Writer, ref *ldattr.Ref, contextKind ldcontext.Kind) {
	if contextKind == "" {
		// If there was no context kind, then we received this as old-style data in which the attribute is
		// interpreted as a plain name rather than an attribute reference. However, ref.String() will always
		// return an attribute reference which could contain escape characters. We should instead get the
		// unescaped attribute name, which AttrRef represents as the first element in a single-element path.
		w.String(ref.Component(0))
	} else {
		w.String(ref.String())
	}
}
