package ldtestdata

import (
	"fmt"
	"sort"

	"github.com/launchdarkly/go-sdk-common/v3/ldcontext"
	"github.com/launchdarkly/go-sdk-common/v3/ldvalue"
	"github.com/launchdarkly/go-server-sdk-evaluation/v2/ldbuilders"
	"github.com/launchdarkly/go-server-sdk-evaluation/v2/ldmodel"

	"golang.org/x/exp/maps"
	"golang.org/x/exp/slices"
)

const (
	trueVariationForBool  = 0
	falseVariationForBool = 1
)

// FlagBuilder is a builder for feature flag configurations to be used with [TestDataSource].
type FlagBuilder struct {
	key                  string
	on                   bool
	offVariation         ldvalue.OptionalInt
	fallthroughVariation ldvalue.OptionalInt
	variations           []ldvalue.Value
	targets              map[ldcontext.Kind]map[int]map[string]bool
	rules                []*RuleBuilder
}

// RuleBuilder is a builder for feature flag rules to be used with [TestDataSource.]
//
// In the LaunchDarkly model, a flag can have any number of rules, and a rule can have any number of
// clauses. A clause is an individual test such as "name is 'X'". A rule matches a user if all of the
// rule's clauses match the user.
//
// To start defining a rule, use one of the flag builder's matching methods such as [RuleBuilder.IfMatch].
// This defines the first clause for the rule. Optionally, you may add more clauses with the rule builder's
// methods such as [RuleBuilder.AndMatch]. Finally, call [RuleBuilder.ThenReturn] or
// [RuleBuilder.ThenReturnIndex] to finish defining the rule.
type RuleBuilder struct {
	owner     *FlagBuilder
	variation int
	clauses   []ldmodel.Clause
}

func newFlagBuilder(key string) *FlagBuilder {
	return &FlagBuilder{
		key: key,
		on:  true,
	}
}

func copyFlagBuilder(from *FlagBuilder) *FlagBuilder {
	f := new(FlagBuilder)
	*f = *from
	f.variations = slices.Clone(from.variations)
	if f.rules != nil {
		f.rules = make([]*RuleBuilder, 0, len(from.rules))
		for _, r := range from.rules {
			f.rules = append(f.rules, copyTestFlagRuleBuilder(r, f))
		}
	}
	if f.targets != nil {
		f.targets = make(map[ldcontext.Kind]map[int]map[string]bool)
		for k, v := range from.targets {
			map1 := make(map[int]map[string]bool)
			for k1, v1 := range v {
				map1[k1] = maps.Clone(v1)
			}
			f.targets[k] = map1
		}
	}
	return f
}

// BooleanFlag is a shortcut for setting the flag to use the standard boolean configuration.
//
// This is the default for all new flags created with [TestDataSource.Flag]. The flag will have two
// variations, true and false (in that order); it will return false whenever targeting is off, and
// true when targeting is on if no other settings specify otherwise.
func (f *FlagBuilder) BooleanFlag() *FlagBuilder {
	if f.isBooleanFlag() {
		return f
	}
	return f.Variations(ldvalue.Bool(true), ldvalue.Bool(false)).
		FallthroughVariationIndex(trueVariationForBool).
		OffVariationIndex(falseVariationForBool)
}

// On sets targeting to be on or off for this flag.
//
// The effect of this depends on the rest of the flag configuration, just as it does on the
// real LaunchDarkly dashboard. In the default configuration that you get from calling
// [TestDataSource.Flag] with a new flag key, the flag will return false whenever targeting is
// off, and true when targeting is on.
func (f *FlagBuilder) On(on bool) *FlagBuilder {
	f.on = on
	return f
}

// FallthroughVariation specifies the fallthrough variation for a boolean flag. The fallthrough is
// the value that is returned if targeting is on and the user was not matched by a more specific
// target or rule.
//
// If the flag was previously configured with other variations, this also changes it to a boolean
// boolean flag.
//
// To specify the variation by variation index instead (such as for a non-boolean flag), use
// [FlagBuilder.FallthroughVariationIndex].
func (f *FlagBuilder) FallthroughVariation(variation bool) *FlagBuilder {
	return f.BooleanFlag().FallthroughVariationIndex(variationForBool(variation))
}

// FallthroughVariationIndex specifies the index of the fallthrough variation. The fallthrough is
// the value that is returned if targeting is on and the user was not matched by a more specific
// target or rule. The index is 0 for the first variation, 1 for the second, etc.
//
// To specify the variation as true or false instead, for a boolean flag, use
// [FlagBuilder.FallthroughVariation].
func (f *FlagBuilder) FallthroughVariationIndex(variationIndex int) *FlagBuilder {
	f.fallthroughVariation = ldvalue.NewOptionalInt(variationIndex)
	return f
}

// OffVariation specifies the off variation for a boolean flag. This is the variation that is
// returned whenever targeting is off.
//
// If the flag was previously configured with other variations, this also changes it to a boolean
// boolean flag.
//
// To specify the variation by variation index instead (such as for a non-boolean flag), use
// [FlagBuilder.OffVariationIndex].
func (f *FlagBuilder) OffVariation(variation bool) *FlagBuilder {
	return f.BooleanFlag().OffVariationIndex(variationForBool(variation))
}

// OffVariationIndex specifies the index of the off variation. This is the variation that is
// returned whenever targeting is off. The index is 0 for the first variation, 1 for the second, etc.
//
// To specify the variation as true or false instead, for a boolean flag, use
// [FlagBuilder.OffVariation].
func (f *FlagBuilder) OffVariationIndex(variationIndex int) *FlagBuilder {
	f.offVariation = ldvalue.NewOptionalInt(variationIndex)
	return f
}

// VariationForAll sets the flag to return the specified boolean variation by default for all contexts.
//
// Targeting is switched on, any existing targets or rules are removed, and the flag's variations are
// set to true and false. The fallthrough variation is set to the specified value. The off variation is
// left unchanged.
//
// To specify the variation by variation index instead (such as for a non-boolean flag), use
// [FlagBuilder.VariationForAllIndex].
func (f *FlagBuilder) VariationForAll(variation bool) *FlagBuilder {
	return f.BooleanFlag().VariationForAllIndex(variationForBool(variation))
}

// VariationForAllIndex sets the flag to always return the specified variation for all contexts.
// The index is 0 for the first variation, 1 for the second, etc.
//
// Targeting is switched on, and any existing targets or rules are removed. The fallthrough variation
// is set to the specified value. The off variation is left unchanged.
//
// To specify the variation as true or false instead, for a boolean flag, use
// [FlagBuilder.VariationForAll].
func (f *FlagBuilder) VariationForAllIndex(variationIndex int) *FlagBuilder {
	return f.On(true).ClearRules().ClearTargets().FallthroughVariationIndex(variationIndex)
}

// ValueForAll sets the flag to always return the specified variation value for all contexts.
//
// The value may be of any JSON type, as defined by [ldvalue.Value]. This method changes the flag to
// only a single variation, which is this value, and to return the same variation regardless of
// whether targeting is on or off. Any existing targets or rules are removed.
func (f *FlagBuilder) ValueForAll(value ldvalue.Value) *FlagBuilder {
	f.variations = []ldvalue.Value{value}
	return f.VariationForAllIndex(0)
}

// VariationForUser sets the flag to return the specified boolean variation for a specific user key
// (that is, for a context with that key whose context kind is "user") when targeting is on.
//
// This has no effect when targeting is turned off for the flag.
//
// If the flag was not already a boolean flag, this also changes it to a boolean flag.
//
// To specify the variation by variation index instead (such as for a non-boolean flag), use
// [FlagBuilder.VariationIndexForUser].
func (f *FlagBuilder) VariationForUser(userKey string, variation bool) *FlagBuilder {
	return f.BooleanFlag().VariationIndexForUser(userKey, variationForBool(variation))
}

// VariationForKey sets the flag to return the specified boolean variation for a specific context,
// identified by context kind and key, when targeting is on.
//
// This has no effect when targeting is turned off for the flag.
//
// If the flag was not already a boolean flag, this also changes it to a boolean flag.
//
// To specify the variation by variation index instead (such as for a non-boolean flag), use
// [FlagBuilder.VariationIndexForKey].
func (f *FlagBuilder) VariationForKey(contextKind ldcontext.Kind, key string, variation bool) *FlagBuilder {
	return f.BooleanFlag().VariationIndexForKey(contextKind, key, variationForBool(variation))
}

// VariationIndexForUser sets the flag to return the specified variation for a specific user key
// (that is, for a context with that key whose context kind is "user") when targeting is on.
// The index is 0 for the first variation, 1 for the second, etc.
//
// This has no effect when targeting is turned off for the flag.
//
// To specify the variation as a true or false value if it is a boolean flag, you can use
// [FlagBuilder.VariationForUser] instead.
func (f *FlagBuilder) VariationIndexForUser(userKey string, variationIndex int) *FlagBuilder {
	return f.VariationIndexForKey(ldcontext.DefaultKind, userKey, variationIndex)
}

// VariationIndexForKey sets the flag to return the specified variation for a specific context,
// identified by context kind and key, when targeting is on. The index is 0 for the first variation,
// 1 for the second, etc.
//
// This has no effect when targeting is turned off for the flag.
//
// To specify the variation as a true or false value if it is a boolean flag, you can use
// [FlagBuilder.VariationForKey] instead.
func (f *FlagBuilder) VariationIndexForKey(contextKind ldcontext.Kind, key string, variationIndex int) *FlagBuilder {
	if f.targets == nil {
		f.targets = make(map[ldcontext.Kind]map[int]map[string]bool)
	}
	if contextKind == "" {
		contextKind = ldcontext.DefaultKind
	}
	keysByVar := f.targets[contextKind]
	if keysByVar == nil {
		keysByVar = make(map[int]map[string]bool)
		f.targets[contextKind] = keysByVar
	}
	for i := range f.variations {
		keys := keysByVar[i]
		if i == variationIndex {
			if keys == nil {
				keys = make(map[string]bool)
				keysByVar[i] = keys
			}
			keys[key] = true
		} else {
			delete(keys, key)
		}
	}
	return f
}

// Variations changes the allowable variation values for the flag.
//
// The values may be of any JSON type, as defined by [ldvalue.Value]. For instance, a boolean flag
// normally has ldvalue.Bool(true), ldvalue.Bool(false); a string-valued flag might have
// ldvalue.String("red"), ldvalue.String("green")}; etc.
func (f *FlagBuilder) Variations(values ...ldvalue.Value) *FlagBuilder {
	f.variations = slices.Clone(values)
	return f
}

// IfMatch starts defining a flag rule, using the "is one of" operator. This is a shortcut for
// calling [FlagBuilder.IfMatchContext] with "user" as the context kind.
//
// The method returns a [RuleBuilder]. Call its [RuleBuilder.ThenReturn] or [RuleBuilder.ThenReturnIndex]
// method to finish the rule, or add more tests with another method like [RuleBuilder.AndMatch].
//
// For example, this creates a rule that returns true if the user name attribute is "Patsy" or "Edina":
//
//	testData.Flag("flag").
//	    IfMatch("name", ldvalue.String("Patsy"), ldvalue.String("Edina")).
//	        ThenReturn(true)
func (f *FlagBuilder) IfMatch(attribute string, values ...ldvalue.Value) *RuleBuilder {
	return newTestFlagRuleBuilder(f).AndMatch(attribute, values...)
}

// IfMatchContext starts defining a flag rule, using the "is one of" operator. This matching
// expression only applies to contexts of a specific kind, identified by the contextKind parameter.
//
// The method returns a [RuleBuilder]. Call its [RuleBuilder.ThenReturn] or [RuleBuilder.ThenReturnIndex]
// method to finish the rule, or add more tests with another method like [RuleBuilder.AndMatch].
//
// For example, this creates a rule that returns true if the name attribute for the "company" context
// is "Ella" or "Monsoon":
//
//	testData.Flag("flag").
//	    IfMatchContext("company", "name", ldvalue.String("Ella"), ldvalue.String("Monsoon")).
//	        ThenReturn(true)
func (f *FlagBuilder) IfMatchContext(
	contextKind ldcontext.Kind,
	attribute string,
	values ...ldvalue.Value,
) *RuleBuilder {
	return newTestFlagRuleBuilder(f).AndMatchContext(contextKind, attribute, values...)
}

// IfNotMatch starts defining a flag rule, using the "is not one of" operator. This is a shortcut for
// calling [FlagBuilder.IfNotMatchContext] with "user" as the context kind.
//
// The method returns a [RuleBuilder]. Call its [RuleBuilder.ThenReturn] or [RuleBuilder.ThenReturnIndex]
// method to finish the rule, or add more tests with another method like [RuleBuilder.AndMatch].
//
// For example, this creates a rule that returns true if the user name attribute is neither "Saffron"
// nor "Bubble":
//
//	testData.Flag("flag").
//	    IfNotMatch("name", ldvalue.String("Saffron"), ldvalue.String("Bubble")).
//	    ThenReturn(true)
func (f *FlagBuilder) IfNotMatch(attribute string, values ...ldvalue.Value) *RuleBuilder {
	return newTestFlagRuleBuilder(f).AndNotMatch(attribute, values...)
}

// IfNotMatchContext starts defining a flag rule, using the "is not one of" operator. This matching
// expression only applies to contexts of a specific kind, identified by the contextKind parameter.
//
// The method returns a [RuleBuilder]. Call its [RuleBuilder.ThenReturn] or [RuleBuilder.ThenReturnIndex]
// method to finish the rule, or add more tests with another method like [RuleBuilder.AndMatch].
//
// For example, this creates a rule that returns true if the name attribute for the "company" context
// is neither "Pendant" nor "Sterling Cooper":
//
//	testData.Flag("flag").
//	    IfNotMatch("company", "name", ldvalue.String("Pendant"), ldvalue.String("Sterling Cooper")).
//	    ThenReturn(true)
func (f *FlagBuilder) IfNotMatchContext(
	contextKind ldcontext.Kind,
	attribute string,
	values ...ldvalue.Value,
) *RuleBuilder {
	return newTestFlagRuleBuilder(f).AndNotMatchContext(contextKind, attribute, values...)
}

// ClearRules removes any existing rules from the flag. This undoes the effect of methods like
// [FlagBuilder.IfMatch].
func (f *FlagBuilder) ClearRules() *FlagBuilder {
	f.rules = nil
	return f
}

// ClearTargets removes any existing user targets from the flag. This undoes the effect of methods
// like [FlagBuilder.VariationForUser].
func (f *FlagBuilder) ClearTargets() *FlagBuilder {
	f.targets = nil
	return f
}

func (f *FlagBuilder) isBooleanFlag() bool {
	return len(f.variations) == 2 &&
		f.variations[trueVariationForBool].Equal(ldvalue.Bool(true)) &&
		f.variations[falseVariationForBool].Equal(ldvalue.Bool(false))
}

func (f *FlagBuilder) createFlag(version int) ldmodel.FeatureFlag {
	fb := ldbuilders.NewFlagBuilder(f.key).
		Version(version).
		On(f.on).
		Variations(f.variations...)
	if f.offVariation.IsDefined() {
		fb.OffVariation(f.offVariation.IntValue())
	}
	if f.fallthroughVariation.IsDefined() {
		fb.FallthroughVariation(f.fallthroughVariation.IntValue())
	}

	// Iterate through any context kinds that there are targets for. A quirk of the data model, for
	// backward-compatibility reasons, is that each entry in the old-style targets list (for users)
	// must be matched by a placeholder entry in ContextTargets.
	// Also, for the sake of test determinaciy, we sort the context kinds and the context keys.
	targetKinds := make([]ldcontext.Kind, 0, len(f.targets))
	for kind := range f.targets {
		targetKinds = append(targetKinds, kind)
	}
	slices.Sort(targetKinds)
	for _, kind := range targetKinds {
		keysByVar := f.targets[kind]
		for varIndex := range f.variations {
			if keysMap, ok := keysByVar[varIndex]; ok {
				keys := make([]string, 0, len(keysMap))
				for key := range keysMap {
					keys = append(keys, key)
				}
				sort.Strings(keys)
				if kind == ldcontext.DefaultKind {
					fb.AddTarget(varIndex, keys...)
					// A quirk of the data model, for backward-compatibility reasons, is that each entry in the
					// old-style targets list (for users) must be matched by a placeholder entry in ContextTargets.
					fb.AddContextTarget(ldcontext.DefaultKind, varIndex)
				} else {
					fb.AddContextTarget(kind, varIndex, keys...)
				}
			}
		}
	}
	for i, r := range f.rules {
		fb.AddRule(ldbuilders.NewRuleBuilder().
			ID(fmt.Sprintf("rule%d", i)).
			Variation(r.variation).
			Clauses(r.clauses...),
		)
	}
	return fb.Build()
}

func newTestFlagRuleBuilder(owner *FlagBuilder) *RuleBuilder {
	return &RuleBuilder{owner: owner}
}

func copyTestFlagRuleBuilder(from *RuleBuilder, owner *FlagBuilder) *RuleBuilder {
	r := RuleBuilder{owner: owner, variation: from.variation}
	r.clauses = slices.Clone(from.clauses)
	return &r
}

// AndMatch adds another clause, using the "is one of" operator. This is a shortcut for calling
// [RuleBuilder.AndMatchContext] with "user" as the context kind.
//
// For example, this creates a rule that returns true if the user name attribute is "Patsy" and the
// country is "gb":
//
//	testData.Flag("flag").
//	    IfMatch("name", ldvalue.String("Patsy")).
//	        AndMatch("country", ldvalue.String("gb")).
//	        ThenReturn(true)
func (r *RuleBuilder) AndMatch(attribute string, values ...ldvalue.Value) *RuleBuilder {
	return r.AndMatchContext(ldcontext.DefaultKind, attribute, values...)
}

// AndMatchContext adds another clause, using the "is one of" operator. This matching expression
// only applies to contexts of a specific kind, identified by the contextKind parameter.
//
// For example, this creates a rule that returns true if the name attribute for the "company" context
// is "Ella" and the country is "gb":
//
//	testData.Flag("flag").
//	    IfMatchContext("company", "name", ldvalue.String("Ella")).
//	        AndMatchContext("company", "country", ldvalue.String("gb")).
//	        ThenReturn(true)
func (r *RuleBuilder) AndMatchContext(
	contextKind ldcontext.Kind,
	attribute string,
	values ...ldvalue.Value,
) *RuleBuilder {
	r.clauses = append(r.clauses, ldbuilders.ClauseWithKind(contextKind, attribute, ldmodel.OperatorIn, values...))
	return r
}

// AndNotMatch adds another clause, using the "is not one of" operator. This is a shortcut for calling
// [RuleBuilder.AndNotMatchContext] with "user" as the context kind.
//
// For example, this creates a rule that returns true if the user name attribute is "Patsy" and the
// country is not "gb":
//
//	testData.Flag("flag").
//	    IfMatch("name", ldvalue.String("Patsy")).
//	        AndNotMatch("country", ldvalue.String("gb")).
//	        ThenReturn(true)
func (r *RuleBuilder) AndNotMatch(attribute string, values ...ldvalue.Value) *RuleBuilder {
	return r.AndNotMatchContext(ldcontext.DefaultKind, attribute, values...)
}

// AndNotMatchContext adds another clause, using the "is not one of" operator. This matching expression
// only applies to contexts of a specific kind, identified by the contextKind parameter.
//
// For example, this creates a rule that returns true if the name attribute for the "company" context
// is "Ella" and the country is not "gb":
//
//	testData.Flag("flag").
//	    IfMatchContext("company", "name", ldvalue.String("Ella")).
//	        AndNotMatchContext("company", "country", ldvalue.String("gb")).
//	        ThenReturn(true)
func (r *RuleBuilder) AndNotMatchContext(
	contextKind ldcontext.Kind,
	attribute string,
	values ...ldvalue.Value,
) *RuleBuilder {
	r.clauses = append(r.clauses, ldbuilders.Negate(ldbuilders.ClauseWithKind(contextKind,
		attribute, ldmodel.OperatorIn, values...)))
	return r
}

// ThenReturn finishes defining the rule, specifying the result value as a boolean.
func (r *RuleBuilder) ThenReturn(variation bool) *FlagBuilder {
	r.owner.BooleanFlag()
	return r.ThenReturnIndex(variationForBool(variation))
}

// ThenReturnIndex finishes defining the rule, specifying the result as a variation index. The index
// is 0 for the first variation, 1 for the second, etc.
func (r *RuleBuilder) ThenReturnIndex(variation int) *FlagBuilder {
	r.variation = variation
	r.owner.rules = append(r.owner.rules, r)
	return r.owner
}

func variationForBool(value bool) int {
	if value {
		return trueVariationForBool
	}
	return falseVariationForBool
}
