package ldtestdata

import (
	"sync"

	"github.com/launchdarkly/go-server-sdk-evaluation/v2/ldmodel"
	"github.com/launchdarkly/go-server-sdk/v6/interfaces"
	"github.com/launchdarkly/go-server-sdk/v6/subsystems"
	"github.com/launchdarkly/go-server-sdk/v6/subsystems/ldstoreimpl"
	"github.com/launchdarkly/go-server-sdk/v6/subsystems/ldstoretypes"

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

// TestDataSource is a test fixture that provides dynamically updatable feature flag state in a
// simplified form to an SDK client in test scenarios.
//
// See package description for more details and usage examples.
type TestDataSource struct {
	currentFlags    map[string]ldstoretypes.ItemDescriptor
	currentBuilders map[string]*FlagBuilder
	currentSegments map[string]ldstoretypes.ItemDescriptor
	instances       []*testDataSourceImpl
	lock            sync.Mutex
}

type testDataSourceImpl struct {
	owner   *TestDataSource
	updates subsystems.DataSourceUpdateSink
}

// DataSource creates an instance of [TestDataSource].
//
// Storing this object in the DataSource field of [github.com/launchdarkly/go-server-sdk/v6.Config]
// causes the SDK client to use the test data. Any subsequent changes made using methods like
// [TestDataSource.Update] will propagate to all LDClient instances that are using this data source.
func DataSource() *TestDataSource {
	return &TestDataSource{
		currentFlags:    make(map[string]ldstoretypes.ItemDescriptor),
		currentBuilders: make(map[string]*FlagBuilder),
		currentSegments: make(map[string]ldstoretypes.ItemDescriptor),
	}
}

// Flag creates or copies a [FlagBuilder] for building a test flag configuration.
//
// If this flag key has already been defined in this TestDataSource instance, then the builder
// starts with the same configuration that was last provided for this flag.
//
// Otherwise, it starts with a new default configuration in which the flag has true and false
// variations, is true for all users when targeting is turned on and false otherwise, and
// currently has targeting turned on. You can change any of those properties, and provide more
// complex behavior, using the FlagBuilder methods.
//
// Once you have set the desired configuration, pass the builder to Update.
func (t *TestDataSource) Flag(key string) *FlagBuilder {
	t.lock.Lock()
	defer t.lock.Unlock()
	existingBuilder := t.currentBuilders[key]
	if existingBuilder == nil {
		return newFlagBuilder(key).BooleanFlag()
	}
	return copyFlagBuilder(existingBuilder)
}

// Update updates the test data with the specified flag configuration.
//
// This has the same effect as if a flag were added or modified on the LaunchDarkly dashboard.
// It immediately propagates the flag change to any LDClient instance(s) that you have already
// configured to use this TestDataSource. If no LDClient has been started yet, it simply adds
// this flag to the test data which will be provided to any LDClient that you subsequently
// configure.
//
// Any subsequent changes to this FlagBuilder instance do not affect the test data, unless
// you call Update again.
func (t *TestDataSource) Update(flagBuilder *FlagBuilder) *TestDataSource {
	key := flagBuilder.key
	clonedBuilder := copyFlagBuilder(flagBuilder)
	t.updateInternal(key, flagBuilder.createFlag, clonedBuilder)
	return t
}

// UpdateStatus simulates a change in the data source status.
//
// Use this if you want to test the behavior of application code that uses
// LDClient.GetDataSourceStatusProvider to track whether the data source is having problems (for example,
// a network failure interruptsingthe streaming connection). It does not actually stop the
// TestDataSource from working, so even if you have simulated an outage, calling Update will still send
// updates.
func (t *TestDataSource) UpdateStatus(
	newState interfaces.DataSourceState,
	newError interfaces.DataSourceErrorInfo,
) *TestDataSource {
	t.lock.Lock()
	instances := slices.Clone(t.instances)
	t.lock.Unlock()

	for _, instance := range instances {
		instance.updates.UpdateStatus(newState, newError)
	}

	return t
}

// UsePreconfiguredFlag copies a full feature flag data model object into the test data.
//
// It immediately propagates the flag change to any LDClient instance(s) that you have already
// configured to use this TestDataSource. If no LDClient has been started yet, it simply adds
// this flag to the test data which will be provided to any LDClient that you subsequently
// configure.
//
// Use this method if you need to use advanced flag configuration properties that are not supported by
// the simplified FlagBuilder API. Otherwise it is recommended to use the regular Flag/Update
// mechanism to avoid dependencies on details of the data model.
//
// You cannot make incremental changes with Flag/Update to a flag that has been added in this way;
// you can only replace it with an entirely new flag configuration.
//
// To construct an instance of ldmodel.FeatureFlag, rather than accessing the fields directly it is
// recommended to use the builder API in [github.com/launchdarkly/go-server-sdk-evaluation/v2/ldbuilders].
func (t *TestDataSource) UsePreconfiguredFlag(flag ldmodel.FeatureFlag) *TestDataSource {
	t.updateInternal(
		flag.Key,
		func(version int) ldmodel.FeatureFlag {
			f := flag
			if f.Version < version {
				f.Version = version
			}
			return f
		},
		nil,
	)
	return t
}

// UsePreconfiguredSegment copies a full user segment data model object into the test data.
//
// It immediately propagates the flag change to any LDClient instance(s) that you have already
// configured to use this TestDataSource. If no LDClient has been started yet, it simply adds
// this flag to the test data which will be provided to any LDClient that you subsequently
// configure.
//
// This method is currently the only way to inject user segment data, since there is no builder
// API for segments. It is mainly intended for the SDK's own tests of user segment functionality,
// since application tests that need to produce a desired evaluation state could do so more easily
// by just setting flag values.
//
// To construct an instance of ldmodel.Segment, rather than accessing the fields directly it is
// recommended to use the builder API in [github.com/launchdarkly/go-server-sdk-evaluation/v2/ldbuilders].
func (t *TestDataSource) UsePreconfiguredSegment(segment ldmodel.Segment) *TestDataSource {
	t.lock.Lock()
	oldItem := t.currentSegments[segment.Key]
	newSegment := segment
	newSegment.Version = oldItem.Version + 1
	newItem := ldstoretypes.ItemDescriptor{Version: newSegment.Version, Item: &newSegment}
	t.currentSegments[segment.Key] = newItem
	instances := slices.Clone(t.instances)
	t.lock.Unlock()

	for _, instance := range instances {
		instance.updates.Upsert(ldstoreimpl.Segments(), segment.Key, newItem)
	}

	return t
}

func (t *TestDataSource) updateInternal(
	key string,
	makeFlag func(int) ldmodel.FeatureFlag,
	builder *FlagBuilder,
) {
	t.lock.Lock()
	oldItem := t.currentFlags[key]
	newVersion := oldItem.Version + 1
	newFlag := makeFlag(newVersion)
	newItem := ldstoretypes.ItemDescriptor{Version: newVersion, Item: &newFlag}
	t.currentFlags[key] = newItem
	t.currentBuilders[key] = builder
	instances := slices.Clone(t.instances)
	t.lock.Unlock()

	for _, instance := range instances {
		instance.updates.Upsert(ldstoreimpl.Features(), key, newItem)
	}
}

// Build is called internally by the SDK to associate this test data source with an
// LDClient instance. You do not need to call this method.
func (t *TestDataSource) Build(context subsystems.ClientContext) (subsystems.DataSource, error) {
	instance := &testDataSourceImpl{owner: t, updates: context.GetDataSourceUpdateSink()}
	t.lock.Lock()
	t.instances = append(t.instances, instance)
	t.lock.Unlock()
	return instance, nil
}

func (t *TestDataSource) makeInitData() []ldstoretypes.Collection {
	t.lock.Lock()
	defer t.lock.Unlock()
	flags := make([]ldstoretypes.KeyedItemDescriptor, 0, len(t.currentFlags))
	segments := make([]ldstoretypes.KeyedItemDescriptor, 0, len(t.currentSegments))
	for key, item := range t.currentFlags {
		flags = append(flags, ldstoretypes.KeyedItemDescriptor{Key: key, Item: item})
	}
	for key, item := range t.currentSegments {
		segments = append(segments, ldstoretypes.KeyedItemDescriptor{Key: key, Item: item})
	}
	return []ldstoretypes.Collection{
		{Kind: ldstoreimpl.Features(), Items: flags},
		{Kind: ldstoreimpl.Segments(), Items: segments},
	}
}

func (t *TestDataSource) closedInstance(instance *testDataSourceImpl) {
	t.lock.Lock()
	defer t.lock.Unlock()
	for i, in := range t.instances {
		if in == instance {
			copy(t.instances[i:], t.instances[i+1:])
			t.instances[len(t.instances)-1] = nil
			t.instances = t.instances[:len(t.instances)-1]
			break
		}
	}
}

func (d *testDataSourceImpl) Close() error {
	d.owner.closedInstance(d)
	return nil
}

func (d *testDataSourceImpl) IsInitialized() bool {
	return true
}

func (d *testDataSourceImpl) Start(closeWhenReady chan<- struct{}) {
	_ = d.updates.Init(d.owner.makeInitData())
	d.updates.UpdateStatus(interfaces.DataSourceStateValid, interfaces.DataSourceErrorInfo{})
	close(closeWhenReady)
}
