package ldclient

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"reflect"
	"time"

	"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"
	ldevents "github.com/launchdarkly/go-sdk-events/v2"
	ldeval "github.com/launchdarkly/go-server-sdk-evaluation/v2"
	"github.com/launchdarkly/go-server-sdk-evaluation/v2/ldmodel"
	"github.com/launchdarkly/go-server-sdk/v6/interfaces"
	"github.com/launchdarkly/go-server-sdk/v6/interfaces/flagstate"
	"github.com/launchdarkly/go-server-sdk/v6/internal"
	"github.com/launchdarkly/go-server-sdk/v6/internal/bigsegments"
	"github.com/launchdarkly/go-server-sdk/v6/internal/datakinds"
	"github.com/launchdarkly/go-server-sdk/v6/internal/datasource"
	"github.com/launchdarkly/go-server-sdk/v6/internal/datastore"
	"github.com/launchdarkly/go-server-sdk/v6/ldcomponents"
	"github.com/launchdarkly/go-server-sdk/v6/subsystems"
	"github.com/launchdarkly/go-server-sdk/v6/subsystems/ldstoreimpl"
)

// Version is the SDK version.
const Version = internal.SDKVersion

// LDClient is the LaunchDarkly client.
//
// This object evaluates feature flags, generates analytics events, and communicates with
// LaunchDarkly services. Applications should instantiate a single instance for the lifetime
// of their application and share it wherever feature flags need to be evaluated; all LDClient
// methods are safe to be called concurrently from multiple goroutines.
//
// Some advanced client features are grouped together in API facades that are accessed through
// an LDClient method, such as [LDClient.GetDataSourceStatusProvider].
//
// When an application is shutting down or no longer needs to use the LDClient instance, it
// should call [LDClient.Close] to ensure that all of its connections and goroutines are shut down and
// that any pending analytics events have been delivered.
//
// For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/server-side/go
type LDClient struct {
	sdkKey                           string
	loggers                          ldlog.Loggers
	eventProcessor                   ldevents.EventProcessor
	dataSource                       subsystems.DataSource
	store                            subsystems.DataStore
	evaluator                        ldeval.Evaluator
	dataSourceStatusBroadcaster      *internal.Broadcaster[interfaces.DataSourceStatus]
	dataSourceStatusProvider         interfaces.DataSourceStatusProvider
	dataStoreStatusBroadcaster       *internal.Broadcaster[interfaces.DataStoreStatus]
	dataStoreStatusProvider          interfaces.DataStoreStatusProvider
	flagChangeEventBroadcaster       *internal.Broadcaster[interfaces.FlagChangeEvent]
	flagTracker                      interfaces.FlagTracker
	bigSegmentStoreStatusBroadcaster *internal.Broadcaster[interfaces.BigSegmentStoreStatus]
	bigSegmentStoreStatusProvider    interfaces.BigSegmentStoreStatusProvider
	bigSegmentStoreWrapper           *ldstoreimpl.BigSegmentStoreWrapper
	eventsDefault                    eventsScope
	eventsWithReasons                eventsScope
	withEventsDisabled               interfaces.LDClientInterface
	logEvaluationErrors              bool
	offline                          bool
}

// Initialization errors
var (
	// MakeClient and MakeCustomClient will return this error if the SDK was not able to establish a
	// LaunchDarkly connection within the specified time interval. In this case, the LDClient will still
	// continue trying to connect in the background.
	ErrInitializationTimeout = errors.New("timeout encountered waiting for LaunchDarkly client initialization")

	// MakeClient and MakeCustomClient will return this error if the SDK detected an error that makes it
	// impossible for a LaunchDarkly connection to succeed. Currently, the only such condition is if the
	// SDK key is invalid, since an invalid SDK key will never become valid.
	ErrInitializationFailed = errors.New("LaunchDarkly client initialization failed")

	// This error is returned by the Variation/VariationDetail methods if feature flags are not available
	// because the client has not successfully initialized. In this case, the result value will be whatever
	// default value was specified by the application.
	ErrClientNotInitialized = errors.New("feature flag evaluation called before LaunchDarkly client initialization completed") //nolint:lll
)

// MakeClient creates a new client instance that connects to LaunchDarkly with the default configuration.
//
// For advanced configuration options, use [MakeCustomClient]. Calling MakeClient is exactly equivalent to
// calling MakeCustomClient with the config parameter set to an empty value, ld.Config{}.
//
// The client will begin attempting to connect to LaunchDarkly as soon as you call this constructor. The
// constructor will return when it successfully connects, or when the timeout set by the waitFor parameter
// expires, whichever comes first.
//
// If the connection succeeded, the first return value is the client instance, and the error value is nil.
//
// If the timeout elapsed without a successful connection, it still returns a client instance-- in an
// uninitialized state, where feature flags will return default values-- and the error value is
// [ErrInitializationTimeout]. In this case, it will still continue trying to connect in the background.
//
// If there was an unrecoverable error such that it cannot succeed by retrying-- for instance, the SDK key is
// invalid-- it will return a client instance in an uninitialized state, and the error value is
// [ErrInitializationFailed].
//
// If you set waitFor to zero, the function will return immediately after creating the client instance, and
// do any further initialization in the background.
//
// The only time it returns nil instead of a client instance is if the client cannot be created at all due to
// an invalid configuration. This is rare, but could happen if for instance you specified a custom TLS
// certificate file that did not contain a valid certificate.
//
// For more about the difference between an initialized and uninitialized client, and other ways to monitor
// the client's status, see [LDClient.Initialized] and [LDClient.GetDataSourceStatusProvider].
func MakeClient(sdkKey string, waitFor time.Duration) (*LDClient, error) {
	// COVERAGE: this constructor cannot be called in unit tests because it uses the default base
	// URI and will attempt to make a live connection to LaunchDarkly.
	return MakeCustomClient(sdkKey, Config{}, waitFor)
}

// MakeCustomClient creates a new client instance that connects to LaunchDarkly with a custom configuration.
//
// The config parameter allows customization of all SDK properties; some of these are represented directly as
// fields in Config, while others are set by builder methods on a more specific configuration object. See
// [Config] for details.
//
// Unless it is configured to be offline with Config.Offline or [ldcomponents.ExternalUpdatesOnly], the client
// will begin attempting to connect to LaunchDarkly as soon as you call this constructor. The constructor will
// return when it successfully connects, or when the timeout set by the waitFor parameter expires, whichever
// comes first.
//
// If the connection succeeded, the first return value is the client instance, and the error value is nil.
//
// If the timeout elapsed without a successful connection, it still returns a client instance-- in an
// uninitialized state, where feature flags will return default values-- and the error value is
// [ErrInitializationTimeout]. In this case, it will still continue trying to connect in the background.
//
// If there was an unrecoverable error such that it cannot succeed by retrying-- for instance, the SDK key is
// invalid-- it will return a client instance in an uninitialized state, and the error value is
// [ErrInitializationFailed].
//
// If you set waitFor to zero, the function will return immediately after creating the client instance, and
// do any further initialization in the background.
//
// The only time it returns nil instead of a client instance is if the client cannot be created at all due to
// an invalid configuration. This is rare, but could happen if for instance you specified a custom TLS
// certificate file that did not contain a valid certificate.
//
// For more about the difference between an initialized and uninitialized client, and other ways to monitor
// the client's status, see [LDClient.Initialized] and [LDClient.GetDataSourceStatusProvider].
func MakeCustomClient(sdkKey string, config Config, waitFor time.Duration) (*LDClient, error) {
	// Ensure that any intermediate components we create will be disposed of if we return an error
	client := &LDClient{sdkKey: sdkKey}
	clientValid := false
	defer func() {
		if !clientValid {
			_ = client.Close()
		}
	}()

	closeWhenReady := make(chan struct{})

	eventProcessorFactory := getEventProcessorFactory(config)

	clientContext, err := newClientContextFromConfig(sdkKey, config)
	if err != nil {
		return nil, err
	}

	// Do not create a diagnostics manager if diagnostics are disabled, or if we're not using the standard event processor.
	if !config.DiagnosticOptOut {
		if reflect.TypeOf(eventProcessorFactory) == reflect.TypeOf(ldcomponents.SendEvents()) {
			clientContext.DiagnosticsManager = createDiagnosticsManager(clientContext, sdkKey, config, waitFor)
		}
	}

	loggers := clientContext.GetLogging().Loggers
	loggers.Infof("Starting LaunchDarkly client %s", Version)

	client.loggers = loggers
	client.logEvaluationErrors = clientContext.GetLogging().LogEvaluationErrors

	client.offline = config.Offline

	client.dataStoreStatusBroadcaster = internal.NewBroadcaster[interfaces.DataStoreStatus]()
	dataStoreUpdateSink := datastore.NewDataStoreUpdateSinkImpl(client.dataStoreStatusBroadcaster)
	storeFactory := config.DataStore
	if storeFactory == nil {
		storeFactory = ldcomponents.InMemoryDataStore()
	}
	clientContextWithDataStoreUpdateSink := clientContext
	clientContextWithDataStoreUpdateSink.DataStoreUpdateSink = dataStoreUpdateSink
	store, err := storeFactory.Build(clientContextWithDataStoreUpdateSink)
	if err != nil {
		return nil, err
	}
	client.store = store

	bigSegments := config.BigSegments
	if bigSegments == nil {
		bigSegments = ldcomponents.BigSegments(nil)
	}
	bsConfig, err := bigSegments.Build(clientContext)
	if err != nil {
		return nil, err
	}
	bsStore := bsConfig.GetStore()
	client.bigSegmentStoreStatusBroadcaster = internal.NewBroadcaster[interfaces.BigSegmentStoreStatus]()
	if bsStore != nil {
		client.bigSegmentStoreWrapper = ldstoreimpl.NewBigSegmentStoreWrapperWithConfig(
			ldstoreimpl.BigSegmentsConfigurationProperties{
				Store:              bsStore,
				StartPolling:       true,
				StatusPollInterval: bsConfig.GetStatusPollInterval(),
				StaleAfter:         bsConfig.GetStaleAfter(),
				ContextCacheSize:   bsConfig.GetContextCacheSize(),
				ContextCacheTime:   bsConfig.GetContextCacheTime(),
			},
			client.bigSegmentStoreStatusBroadcaster.Broadcast,
			loggers,
		)
		client.bigSegmentStoreStatusProvider = bigsegments.NewBigSegmentStoreStatusProviderImpl(
			client.bigSegmentStoreWrapper.GetStatus,
			client.bigSegmentStoreStatusBroadcaster,
		)
	} else {
		client.bigSegmentStoreStatusProvider = bigsegments.NewBigSegmentStoreStatusProviderImpl(
			nil, client.bigSegmentStoreStatusBroadcaster,
		)
	}

	dataProvider := ldstoreimpl.NewDataStoreEvaluatorDataProvider(store, loggers)
	evalOptions := []ldeval.EvaluatorOption{
		ldeval.EvaluatorOptionErrorLogger(client.loggers.ForLevel(ldlog.Error)),
	}
	if client.bigSegmentStoreWrapper != nil {
		evalOptions = append(evalOptions, ldeval.EvaluatorOptionBigSegmentProvider(client.bigSegmentStoreWrapper))
	}
	client.evaluator = ldeval.NewEvaluatorWithOptions(dataProvider, evalOptions...)

	client.dataStoreStatusProvider = datastore.NewDataStoreStatusProviderImpl(store, dataStoreUpdateSink)

	client.dataSourceStatusBroadcaster = internal.NewBroadcaster[interfaces.DataSourceStatus]()
	client.flagChangeEventBroadcaster = internal.NewBroadcaster[interfaces.FlagChangeEvent]()
	dataSourceUpdateSink := datasource.NewDataSourceUpdateSinkImpl(
		store,
		client.dataStoreStatusProvider,
		client.dataSourceStatusBroadcaster,
		client.flagChangeEventBroadcaster,
		clientContext.GetLogging().LogDataSourceOutageAsErrorAfter,
		loggers,
	)

	client.eventProcessor, err = eventProcessorFactory.Build(clientContext)
	if err != nil {
		return nil, err
	}
	if isNullEventProcessorFactory(eventProcessorFactory) {
		client.eventsDefault = newDisabledEventsScope()
		client.eventsWithReasons = newDisabledEventsScope()
	} else {
		client.eventsDefault = newEventsScope(client, false)
		client.eventsWithReasons = newEventsScope(client, true)
	}
	// Pre-create the WithEventsDisabled object so that if an application ends up calling WithEventsDisabled
	// frequently, it won't be causing an allocation each time.
	client.withEventsDisabled = newClientEventsDisabledDecorator(client)

	dataSource, err := createDataSource(config, clientContext, dataSourceUpdateSink)
	client.dataSource = dataSource
	if err != nil {
		return nil, err
	}
	client.dataSourceStatusProvider = datasource.NewDataSourceStatusProviderImpl(
		client.dataSourceStatusBroadcaster,
		dataSourceUpdateSink,
	)

	client.flagTracker = internal.NewFlagTrackerImpl(
		client.flagChangeEventBroadcaster,
		func(flagKey string, context ldcontext.Context, defaultValue ldvalue.Value) ldvalue.Value {
			value, _ := client.JSONVariation(flagKey, context, defaultValue)
			return value
		},
	)

	clientValid = true
	client.dataSource.Start(closeWhenReady)
	if waitFor > 0 && client.dataSource != datasource.NewNullDataSource() {
		loggers.Infof("Waiting up to %d milliseconds for LaunchDarkly client to start...",
			waitFor/time.Millisecond)
		timeout := time.After(waitFor)
		for {
			select {
			case <-closeWhenReady:
				if !client.dataSource.IsInitialized() {
					loggers.Warn("LaunchDarkly client initialization failed")
					return client, ErrInitializationFailed
				}

				loggers.Info("Initialized LaunchDarkly client")
				return client, nil
			case <-timeout:
				loggers.Warn("Timeout encountered waiting for LaunchDarkly client initialization")
				go func() { <-closeWhenReady }() // Don't block the DataSource when not waiting
				return client, ErrInitializationTimeout
			}
		}
	}
	go func() { <-closeWhenReady }() // Don't block the DataSource when not waiting
	return client, nil
}

func createDataSource(
	config Config,
	context *internal.ClientContextImpl,
	dataSourceUpdateSink subsystems.DataSourceUpdateSink,
) (subsystems.DataSource, error) {
	if config.Offline {
		context.GetLogging().Loggers.Info("Starting LaunchDarkly client in offline mode")
		dataSourceUpdateSink.UpdateStatus(interfaces.DataSourceStateValid, interfaces.DataSourceErrorInfo{})
		return datasource.NewNullDataSource(), nil
	}
	factory := config.DataSource
	if factory == nil {
		// COVERAGE: can't cause this condition in unit tests because it would try to connect to production LD
		factory = ldcomponents.StreamingDataSource()
	}
	contextCopy := *context
	contextCopy.BasicClientContext.DataSourceUpdateSink = dataSourceUpdateSink
	return factory.Build(&contextCopy)
}

// Identify reports details about an evaluation context.
//
// For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/features/identify#go
func (client *LDClient) Identify(context ldcontext.Context) error {
	if client.eventsDefault.disabled {
		return nil
	}
	if err := context.Err(); err != nil {
		client.loggers.Warnf("Identify called with invalid context: %s", err)
		return nil // Don't return an error value because we didn't in the past and it might confuse users
	}
	evt := client.eventsDefault.factory.NewIdentifyEventData(ldevents.Context(context))
	client.eventProcessor.RecordIdentifyEvent(evt)
	return nil
}

// TrackEvent reports an event associated with an evaluation context.
//
// The eventName parameter is defined by the application and will be shown in analytics reports;
// it normally corresponds to the event name of a metric that you have created through the
// LaunchDarkly dashboard. If you want to associate additional data with this event, use [TrackData]
// or [TrackMetric].
//
// For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/features/events#go
func (client *LDClient) TrackEvent(eventName string, context ldcontext.Context) error {
	return client.TrackData(eventName, context, ldvalue.Null())
}

// TrackData reports an event associated with an evaluation context, and adds custom data.
//
// The eventName parameter is defined by the application and will be shown in analytics reports;
// it normally corresponds to the event name of a metric that you have created through the
// LaunchDarkly dashboard.
//
// The data parameter is a value of any JSON type, represented with the ldvalue.Value type, that
// will be sent with the event. If no such value is needed, use [ldvalue.Null]() (or call [TrackEvent]
// instead). To send a numeric value for experimentation, use [TrackMetric].
//
// For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/features/events#go
func (client *LDClient) TrackData(eventName string, context ldcontext.Context, data ldvalue.Value) error {
	if client.eventsDefault.disabled {
		return nil
	}
	if err := context.Err(); err != nil {
		client.loggers.Warnf("Track called with invalid context: %s", err)
		return nil // Don't return an error value because we didn't in the past and it might confuse users
	}
	client.eventProcessor.RecordCustomEvent(
		client.eventsDefault.factory.NewCustomEventData(
			eventName,
			ldevents.Context(context),
			data,
			false,
			0,
		))
	return nil
}

// TrackMetric reports an event associated with an evaluation context, and adds a numeric value.
// This value is used by the LaunchDarkly experimentation feature in numeric custom metrics, and will also
// be returned as part of the custom event for Data Export.
//
// The eventName parameter is defined by the application and will be shown in analytics reports;
// it normally corresponds to the event name of a metric that you have created through the
// LaunchDarkly dashboard.
//
// The data parameter is a value of any JSON type, represented with the ldvalue.Value type, that
// will be sent with the event. If no such value is needed, use [ldvalue.Null]().
//
// For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/features/events#go
func (client *LDClient) TrackMetric(
	eventName string,
	context ldcontext.Context,
	metricValue float64,
	data ldvalue.Value,
) error {
	if client.eventsDefault.disabled {
		return nil
	}
	if err := context.Err(); err != nil {
		client.loggers.Warnf("TrackMetric called with invalid context: %s", err)
		return nil // Don't return an error value because we didn't in the past and it might confuse users
	}
	client.eventProcessor.RecordCustomEvent(
		client.eventsDefault.factory.NewCustomEventData(
			eventName,
			ldevents.Context(context),
			data,
			true,
			metricValue,
		))
	return nil
}

// IsOffline returns whether the LaunchDarkly client is in offline mode.
//
// This is only true if you explicitly set the Offline field to true in [Config], to force the client to
// be offline. It does not mean that the client is having a problem connecting to LaunchDarkly. To detect
// the status of a client that is configured to be online, use [LDClient.Initialized] or
// [LDClient.GetDataSourceStatusProvider].
//
// For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/features/offline-mode#go
func (client *LDClient) IsOffline() bool {
	return client.offline
}

// SecureModeHash generates the secure mode hash value for an evaluation context.
//
// For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/features/secure-mode#go
func (client *LDClient) SecureModeHash(context ldcontext.Context) string {
	key := []byte(client.sdkKey)
	h := hmac.New(sha256.New, key)
	_, _ = h.Write([]byte(context.FullyQualifiedKey()))
	return hex.EncodeToString(h.Sum(nil))
}

// Initialized returns whether the LaunchDarkly client is initialized.
//
// If this value is true, it means the client has succeeded at some point in connecting to LaunchDarkly and
// has received feature flag data. It could still have encountered a connection problem after that point, so
// this does not guarantee that the flags are up to date; if you need to know its status in more detail, use
// [LDClient.GetDataSourceStatusProvider].
//
// If this value is false, it means the client has not yet connected to LaunchDarkly, or has permanently
// failed. See [MakeClient] for the reasons that this could happen. In this state, feature flag evaluations
// will always return default values-- unless you are using a database integration and feature flags had
// already been stored in the database by a successfully connected SDK in the past. You can use
// [LDClient.GetDataSourceStatusProvider] to get information on errors, or to wait for a successful retry.
func (client *LDClient) Initialized() bool {
	return client.dataSource.IsInitialized()
}

// Close shuts down the LaunchDarkly client. After calling this, the LaunchDarkly client
// should no longer be used. The method will block until all pending analytics events (if any)
// been sent.
func (client *LDClient) Close() error {
	client.loggers.Info("Closing LaunchDarkly client")

	// Normally all of the following components exist; but they could be nil if we errored out
	// partway through the MakeCustomClient constructor, in which case we want to close whatever
	// did get created so far.
	if client.eventProcessor != nil {
		_ = client.eventProcessor.Close()
	}
	if client.dataSource != nil {
		_ = client.dataSource.Close()
	}
	if client.store != nil {
		_ = client.store.Close()
	}
	if client.dataSourceStatusBroadcaster != nil {
		client.dataSourceStatusBroadcaster.Close()
	}
	if client.dataStoreStatusBroadcaster != nil {
		client.dataStoreStatusBroadcaster.Close()
	}
	if client.flagChangeEventBroadcaster != nil {
		client.flagChangeEventBroadcaster.Close()
	}
	if client.bigSegmentStoreStatusBroadcaster != nil {
		client.bigSegmentStoreStatusBroadcaster.Close()
	}
	if client.bigSegmentStoreWrapper != nil {
		client.bigSegmentStoreWrapper.Close()
	}
	return nil
}

// Flush tells the client that all pending analytics events (if any) should be delivered as soon
// as possible. This flush is asynchronous, so this method will return before it is complete. To wait
// for the flush to complete, use [LDClient.FlushAndWait] instead (or, if you are done with the SDK,
// [LDClient.Close]).
//
// For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/features/flush#go
func (client *LDClient) Flush() {
	client.eventProcessor.Flush()
}

// FlushAndWait tells the client to deliver any pending analytics events synchronously now.
//
// Unlike [LDClient.Flush], this method waits for event delivery to finish. The timeout parameter, if
// greater than zero, specifies the maximum amount of time to wait. If the timeout elapses before
// delivery is finished, the method returns early and returns false; in this case, the SDK may still
// continue trying to deliver the events in the background.
//
// If the timeout parameter is zero or negative, the method waits as long as necessary to deliver the
// events. However, the SDK does not retry event delivery indefinitely; currently, any network error
// or server error will cause the SDK to wait one second and retry one time, after which the events
// will be discarded so that the SDK will not keep consuming more memory for events indefinitely.
//
// The method returns true if event delivery either succeeded, or definitively failed, before the
// timeout elapsed. It returns false if the timeout elapsed.
//
// This method is also implicitly called if you call [LDClient.Close]. The difference is that
// FlushAndWait does not shut down the SDK client.
//
// For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/features/flush#go
func (client *LDClient) FlushAndWait(timeout time.Duration) bool {
	return client.eventProcessor.FlushBlocking(timeout)
}

// AllFlagsState returns an object that encapsulates the state of all feature flags for a given evaluation.
// context. This includes the flag values, and also metadata that can be used on the front end.
//
// The most common use case for this method is to bootstrap a set of client-side feature flags from a
// back-end service.
//
// You may pass any combination of [flagstate.ClientSideOnly], [flagstate.WithReasons], and
// [flagstate.DetailsOnlyForTrackedFlags] as optional parameters to control what data is included.
//
// For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/features/all-flags#go
func (client *LDClient) AllFlagsState(context ldcontext.Context, options ...flagstate.Option) flagstate.AllFlags {
	valid := true
	if client.IsOffline() {
		client.loggers.Warn("Called AllFlagsState in offline mode. Returning empty state")
		valid = false
	} else if !client.Initialized() {
		if client.store.IsInitialized() {
			client.loggers.Warn("Called AllFlagsState before client initialization; using last known values from data store")
		} else {
			client.loggers.Warn("Called AllFlagsState before client initialization. Data store not available; returning empty state") //nolint:lll
			valid = false
		}
	}

	if !valid {
		return flagstate.AllFlags{}
	}

	items, err := client.store.GetAll(datakinds.Features)
	if err != nil {
		client.loggers.Warn("Unable to fetch flags from data store. Returning empty state. Error: " + err.Error())
		return flagstate.AllFlags{}
	}

	clientSideOnly := false
	for _, o := range options {
		if o == flagstate.OptionClientSideOnly() {
			clientSideOnly = true
			break
		}
	}

	state := flagstate.NewAllFlagsBuilder(options...)
	for _, item := range items {
		if item.Item.Item != nil {
			if flag, ok := item.Item.Item.(*ldmodel.FeatureFlag); ok {
				if clientSideOnly && !flag.ClientSideAvailability.UsingEnvironmentID {
					continue
				}

				result := client.evaluator.Evaluate(flag, context, nil)

				state.AddFlag(
					item.Key,
					flagstate.FlagState{
						Value:                result.Detail.Value,
						Variation:            result.Detail.VariationIndex,
						Reason:               result.Detail.Reason,
						Version:              flag.Version,
						TrackEvents:          flag.TrackEvents || result.IsExperiment,
						TrackReason:          result.IsExperiment,
						DebugEventsUntilDate: flag.DebugEventsUntilDate,
					},
				)
			}
		}
	}

	return state.Build()
}

// BoolVariation returns the value of a boolean feature flag for a given evaluation context.
//
// Returns defaultVal if there is an error, if the flag doesn't exist, or the feature is turned off and
// has no off variation.
//
// For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/features/evaluating#go
func (client *LDClient) BoolVariation(key string, context ldcontext.Context, defaultVal bool) (bool, error) {
	detail, err := client.variation(key, context, ldvalue.Bool(defaultVal), true, client.eventsDefault)
	return detail.Value.BoolValue(), err
}

// BoolVariationDetail is the same as [LDClient.BoolVariation], but also returns further information about how
// the value was calculated. The "reason" data will also be included in analytics events.
//
// For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/features/evaluation-reasons#go
func (client *LDClient) BoolVariationDetail(
	key string,
	context ldcontext.Context,
	defaultVal bool,
) (bool, ldreason.EvaluationDetail, error) {
	detail, err := client.variation(key, context, ldvalue.Bool(defaultVal), true, client.eventsWithReasons)
	return detail.Value.BoolValue(), detail, err
}

// IntVariation returns the value of a feature flag (whose variations are integers) for the given evaluation
// context.
//
// Returns defaultVal if there is an error, if the flag doesn't exist, or the feature is turned off and
// has no off variation.
//
// If the flag variation has a numeric value that is not an integer, it is rounded toward zero (truncated).
//
// For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/features/evaluating#go
func (client *LDClient) IntVariation(key string, context ldcontext.Context, defaultVal int) (int, error) {
	detail, err := client.variation(key, context, ldvalue.Int(defaultVal), true, client.eventsDefault)
	return detail.Value.IntValue(), err
}

// IntVariationDetail is the same as [LDClient.IntVariation], but also returns further information about how
// the value was calculated. The "reason" data will also be included in analytics events.
//
// For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/features/evaluation-reasons#go
func (client *LDClient) IntVariationDetail(
	key string,
	context ldcontext.Context,
	defaultVal int,
) (int, ldreason.EvaluationDetail, error) {
	detail, err := client.variation(key, context, ldvalue.Int(defaultVal), true, client.eventsWithReasons)
	return detail.Value.IntValue(), detail, err
}

// Float64Variation returns the value of a feature flag (whose variations are floats) for the given evaluation
// context.
//
// Returns defaultVal if there is an error, if the flag doesn't exist, or the feature is turned off and
// has no off variation.
//
// For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/features/evaluating#go
func (client *LDClient) Float64Variation(key string, context ldcontext.Context, defaultVal float64) (float64, error) {
	detail, err := client.variation(key, context, ldvalue.Float64(defaultVal), true, client.eventsDefault)
	return detail.Value.Float64Value(), err
}

// Float64VariationDetail is the same as [LDClient.Float64Variation], but also returns further information about how
// the value was calculated. The "reason" data will also be included in analytics events.
//
// For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/features/evaluation-reasons#go
func (client *LDClient) Float64VariationDetail(
	key string,
	context ldcontext.Context,
	defaultVal float64,
) (float64, ldreason.EvaluationDetail, error) {
	detail, err := client.variation(key, context, ldvalue.Float64(defaultVal), true, client.eventsWithReasons)
	return detail.Value.Float64Value(), detail, err
}

// StringVariation returns the value of a feature flag (whose variations are strings) for the given evaluation
// context.
//
// Returns defaultVal if there is an error, if the flag doesn't exist, or the feature is turned off and has
// no off variation.
//
// For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/features/evaluating#go
func (client *LDClient) StringVariation(key string, context ldcontext.Context, defaultVal string) (string, error) {
	detail, err := client.variation(key, context, ldvalue.String(defaultVal), true, client.eventsDefault)
	return detail.Value.StringValue(), err
}

// StringVariationDetail is the same as [LDClient.StringVariation], but also returns further information about how
// the value was calculated. The "reason" data will also be included in analytics events.
//
// For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/features/evaluation-reasons#go
func (client *LDClient) StringVariationDetail(
	key string,
	context ldcontext.Context,
	defaultVal string,
) (string, ldreason.EvaluationDetail, error) {
	detail, err := client.variation(key, context, ldvalue.String(defaultVal), true, client.eventsWithReasons)
	return detail.Value.StringValue(), detail, err
}

// JSONVariation returns the value of a feature flag for the given evaluation context, allowing the value to
// be of any JSON type.
//
// The value is returned as an [ldvalue.Value], which can be inspected or converted to other types using
// methods such as [ldvalue.Value.GetType] and [ldvalue.Value.BoolValue]. The defaultVal parameter also uses this
// type. For instance, if the values for this flag are JSON arrays:
//
//	defaultValAsArray := ldvalue.BuildArray().
//	    Add(ldvalue.String("defaultFirstItem")).
//	    Add(ldvalue.String("defaultSecondItem")).
//	    Build()
//	result, err := client.JSONVariation(flagKey, context, defaultValAsArray)
//	firstItemAsString := result.GetByIndex(0).StringValue() // "defaultFirstItem", etc.
//
// You can also use unparsed json.RawMessage values:
//
//	defaultValAsRawJSON := ldvalue.Raw(json.RawMessage(`{"things":[1,2,3]}`))
//	result, err := client.JSONVariation(flagKey, context, defaultValAsJSON
//	resultAsRawJSON := result.AsRaw()
//
// Returns defaultVal if there is an error, if the flag doesn't exist, or the feature is turned off.
//
// For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/features/evaluating#go
func (client *LDClient) JSONVariation(
	key string,
	context ldcontext.Context,
	defaultVal ldvalue.Value,
) (ldvalue.Value, error) {
	detail, err := client.variation(key, context, defaultVal, false, client.eventsDefault)
	return detail.Value, err
}

// JSONVariationDetail is the same as [LDClient.JSONVariation], but also returns further information about how
// the value was calculated. The "reason" data will also be included in analytics events.
//
// For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/features/evaluation-reasons#go
func (client *LDClient) JSONVariationDetail(
	key string,
	context ldcontext.Context,
	defaultVal ldvalue.Value,
) (ldvalue.Value, ldreason.EvaluationDetail, error) {
	detail, err := client.variation(key, context, defaultVal, false, client.eventsWithReasons)
	return detail.Value, detail, err
}

// GetDataSourceStatusProvider returns an interface for tracking the status of the data source.
//
// The data source is the mechanism that the SDK uses to get feature flag configurations, such as a
// streaming connection (the default) or poll requests. The [interfaces.DataSourceStatusProvider] has methods
// for checking whether the data source is (as far as the SDK knows) currently operational and tracking
// changes in this status.
//
// See the DataSourceStatusProvider interface for more about this functionality.
func (client *LDClient) GetDataSourceStatusProvider() interfaces.DataSourceStatusProvider {
	return client.dataSourceStatusProvider
}

// GetDataStoreStatusProvider returns an interface for tracking the status of a persistent data store.
//
// The [interfaces.DataStoreStatusProvider] has methods for checking whether the data store is (as far as the SDK
// knows) currently operational, tracking changes in this status, and getting cache statistics. These
// are only relevant for a persistent data store; if you are using an in-memory data store, then this
// method will always report that the store is operational.
//
// See the DataStoreStatusProvider interface for more about this functionality.
func (client *LDClient) GetDataStoreStatusProvider() interfaces.DataStoreStatusProvider {
	return client.dataStoreStatusProvider
}

// GetFlagTracker returns an interface for tracking changes in feature flag configurations.
//
// See [interfaces.FlagTracker] for more about this functionality.
func (client *LDClient) GetFlagTracker() interfaces.FlagTracker {
	return client.flagTracker
}

// GetBigSegmentStoreStatusProvider returns an interface for tracking the status of a Big
// Segment store.
//
// The BigSegmentStoreStatusProvider has methods for checking whether the Big Segment store
// is (as far as the SDK knows) currently operational and tracking changes in this status.
//
// See [interfaces.BigSegmentStoreStatusProvider] for more about this functionality.
func (client *LDClient) GetBigSegmentStoreStatusProvider() interfaces.BigSegmentStoreStatusProvider {
	return client.bigSegmentStoreStatusProvider
}

// WithEventsDisabled returns a decorator for the LDClient that implements the same basic operations
// but will not generate any analytics events.
//
// If events were already disabled, this is just the same LDClient. Otherwise, it is an object whose
// Variation methods use the same LDClient to evaluate feature flags, but without generating any
// events, and whose Identify/Track/Custom methods do nothing. Neither evaluation counts nor context
// properties will be sent to LaunchDarkly for any operations done with this object.
//
// You can use this to suppress events within some particular area of your code where you do not want
// evaluations to affect your dashboard statistics, or do not want to incur the overhead of processing
// the events.
//
// Note that if the original client configuration already had events disabled
// (config.Events = ldcomponents.NoEvents()), you cannot re-enable them with this method. It is only
// useful for temporarily disabling events on a client that had them enabled.
func (client *LDClient) WithEventsDisabled(disabled bool) interfaces.LDClientInterface {
	if !disabled || client.eventsDefault.disabled {
		return client
	}
	return client.withEventsDisabled
}

// Generic method for evaluating a feature flag for a given evaluation context.
func (client *LDClient) variation(
	key string,
	context ldcontext.Context,
	defaultVal ldvalue.Value,
	checkType bool,
	eventsScope eventsScope,
) (ldreason.EvaluationDetail, error) {
	if err := context.Err(); err != nil {
		client.loggers.Warnf("Tried to evaluate a flag with an invalid context: %s", err)
		return newEvaluationError(defaultVal, ldreason.EvalErrorUserNotSpecified), err
	}
	if client.IsOffline() {
		return newEvaluationError(defaultVal, ldreason.EvalErrorClientNotReady), nil
	}
	result, flag, err := client.evaluateInternal(key, context, defaultVal, eventsScope)
	if err != nil {
		result.Detail.Value = defaultVal
		result.Detail.VariationIndex = ldvalue.OptionalInt{}
	} else if checkType && defaultVal.Type() != ldvalue.NullType && result.Detail.Value.Type() != defaultVal.Type() {
		result.Detail = newEvaluationError(defaultVal, ldreason.EvalErrorWrongType)
	}

	if !eventsScope.disabled {
		var eval ldevents.EvaluationData
		if flag == nil {
			eval = eventsScope.factory.NewUnknownFlagEvaluationData(
				key,
				ldevents.Context(context),
				defaultVal,
				result.Detail.Reason,
			)
		} else {
			eval = eventsScope.factory.NewEvaluationData(
				ldevents.FlagEventProperties{
					Key:                  flag.Key,
					Version:              flag.Version,
					RequireFullEvent:     flag.TrackEvents,
					DebugEventsUntilDate: flag.DebugEventsUntilDate,
				},
				ldevents.Context(context),
				result.Detail,
				result.IsExperiment,
				defaultVal,
				"",
			)
		}
		client.eventProcessor.RecordEvaluation(eval)
	}

	return result.Detail, err
}

// Performs all the steps of evaluation except for sending the feature request event (the main one;
// events for prerequisites will be sent).
func (client *LDClient) evaluateInternal(
	key string,
	context ldcontext.Context,
	defaultVal ldvalue.Value,
	eventsScope eventsScope,
) (ldeval.Result, *ldmodel.FeatureFlag, error) {
	// THIS IS A HIGH-TRAFFIC CODE PATH so performance tuning is important. Please see CONTRIBUTING.md for guidelines
	// to keep in mind during any changes to the evaluation logic.

	var feature *ldmodel.FeatureFlag
	var storeErr error
	var ok bool

	evalErrorResult := func(
		errKind ldreason.EvalErrorKind,
		flag *ldmodel.FeatureFlag,
		err error,
	) (ldeval.Result, *ldmodel.FeatureFlag, error) {
		detail := newEvaluationError(defaultVal, errKind)
		if client.logEvaluationErrors {
			client.loggers.Warn(err)
		}
		return ldeval.Result{Detail: detail}, flag, err
	}

	if !client.Initialized() {
		if client.store.IsInitialized() {
			client.loggers.Warn("Feature flag evaluation called before LaunchDarkly client initialization completed; using last known values from data store") //nolint:lll
		} else {
			return evalErrorResult(ldreason.EvalErrorClientNotReady, nil, ErrClientNotInitialized)
		}
	}

	itemDesc, storeErr := client.store.Get(datakinds.Features, key)

	if storeErr != nil {
		client.loggers.Errorf("Encountered error fetching feature from store: %+v", storeErr)
		detail := newEvaluationError(defaultVal, ldreason.EvalErrorException)
		return ldeval.Result{Detail: detail}, nil, storeErr
	}

	if itemDesc.Item != nil {
		feature, ok = itemDesc.Item.(*ldmodel.FeatureFlag)
		if !ok {
			return evalErrorResult(ldreason.EvalErrorException, nil,
				fmt.Errorf(
					"unexpected data type (%T) found in store for feature key: %s. Returning default value",
					itemDesc.Item,
					key,
				))
		}
	} else {
		return evalErrorResult(ldreason.EvalErrorFlagNotFound, nil,
			fmt.Errorf("unknown feature key: %s. Verify that this feature key exists. Returning default value", key))
	}

	result := client.evaluator.Evaluate(feature, context, eventsScope.prerequisiteEventRecorder)
	if result.Detail.Reason.GetKind() == ldreason.EvalReasonError && client.logEvaluationErrors {
		client.loggers.Warnf("Flag evaluation for %s failed with error %s, default value was returned",
			key, result.Detail.Reason.GetErrorKind())
	}
	if result.Detail.IsDefaultValue() {
		result.Detail.Value = defaultVal
	}
	return result, feature, nil
}

func newEvaluationError(jsonValue ldvalue.Value, errorKind ldreason.EvalErrorKind) ldreason.EvaluationDetail {
	return ldreason.EvaluationDetail{
		Value:  jsonValue,
		Reason: ldreason.NewEvalReasonError(errorKind),
	}
}
