package ldmodel

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

// FeatureFlag describes an individual feature flag.
//
// The fields of this struct are exported for use by LaunchDarkly internal components. Application code
// should normally not reference FeatureFlag fields directly; flag data normally comes from LaunchDarkly
// SDK endpoints in JSON form and can be deserialized using the DataModelSerialization interface.
type FeatureFlag struct {
	// Key is the unique string key of the feature flag.
	Key string
	// On is true if targeting is turned on for this flag.
	//
	// If On is false, the evaluator always uses OffVariation and ignores all other fields.
	On bool
	// Prerequisites is a list of feature flag conditions that are prerequisites for this flag.
	//
	// If any prerequisite is not met, the flag behaves as if targeting is turned off.
	Prerequisites []Prerequisite
	// Targets contains sets of individually targeted users for the default context kind (user).
	//
	// Targets take precedence over Rules: if a user is matched by any Target, the Rules are ignored.
	// Targets are ignored if targeting is turned off.
	Targets []Target
	// ContextTargets contains sets of individually targeted users for specific context kinds.
	//
	// Targets take precedence over Rules: if a user is matched by any Target, the Rules are ignored.
	// Targets are ignored if targeting is turned off.
	ContextTargets []Target
	// Rules is a list of rules that may match a user.
	//
	// If a user is matched by a Rule, all subsequent Rules in the list are skipped. Rules are ignored
	// if targeting is turned off.
	Rules []FlagRule
	// Fallthrough defines the flag's behavior if targeting is turned on but the user is not matched
	// by any Target or Rule.
	Fallthrough VariationOrRollout
	// OffVariation specifies the variation index to use if targeting is turned off.
	//
	// If this is undefined (ldvalue.OptionalInt{}), Evaluate returns undefined for the variation
	// index and ldvalue.Null() for the value.
	OffVariation ldvalue.OptionalInt
	// Variations is the list of all allowable variations for this flag. The variation index in a
	// Target or Rule is a zero-based index to this list.
	Variations []ldvalue.Value
	// ClientSideAvailability indicates whether a flag is available using each of the client-side
	// authentication methods.
	ClientSideAvailability ClientSideAvailability
	// Salt is a randomized value assigned to this flag when it is created.
	//
	// The hash function used for calculating percentage rollouts uses this as a salt to ensure that
	// rollouts are consistent within each flag but not predictable from one flag to another.
	Salt string
	// TrackEvents is used internally by the SDK analytics event system.
	//
	// This field is true if the current LaunchDarkly account has data export enabled, and has turned on
	// the "send detailed event information for this flag" option for this flag. This tells the SDK to
	// send full event data for each flag evaluation, rather than only aggregate data in a summary event.
	//
	// The go-server-sdk-evaluation package does not implement that behavior; it is only in the data
	// model for use by the SDK.
	TrackEvents bool
	// TrackEventsFallthrough is used internally by the SDK analytics event system.
	//
	// This field is true if the current LaunchDarkly account has experimentation enabled, has associated
	// this flag with an experiment, and has enabled "default rule" for the experiment. This tells the
	// SDK to send full event data for any evaluation where this flag had targeting turned on but the
	// user did not match any targets or rules.
	//
	// The go-server-sdk-evaluation package does not implement that behavior; it is only in the data
	// model for use by the SDK.
	TrackEventsFallthrough bool
	// DebugEventsUntilDate is used internally by the SDK analytics event system.
	//
	// This field is non-zero if debugging for this flag has been turned on temporarily in the
	// LaunchDarkly dashboard. Debugging always is for a limited time, so the field specifies a Unix
	// millisecond timestamp when this mode should expire. Until then, the SDK will send full event data
	// for each evaluation of this flag.
	//
	// The go-server-sdk-evaluation package does not implement that behavior; it is only in the data
	// model for use by the SDK.
	DebugEventsUntilDate ldtime.UnixMillisecondTime
	// Version is an integer that is incremented by LaunchDarkly every time the configuration of the flag is
	// changed.
	Version int
	// Deleted is true if this is not actually a feature flag but rather a placeholder (tombstone) for a
	// deleted flag. This is only relevant in data store implementations. The SDK does not evaluate
	// deleted flags.
	Deleted bool
}

// FlagRule describes a single rule within a feature flag.
//
// A rule consists of a set of ANDed matching conditions (Clause) for a user, along with either a fixed
// variation or a set of rollout percentages to use if the user matches all of the clauses.
type FlagRule struct {
	// VariationRollout properties for a FlagRule define what variation to return if the user matches
	// this rule.
	VariationOrRollout
	// ID is a randomized identifier assigned to each rule when it is created.
	//
	// This is used to populate the RuleID property of ldreason.EvaluationReason.
	ID string
	// Clauses is a list of test conditions that make up the rule. These are ANDed: every Clause must
	// match in order for the FlagRule to match.
	Clauses []Clause
	// TrackEvents is used internally by the SDK analytics event system.
	//
	// This field is true if the current LaunchDarkly account has experimentation enabled, has associated
	// this flag with an experiment, and has enabled this rule for the experiment. This tells the SDK to
	// send full event data for any evaluation that matches this rule.
	//
	// The go-server-sdk-evaluation package does not implement that behavior; it is only in the data
	// model for use by the SDK.
	TrackEvents bool
}

// RolloutKind describes whether a rollout is a simple percentage rollout or represents an experiment. Experiments have
// different behaviour for tracking and variation bucketing.
type RolloutKind string

const (
	// RolloutKindRollout represents a simple percentage rollout. This is the default rollout kind, and will be assumed if
	// not otherwise specified.
	RolloutKindRollout RolloutKind = "rollout"
	// RolloutKindExperiment represents an experiment. Experiments have different behaviour for tracking and variation
	// bucketing.
	RolloutKindExperiment RolloutKind = "experiment"
)

// VariationOrRollout desscribes either a fixed variation or a percentage rollout.
//
// There is a VariationOrRollout for every FlagRule, and also one in FeatureFlag.Fallthrough which is
// used if no rules match.
//
// Invariant: one of the variation or rollout must be non-nil.
type VariationOrRollout struct {
	// Variation specifies the index of the variation to return. It is undefined (ldvalue.OptionalInt{})
	// if no specific variation is defined.
	Variation ldvalue.OptionalInt
	// Rollout specifies a percentage rollout to be used instead of a specific variation. A rollout is
	// only defined if it has a non-empty Variations list.
	Rollout Rollout
}

// Rollout describes how users will be bucketed into variations during a percentage rollout.
type Rollout struct {
	// Kind specifies whether this rollout is a simple percentage rollout or represents an experiment. Experiments have
	// different behaviour for tracking and variation bucketing.
	Kind RolloutKind
	// ContextKind is the context kind that this rollout will use to get any necessary context attributes.
	//
	// LaunchDarkly will normally always set this property, but if it is empty/omitted, it should be
	// treated as ldcontext.DefaultKind. An empty string value here represents the property being unset
	// (so it will be omitted in serialization).
	ContextKind ldcontext.Kind
	// Variations is a list of the variations in the percentage rollout and what percentage of users
	// to include in each.
	//
	// The Weight values of all elements in this list should add up to 100000 (100%). If they do not,
	// the last element in the list will behave as if it includes any leftover percentage (that is, if
	// the weights are [1000, 1000, 1000] they will be treated as if they were [1000, 1000, 98000]).
	Variations []WeightedVariation
	// BucketBy specifies which user attribute should be used to distinguish between users in a rollout.
	// This only works for simple rollouts; it is ignored for experiments.
	//
	// The default (when BucketBy is empty) is ldattr.KeyAttr, the user's primary key. If you wish to
	// treat users with different keys as the same for rollout purposes as long as they have the same
	// "country" attribute, you would set this to "country".
	//
	// Simple rollouts always take the user's "secondary key" attribute into account as well if the user
	// has one. Experiments ignore the secondary key.
	BucketBy ldattr.Ref
	// Seed, if present, specifies the seed for the hashing algorithm this rollout will use to bucket users, so that
	// rollouts with the same Seed will assign the same users to the same buckets.
	// If unspecified, the seed will default to a combination of the flag key and flag-level Salt.
	Seed ldvalue.OptionalInt
}

// IsExperiment returns whether this rollout represents an experiment.
func (r Rollout) IsExperiment() bool {
	return r.Kind == RolloutKindExperiment
}

// Clause describes an individual clause within a FlagRule or SegmentRule.
type Clause struct {
	// ContextKind is the context kind that this clause applies to.
	//
	// LaunchDarkly will normally always set this property, but if it is empty/omitted, it should be
	// treated as ldcontext.DefaultKind. An empty string value here represents the property being unset (so
	// it will be omitted in serialization).
	//
	// If the value of Attribute is "kind", then ContextKind is ignored because the nature of the context kind
	// test is described in a richer way by Operator and Values.
	ContextKind ldcontext.Kind
	// Attribute specifies the context attribute that is being tested.
	//
	// This is required for all Operator types except SegmentMatch. If Op is SegmentMatch then Attribute
	// is ignored (and will normally be an empty ldattr.Ref{}).
	//
	// If the context's value for this attribute is a JSON array, then the test specified in the Clause is
	// repeated for each value in the array until a match is found or there are no more values.
	Attribute ldattr.Ref
	// Op specifies the type of test to perform.
	Op Operator
	// Values is a list of values to be compared to the user attribute.
	//
	// This is interpreted as an OR: if the user attribute matches any of these values with the specified
	// operator, the Clause matches the user.
	//
	// In the special case where Op is OperatorSegmentMtach, there should only be a single Value, which
	// must be a string: the key of the user segment.
	//
	// If the user does not have a value for the specified attribute, the Values are ignored and the
	// Clause is always treated as a non-match.
	Values []ldvalue.Value
	// Negate is true if the specified Operator should be inverted.
	//
	// For instance, this would cause OperatorIn to mean "not equal" rather than "equal". Note that if no
	// tests are performed for this Clause because the user does not have a value for the specified
	// attribute, then Negate will not come into effect (the Clause will just be treated as a non-match).
	Negate bool
	// preprocessed is created by PreprocessFlag() to speed up clause evaluation in scenarios like
	// regex matching.
	preprocessed clausePreprocessedData
}

// WeightedVariation describes a fraction of users who will receive a specific variation.
type WeightedVariation struct {
	// Variation is the index of the variation to be returned if the user is in this bucket. This is
	// always a real variation index; it cannot be undefined.
	Variation int
	// Weight is the proportion of users who should go into this bucket, as an integer from 0 to 100000.
	Weight int
	// Untracked means that users allocated to this variation should not have tracking events sent.
	Untracked bool
}

// Target describes a set of users who will receive a specific variation.
type Target struct {
	// ContextKind is the context kind that this target list applies to.
	//
	// LaunchDarkly will normally always set this property, but if it is empty/omitted, it should be
	// treated as ldcontext.DefaultKind. An empty string value here represents the property being unset (so
	// it will be omitted in serialization).
	ContextKind ldcontext.Kind
	// Values is the set of user keys included in this Target.
	Values []string
	// Variation is the index of the variation to be returned if the user matches one of these keys. This
	// is always a real variation index; it cannot be undefined.
	Variation int
	// preprocessed is created by PreprocessFlag() to speed up target matching.
	preprocessed targetPreprocessedData
}

// Prerequisite describes a requirement that another feature flag return a specific variation.
//
// A prerequisite condition is met if the specified prerequisite flag has targeting turned on and
// returns the specified variation.
type Prerequisite struct {
	// Key is the unique key of the feature flag to be evaluated as a prerequisite.
	Key string
	// Variation is the index of the variation that the prerequisite flag must return in order for
	// the prerequisite condition to be met. If the prerequisite flag has targeting turned on, then
	// the condition is not met even if the flag's OffVariation matches this value. This is always a
	// real variation index; it cannot be undefined.
	Variation int
}

// ClientSideAvailability describes whether a flag is available to client-side SDKs.
//
// This field can be used by a server-side client to determine whether to include an individual flag in
// bootstrapped set of flag data (see https://docs.launchdarkly.com/sdk/client-side/javascript#bootstrapping).
type ClientSideAvailability struct {
	// UsingMobileKey indicates that this flag is available to clients using the mobile key for authorization
	// (includes most desktop and mobile clients).
	UsingMobileKey bool
	// UsingEnvironmentID indicates that this flag is available to clients using the environment id to identify an
	// environment (includes client-side javascript clients).
	UsingEnvironmentID bool
	// Explicit is true if, when serializing this flag, all of the ClientSideAvailability properties should
	// be included. If it is false, then an older schema is used in which this object is entirely omitted,
	// UsingEnvironmentID is stored in a deprecated property, and UsingMobileKey is assumed to be true.
	//
	// This field exists to ensure that flag representations remain consistent when sent and received
	// even though the clientSideAvailability property may not be present in the JSON data. It is false
	// if the flag was deserialized from an older JSON schema that did not include that property.
	//
	// Similarly, when deserializing a flag, if it used the older schema then Explicit will be false and
	// UsingMobileKey will be true.
	Explicit bool
}
