package datakinds

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

	"github.com/launchdarkly/go-jsonstream/v3/jreader"
)

// This file defines the StoreDataKind implementations corresponding to our two top-level data model
// types, FeatureFlag and Segment (both of which are defined in go-server-sdk-evaluation). We access
// these objects directly throughout the SDK. We also export them indirectly in the package
// subsystems/ldstoreimpl, because they may be needed by external code that is implementing a
// custom data store.

//nolint:gochecknoglobals // global used as a constant for efficiency
var modelSerialization = ldmodel.NewJSONDataModelSerialization()

// When we produce a JSON representation for a deleted item (tombstone), we need to ensure that it has
// all the properties that the SDKs expect to see in a non-deleted item as well, since some SDKS (like
// PHP) are not tolerant of missing properties when unmarshaling. That means it should have a key as
// well - but the Serialize methods do not receive a key parameter, so we need to make up something.
// The SDK should not actually do anything with the key property in a tombstone but just in case, we'll
// make it something that cannot conflict with a real flag key (since '$' is not allowed).
const deletedItemPlaceholderKey = "$deleted"

// Type aliases for our two implementations of StoreDataKind
type featureFlagStoreDataKind struct{}
type segmentStoreDataKind struct{}

// Features is the global StoreDataKind instance for feature flags.
var Features DataKindInternal = featureFlagStoreDataKind{} //nolint:gochecknoglobals

// Segments is the global StoreDataKind instance for segments.
var Segments DataKindInternal = segmentStoreDataKind{} //nolint:gochecknoglobals

// AllDataKinds returns all the supported data StoreDataKinds.
func AllDataKinds() []ldstoretypes.DataKind {
	return []ldstoretypes.DataKind{Features, Segments}
}

// GetName returns the unique namespace identifier for feature flag objects.
func (fk featureFlagStoreDataKind) GetName() string {
	return "features"
}

// Serialize is used internally by the SDK when communicating with a PersistentDataStore.
func (fk featureFlagStoreDataKind) Serialize(item ldstoretypes.ItemDescriptor) []byte {
	if item.Item == nil {
		flag := ldmodel.FeatureFlag{Key: deletedItemPlaceholderKey, Version: item.Version, Deleted: true}
		if bytes, err := modelSerialization.MarshalFeatureFlag(flag); err == nil {
			return bytes
		}
	} else if flag, ok := item.Item.(*ldmodel.FeatureFlag); ok {
		if bytes, err := modelSerialization.MarshalFeatureFlag(*flag); err == nil {
			return bytes
		}
	}
	return nil
}

// Deserialize is used internally by the SDK when communicating with a PersistentDataStore.
func (fk featureFlagStoreDataKind) Deserialize(data []byte) (ldstoretypes.ItemDescriptor, error) {
	flag, err := modelSerialization.UnmarshalFeatureFlag(data)
	return maybeFlag(flag, err)
}

// DeserializeFromJSONReader is used internally by the SDK when parsing multiple flags at once.
func (fk featureFlagStoreDataKind) DeserializeFromJSONReader(reader *jreader.Reader) (
	ldstoretypes.ItemDescriptor, error) {
	flag := ldmodel.UnmarshalFeatureFlagFromJSONReader(reader)
	return maybeFlag(flag, reader.Error())
}

func maybeFlag(flag ldmodel.FeatureFlag, err error) (ldstoretypes.ItemDescriptor, error) {
	if err != nil {
		return ldstoretypes.ItemDescriptor{}, err
	}
	if flag.Deleted {
		return ldstoretypes.ItemDescriptor{Version: flag.Version, Item: nil}, nil
	}
	return ldstoretypes.ItemDescriptor{Version: flag.Version, Item: &flag}, nil
}

// String returns a human-readable string identifier.
func (fk featureFlagStoreDataKind) String() string {
	return fk.GetName()
}

// GetName returns the unique namespace identifier for segment objects.
func (sk segmentStoreDataKind) GetName() string {
	return "segments"
}

// Serialize is used internally by the SDK when communicating with a PersistentDataStore.
func (sk segmentStoreDataKind) Serialize(item ldstoretypes.ItemDescriptor) []byte {
	if item.Item == nil {
		segment := ldmodel.Segment{Key: deletedItemPlaceholderKey, Version: item.Version, Deleted: true}
		if bytes, err := modelSerialization.MarshalSegment(segment); err == nil {
			return bytes
		}
	} else if segment, ok := item.Item.(*ldmodel.Segment); ok {
		if bytes, err := modelSerialization.MarshalSegment(*segment); err == nil {
			return bytes
		}
	}
	return nil
}

// Deserialize is used internally by the SDK when communicating with a PersistentDataStore.
func (sk segmentStoreDataKind) Deserialize(data []byte) (ldstoretypes.ItemDescriptor, error) {
	segment, err := modelSerialization.UnmarshalSegment(data)
	return maybeSegment(segment, err)
}

// DeserializeFromJSONReader is used internally by the SDK when parsing multiple flags at once.
func (sk segmentStoreDataKind) DeserializeFromJSONReader(reader *jreader.Reader) (ldstoretypes.ItemDescriptor, error) {
	segment := ldmodel.UnmarshalSegmentFromJSONReader(reader)
	return maybeSegment(segment, reader.Error())
}

func maybeSegment(segment ldmodel.Segment, err error) (ldstoretypes.ItemDescriptor, error) {
	if err != nil {
		return ldstoretypes.ItemDescriptor{}, err
	}
	if segment.Deleted {
		return ldstoretypes.ItemDescriptor{Version: segment.Version, Item: nil}, nil
	}
	return ldstoretypes.ItemDescriptor{Version: segment.Version, Item: &segment}, nil
}

// String returns a human-readable string identifier.
func (sk segmentStoreDataKind) String() string {
	return sk.GetName()
}
