package mutexmap

import (
	"sync"

	"github.com/splitio/go-split-commons/v6/dtos"
	"github.com/splitio/go-split-commons/v6/flagsets"
	"github.com/splitio/go-split-commons/v6/storage"
	"github.com/splitio/go-toolkit/v5/datastructures/set"
)

// MMSplitStorage struct contains is an in-memory implementation of split storage
type MMSplitStorage struct {
	data          map[string]dtos.SplitDTO
	flagSets      flagsets.FeaturesBySet
	trafficTypes  map[string]int64
	till          int64
	flagSetsMutex *sync.RWMutex
	mutex         *sync.RWMutex
	ttMutex       *sync.RWMutex
	tillMutex     *sync.RWMutex
	flagSetFilter flagsets.FlagSetFilter
}

// NewMMSplitStorage instantiates a new MMSplitStorage
func NewMMSplitStorage(flagSetFilter flagsets.FlagSetFilter) *MMSplitStorage {
	return &MMSplitStorage{
		data:          make(map[string]dtos.SplitDTO),
		flagSets:      flagsets.NewFeaturesBySet(nil),
		trafficTypes:  make(map[string]int64),
		till:          -1,
		flagSetsMutex: &sync.RWMutex{},
		mutex:         &sync.RWMutex{},
		ttMutex:       &sync.RWMutex{},
		tillMutex:     &sync.RWMutex{},
		flagSetFilter: flagSetFilter,
	}
}

// All returns a list with a copy of each split.
// NOTE: This method will block any further operations regarding splits. Use with caution
func (m *MMSplitStorage) All() []dtos.SplitDTO {
	m.mutex.RLock()
	defer m.mutex.RUnlock()
	splitList := make([]dtos.SplitDTO, 0)
	for _, split := range m.data {
		splitList = append(splitList, split)
	}
	return splitList
}

func (m *MMSplitStorage) GetAllFlagSetNames() []string {
	m.flagSetsMutex.RLock()
	defer m.flagSetsMutex.RUnlock()

	return m.flagSets.Sets()
}

// ChangeNumber returns the last timestamp the split was fetched
func (m *MMSplitStorage) ChangeNumber() (int64, error) {
	m.tillMutex.RLock()
	defer m.tillMutex.RUnlock()
	return m.till, nil
}

func (m *MMSplitStorage) _get(splitName string) *dtos.SplitDTO {
	item, exists := m.data[splitName]
	if !exists {
		return nil
	}
	return &item
}

// FetchMany fetches features in redis and returns an array of split dtos
func (m *MMSplitStorage) FetchMany(splitNames []string) map[string]*dtos.SplitDTO {
	m.mutex.RLock()
	defer m.mutex.RUnlock()
	splits := make(map[string]*dtos.SplitDTO)
	for _, splitName := range splitNames {
		splits[splitName] = m._get(splitName)
	}
	return splits
}

// KillLocally kills the split locally
func (m *MMSplitStorage) KillLocally(splitName string, defaultTreatment string, changeNumber int64) {
	m.mutex.Lock()
	defer m.mutex.Unlock()
	split := m._get(splitName)
	till, err := m.ChangeNumber()
	if err != nil {
		return
	}
	if split != nil && till < changeNumber {
		split.DefaultTreatment = defaultTreatment
		split.Killed = true
		split.ChangeNumber = changeNumber
		m.data[split.Name] = *split
	}
}

// increaseTrafficTypeCount increases value for a traffic type
func (m *MMSplitStorage) increaseTrafficTypeCount(trafficType string) {
	m.ttMutex.Lock()
	defer m.ttMutex.Unlock()
	_, exists := m.trafficTypes[trafficType]
	if !exists {
		m.trafficTypes[trafficType] = 1
	} else {
		m.trafficTypes[trafficType]++
	}
}

// decreaseTrafficTypeCount decreases value for a traffic type
func (m *MMSplitStorage) decreaseTrafficTypeCount(trafficType string) {
	m.ttMutex.Lock()
	defer m.ttMutex.Unlock()
	value, exists := m.trafficTypes[trafficType]
	if exists {
		if value > 0 {
			m.trafficTypes[trafficType]--
		} else {
			delete(m.trafficTypes, trafficType)
		}
	}
}

// removeFromFlagSets removes current sets from feature flag
func (m *MMSplitStorage) removeFromFlagSets(name string, sets []string) {
	m.flagSetsMutex.Lock()
	defer m.flagSetsMutex.Unlock()
	for _, flagSet := range sets {
		m.flagSets.RemoveFlagFromSet(flagSet, name)
	}
}

// addToFlagSets add feature flag to flagSets
func (m *MMSplitStorage) addToFlagSets(name string, sets []string) {
	m.flagSetsMutex.Lock()
	defer m.flagSetsMutex.Unlock()
	for _, set := range sets {
		if !m.flagSetFilter.IsPresent(set) {
			continue
		}
		m.flagSets.Add(set, name)
	}
}

// Update atomically registers new splits, removes archived ones and updates the change number
func (m *MMSplitStorage) Update(toAdd []dtos.SplitDTO, toRemove []dtos.SplitDTO, till int64) {
	m.mutex.Lock()
	defer m.mutex.Unlock()
	for _, split := range toAdd {
		existing, thisIsAnUpdate := m.data[split.Name]
		if thisIsAnUpdate {
			// If it's an update, we decrement the traffic type count of the existing split,
			// and then add the updated one (as part of the normal flow), in case it's different.
			m.decreaseTrafficTypeCount(existing.TrafficTypeName)
			m.removeFromFlagSets(existing.Name, existing.Sets)
		}
		m.data[split.Name] = split
		m.increaseTrafficTypeCount(split.TrafficTypeName)
		m.addToFlagSets(split.Name, split.Sets)
	}

	for _, split := range toRemove {
		cached, exists := m.data[split.Name]
		if exists {
			delete(m.data, split.Name)
			m.decreaseTrafficTypeCount(cached.TrafficTypeName)
			m.removeFromFlagSets(cached.Name, cached.Sets)
		}
	}
	m.SetChangeNumber(till)
}

// Remove deletes a split from the in-memory storage
func (m *MMSplitStorage) Remove(splitName string) {
	m.mutex.Lock()
	defer m.mutex.Unlock()
	split, exists := m.data[splitName]
	if exists {
		delete(m.data, splitName)
		m.decreaseTrafficTypeCount(split.TrafficTypeName)
	}
}

// SegmentNames returns a slice with the names of all segments referenced in splits
func (m *MMSplitStorage) SegmentNames() *set.ThreadUnsafeSet {
	segments := set.NewSet()
	m.mutex.RLock()
	defer m.mutex.RUnlock()
	for _, split := range m.data {
		for _, condition := range split.Conditions {
			for _, matcher := range condition.MatcherGroup.Matchers {
				if matcher.UserDefinedSegment != nil {
					segments.Add(matcher.UserDefinedSegment.SegmentName)
				}

			}
		}
	}
	return segments
}

// SetChangeNumber sets the till value belong to split
func (m *MMSplitStorage) SetChangeNumber(till int64) error {
	m.tillMutex.Lock()
	defer m.tillMutex.Unlock()
	m.till = till
	return nil
}

// Split retrieves a split from the MMSplitStorage
// NOTE: A pointer TO A COPY is returned, in order to avoid race conditions between
// evaluations and sdk <-> backend sync
func (m *MMSplitStorage) Split(splitName string) *dtos.SplitDTO {
	m.mutex.RLock()
	defer m.mutex.RUnlock()
	return m._get(splitName)
}

// SplitNames returns a slice with the names of all the current splits
func (m *MMSplitStorage) SplitNames() []string {
	m.mutex.RLock()
	defer m.mutex.RUnlock()
	splitNames := make([]string, 0)
	for key := range m.data {
		splitNames = append(splitNames, key)
	}
	return splitNames
}

// TrafficTypeExists returns true or false depending on existence and counter
// of trafficType
func (m *MMSplitStorage) TrafficTypeExists(trafficType string) bool {
	m.ttMutex.RLock()
	defer m.ttMutex.RUnlock()
	value, exists := m.trafficTypes[trafficType]
	return exists && value > 0
}

// GetNamesByFlagSets grabs all the feature flags linked to the passed sets
func (m *MMSplitStorage) GetNamesByFlagSets(sets []string) map[string][]string {
	toReturn := make(map[string][]string)
	for _, flagSet := range sets {
		toReturn[flagSet] = m.flagSets.FlagsFromSet(flagSet)
	}
	return toReturn
}

var _ storage.SplitStorageConsumer = (*MMSplitStorage)(nil)
var _ storage.SplitStorageProducer = (*MMSplitStorage)(nil)
