package evaluation

import (
	"github.com/launchdarkly/go-server-sdk-evaluation/v2/ldmodel"

	"github.com/launchdarkly/go-sdk-common/v3/ldcontext"
	"github.com/launchdarkly/go-sdk-common/v3/ldlog"
	"github.com/launchdarkly/go-sdk-common/v3/ldreason"
	"github.com/launchdarkly/go-sdk-common/v3/ldvalue"
)

// Notes on some implementation details in this file:
//
// - We are often passing structs by address rather than by value, even if the usual reasons for using
// a pointer (allowing mutation of the value, or using nil to represent "no value") do not apply. This
// is an optimization to avoid the small but nonzero overhead of copying a struct by value across many
// nested function/method calls; passing a pointer instead is faster. It is safe for us to do this
// as long as the pointer value is not being retained outside the scope of this call.
//
// - In some for loops, we are deliberately taking the address of the range variable and using a
// "//nolint:gosec" directive to turn off the usual linter warning about this:
//       for _, x := range someThings {
//           doSomething(&x) //nolint:gosec
//       }
// The rationale is the same as above, and is safe as long as the same conditions apply.

// Result encapsulates all information returned by Evaluator.Evaluate.
type Result struct {
	// Detail contains the evaluation detail fields.
	Detail ldreason.EvaluationDetail

	// IsExperiment is true if this evaluation result was determined by an experiment. Normally if
	// this is true, then Detail.Reason will also communicate that fact, but there are some cases
	// related to the older experimentation model where this field may be true even if Detail.Reason
	// does not say anything special. When the SDK submits evaluation information to the event
	// processor, it should set the RequireReason field in ldevents.FlagEventProperties to this value.
	IsExperiment bool
}

type evaluator struct {
	dataProvider       DataProvider
	bigSegmentProvider BigSegmentProvider
	errorLogger        ldlog.BaseLogger
	enableSecondaryKey bool
}

const ( // See Evaluate() regarding the use of these constants
	preallocatedPrerequisiteChainSize = 20
	preallocatedSegmentChainSize      = 20
)

// NewEvaluator creates an Evaluator, specifying a DataProvider that it will use if it needs to
// query additional feature flags or user segments during an evaluation.
//
// To support big segments, you must use NewEvaluatorWithOptions and EvaluatorOptionBigSegmentProvider.
func NewEvaluator(dataProvider DataProvider) Evaluator {
	return NewEvaluatorWithOptions(dataProvider)
}

// NewEvaluatorWithOptions creates an Evaluator, specifying a DataProvider that it will use if it
// needs to query additional feature flags or user segments during an evaluation, and also
// any number of EvaluatorOption modifiers.
func NewEvaluatorWithOptions(dataProvider DataProvider, options ...EvaluatorOption) Evaluator {
	e := &evaluator{
		dataProvider: dataProvider,
	}
	for _, o := range options {
		if o != nil {
			o.apply(e)
		}
	}
	return e
}

// Used internally to hold the parameters of an evaluation, to avoid repetitive parameter passing.
// Its methods use a pointer receiver for efficiency, even though it is allocated on the stack and
// its fields are never modified.
type evaluationScope struct {
	owner                         *evaluator
	flag                          *ldmodel.FeatureFlag
	context                       ldcontext.Context
	prerequisiteFlagEventRecorder PrerequisiteFlagEventRecorder
	// These bigSegments properties start out unset. They are computed lazily if we encounter
	// big segment references during an evaluation. See evaluator_segment.go.
	bigSegmentsMemberships map[string]BigSegmentMembership
	bigSegmentsStatus      ldreason.BigSegmentsStatus
}

type evaluationStack struct {
	prerequisiteFlagChain []string
	segmentChain          []string
}

// Implementation of the Evaluator interface.
func (e *evaluator) Evaluate(
	flag *ldmodel.FeatureFlag,
	context ldcontext.Context,
	prerequisiteFlagEventRecorder PrerequisiteFlagEventRecorder,
) Result {
	if context.Err() != nil {
		return Result{Detail: ldreason.NewEvaluationDetailForError(ldreason.EvalErrorUserNotSpecified, ldvalue.Null())}
	}

	es := evaluationScope{
		owner:                         e,
		flag:                          flag,
		context:                       context,
		prerequisiteFlagEventRecorder: prerequisiteFlagEventRecorder,
	}

	// Preallocate some space for prerequisiteFlagChain and segmentChain on the stack. We can
	// get up to that many levels of nested prerequisites or nested segments before appending
	// to the slice will cause a heap allocation.
	stack := evaluationStack{
		prerequisiteFlagChain: make([]string, 0, preallocatedPrerequisiteChainSize),
		segmentChain:          make([]string, 0, preallocatedSegmentChainSize),
	}

	detail, _ := es.evaluate(stack)
	if es.bigSegmentsStatus != "" {
		detail.Reason = ldreason.NewEvalReasonFromReasonWithBigSegmentsStatus(detail.Reason,
			es.bigSegmentsStatus)
	}
	return Result{Detail: detail, IsExperiment: isExperiment(flag, detail.Reason)}
}

// Entry point for evaluating a flag which could be either the original flag or a prerequisite.
// The second return value is normally true. If it is false, it means we should immediately
// terminate the whole current stack of evaluations and not do any more checking or recursing.
//
// Note that the evaluationStack is passed by value-- unlike other structs such as the FeatureFlag
// which we reference by address for the sake of efficiency (see comments at top of file). One
// reason for this is described in the comments at each point where we modify one of its fields
// with append(). The other is that Go's escape analysis is not quite clever enough to let the
// slices that we preallocated in Evaluate() remain on the stack if we pass that struct by address.
func (es *evaluationScope) evaluate(stack evaluationStack) (ldreason.EvaluationDetail, bool) {
	if !es.flag.On {
		return es.getOffValue(ldreason.NewEvalReasonOff()), true
	}

	prereqErrorReason, ok := es.checkPrerequisites(stack)
	if !ok {
		// Is this an actual error, like a malformed flag? Then return an error with default value.
		if prereqErrorReason.GetKind() == ldreason.EvalReasonError {
			return ldreason.NewEvaluationDetailForError(prereqErrorReason.GetErrorKind(), ldvalue.Null()), false
		}
		// No, it's presumably just "prerequisite failed", which gets the off value.
		return es.getOffValue(prereqErrorReason), true
	}

	// Check to see if targets match
	if variation := es.anyTargetMatchVariation(); variation.IsDefined() {
		return es.getVariation(variation.IntValue(), ldreason.NewEvalReasonTargetMatch()), true
	}

	// Now walk through the rules and see if any match
	for ruleIndex, rule := range es.flag.Rules {
		match, err := es.ruleMatchesContext(&rule, stack) //nolint:gosec // see comments at top of file
		if err != nil {
			es.logEvaluationError(err)
			return ldreason.NewEvaluationDetailForError(errorKindForError(err), ldvalue.Null()), false
		}
		if match {
			reason := ldreason.NewEvalReasonRuleMatch(ruleIndex, rule.ID)
			return es.getValueForVariationOrRollout(rule.VariationOrRollout, reason), true
		}
	}

	return es.getValueForVariationOrRollout(es.flag.Fallthrough, ldreason.NewEvalReasonFallthrough()), true
}

// Do a nested evaluation for a prerequisite of the current scope's flag. The second return value is
// normally true; it is false only in the case where we've detected a circular reference, in which
// case we want the entire evaluation to fail with a MalformedFlag error.
func (es *evaluationScope) evaluatePrerequisite(
	prereqFlag *ldmodel.FeatureFlag,
	stack evaluationStack,
) (ldreason.EvaluationDetail, bool) {
	for _, p := range stack.prerequisiteFlagChain {
		if prereqFlag.Key == p {
			err := circularPrereqReferenceError(prereqFlag.Key)
			es.logEvaluationError(err)
			return ldreason.EvaluationDetail{}, false
		}
	}
	subScope := *es
	subScope.flag = prereqFlag
	result, ok := subScope.evaluate(stack)
	es.bigSegmentsStatus = computeUpdatedBigSegmentsStatus(es.bigSegmentsStatus, subScope.bigSegmentsStatus)
	return result, ok
}

// Returns an empty reason if all prerequisites are OK, otherwise constructs an error reason that describes the failure
func (es *evaluationScope) checkPrerequisites(stack evaluationStack) (ldreason.EvaluationReason, bool) {
	if len(es.flag.Prerequisites) == 0 {
		return ldreason.EvaluationReason{}, true
	}

	stack.prerequisiteFlagChain = append(stack.prerequisiteFlagChain, es.flag.Key)
	// Note that the change to stack.prerequisiteFlagChain does not persist after returning from
	// this method. That means we don't ever need to explicitly remove the last item-- but, it
	// introduces a potential edge-case inefficiency with deeply nested prerequisites: if the
	// original slice had a capacity of 20, and then the 20th prerequisite has 5 prerequisites of
	// its own, when checkPrerequisites is called for each of those it will end up hitting the
	// capacity of the slice each time and allocating a new backing array each time. The way
	// around that would be to pass a *pointer* to the slice, so the backing array would be
	// retained. However, doing so appears to defeat Go's escape analysis and cause heap escaping
	// of the slice every time, which would be worse in more typical use cases. We do not expect
	// the preallocated capacity to be reached in typical usage.

	for _, prereq := range es.flag.Prerequisites {
		prereqFeatureFlag := es.owner.dataProvider.GetFeatureFlag(prereq.Key)
		if prereqFeatureFlag == nil {
			return ldreason.NewEvalReasonPrerequisiteFailed(prereq.Key), false
		}
		prereqOK := true

		prereqResultDetail, prereqValid := es.evaluatePrerequisite(prereqFeatureFlag, stack)
		if !prereqValid {
			// In this case we want to immediately exit with an error and not check any more prereqs
			return ldreason.NewEvalReasonError(ldreason.EvalErrorMalformedFlag), false
		}
		if !prereqFeatureFlag.On || prereqResultDetail.IsDefaultValue() ||
			prereqResultDetail.VariationIndex.IntValue() != prereq.Variation {
			// Note that if the prerequisite flag is off, we don't consider it a match no matter what its
			// off variation was. But we still need to evaluate it in order to generate an event.
			prereqOK = false
		}

		if es.prerequisiteFlagEventRecorder != nil {
			event := PrerequisiteFlagEvent{es.flag.Key, es.context, prereqFeatureFlag, Result{
				Detail:       prereqResultDetail,
				IsExperiment: isExperiment(prereqFeatureFlag, prereqResultDetail.Reason),
			}}
			es.prerequisiteFlagEventRecorder(event)
		}

		if !prereqOK {
			return ldreason.NewEvalReasonPrerequisiteFailed(prereq.Key), false
		}
	}
	return ldreason.EvaluationReason{}, true
}

func (es *evaluationScope) getVariation(index int, reason ldreason.EvaluationReason) ldreason.EvaluationDetail {
	if index < 0 || index >= len(es.flag.Variations) {
		err := badVariationError(index)
		es.logEvaluationError(err)
		return ldreason.NewEvaluationDetailForError(err.errorKind(), ldvalue.Null())
	}
	return ldreason.NewEvaluationDetail(es.flag.Variations[index], index, reason)
}

func (es *evaluationScope) getOffValue(reason ldreason.EvaluationReason) ldreason.EvaluationDetail {
	if !es.flag.OffVariation.IsDefined() {
		return ldreason.EvaluationDetail{Reason: reason}
	}
	return es.getVariation(es.flag.OffVariation.IntValue(), reason)
}

func (es *evaluationScope) getValueForVariationOrRollout(
	vr ldmodel.VariationOrRollout,
	reason ldreason.EvaluationReason,
) ldreason.EvaluationDetail {
	index, inExperiment, err := es.variationOrRolloutResult(vr, es.flag.Key, es.flag.Salt)
	if err != nil {
		es.logEvaluationError(err)
		return ldreason.NewEvaluationDetailForError(errorKindForError(err), ldvalue.Null())
	}
	if inExperiment {
		reason = reasonToExperimentReason(reason)
	}
	return es.getVariation(index, reason)
}

func (es *evaluationScope) anyTargetMatchVariation() ldvalue.OptionalInt {
	if len(es.flag.ContextTargets) == 0 {
		// If ContextTargets is empty but Targets is not empty, then this is flag data that originally
		// came from a non-context-aware LD endpoint or SDK. In that case, just look at Targets.
		for _, t := range es.flag.Targets {
			if variation := es.targetMatchVariation(&t); variation.IsDefined() { //nolint:gosec // see comments at top of file
				return variation
			}
		}
	} else {
		// If ContextTargets is provided, we iterate through it-- but, for any target of the default
		// kind (user), if there are no Values, we check for a corresponding target in Targets.
		for _, t := range es.flag.ContextTargets {
			var variation ldvalue.OptionalInt
			if (t.ContextKind == "" || t.ContextKind == ldcontext.DefaultKind) && len(t.Values) == 0 {
				for _, t1 := range es.flag.Targets {
					if t1.Variation == t.Variation {
						variation = es.targetMatchVariation(&t1) //nolint:gosec // see comments at top of file
						break
					}
				}
			} else {
				variation = es.targetMatchVariation(&t) //nolint:gosec // see comments at top of file
			}
			if variation.IsDefined() {
				return variation
			}
		}
	}
	return ldvalue.OptionalInt{}
}

func (es *evaluationScope) targetMatchVariation(t *ldmodel.Target) ldvalue.OptionalInt {
	if context := es.context.IndividualContextByKind(t.ContextKind); context.IsDefined() {
		if ldmodel.EvaluatorAccessors.TargetFindKey(t, context.Key()) {
			return ldvalue.NewOptionalInt(t.Variation)
		}
	}
	return ldvalue.OptionalInt{}
}

func (es *evaluationScope) ruleMatchesContext(rule *ldmodel.FlagRule, stack evaluationStack) (bool, error) {
	// Note that rule is passed by reference only for efficiency; we do not modify it
	for _, clause := range rule.Clauses {
		match, err := es.clauseMatchesContext(&clause, stack) //nolint:gosec // see comments at top of file
		if !match || err != nil {
			return match, err
		}
	}
	return true, nil
}

func (es *evaluationScope) variationOrRolloutResult(
	r ldmodel.VariationOrRollout, key, salt string) (variationIndex int, inExperiment bool, err error) {
	if r.Variation.IsDefined() {
		return r.Variation.IntValue(), false, nil
	}
	if len(r.Rollout.Variations) == 0 {
		// This is an error (malformed flag); either Variation or Rollout must be non-nil.
		return -1, false, emptyRolloutError{}
	}

	isExperiment := r.Rollout.IsExperiment()

	bucketVal, problem, err := es.computeBucketValue(isExperiment, r.Rollout.Seed, r.Rollout.ContextKind,
		key, r.Rollout.BucketBy, salt)
	if err != nil {
		return -1, false, err
	}
	var sum float32

	for _, bucket := range r.Rollout.Variations {
		sum += float32(bucket.Weight) / 100000.0
		if bucketVal < sum {
			resultInExperiment := isExperiment && !bucket.Untracked &&
				problem != bucketingFailureContextLacksDesiredKind
			return bucket.Variation, resultInExperiment, nil
		}
	}

	// The user's bucket value was greater than or equal to the end of the last bucket. This could happen due
	// to a rounding error, or due to the fact that we are scaling to 100000 rather than 99999, or the flag
	// data could contain buckets that don't actually add up to 100000. Rather than returning an error in
	// this case (or changing the scaling, which would potentially change the results for *all* users), we
	// will simply put the user in the last bucket.
	lastBucket := r.Rollout.Variations[len(r.Rollout.Variations)-1]
	return lastBucket.Variation, isExperiment && !lastBucket.Untracked, nil
}

func (es *evaluationScope) logEvaluationError(err error) {
	if err == nil || es.owner.errorLogger == nil {
		return
	}
	es.owner.errorLogger.Printf("Invalid flag configuration detected in flag %q: %s",
		es.flag.Key,
		err,
	)
}

func getApplicableContextKeyByKind(baseContext *ldcontext.Context, kind ldcontext.Kind) (string, bool) {
	if mc := baseContext.IndividualContextByKind(kind); mc.IsDefined() {
		return mc.Key(), true
	}
	return "", false
}

func reasonToExperimentReason(reason ldreason.EvaluationReason) ldreason.EvaluationReason {
	switch reason.GetKind() {
	case ldreason.EvalReasonFallthrough:
		return ldreason.NewEvalReasonFallthroughExperiment(true)
	case ldreason.EvalReasonRuleMatch:
		return ldreason.NewEvalReasonRuleMatchExperiment(reason.GetRuleIndex(), reason.GetRuleID(), true)
	default:
		return reason // COVERAGE: unreachable
	}
}

func isExperiment(flag *ldmodel.FeatureFlag, reason ldreason.EvaluationReason) bool {
	// If the reason says we're in an experiment, we are. Otherwise, apply
	// the legacy rule exclusion logic.
	if reason.IsInExperiment() {
		return true
	}

	switch reason.GetKind() {
	case ldreason.EvalReasonFallthrough:
		return flag.TrackEventsFallthrough
	case ldreason.EvalReasonRuleMatch:
		i := reason.GetRuleIndex()
		if i >= 0 && i < len(flag.Rules) {
			return flag.Rules[i].TrackEvents
		}
	}
	return false
}
