package plugin

import (
	"sync"

	"github.com/pkg/errors"
)

// clientKey is a unique ID for a plugin client.
type clientKey struct {
	kind PluginKind

	// scope is an additional identifier that allows multiple clients
	// for the same kind/name to be differentiated. It will typically
	// be the name of the applicable backup/restore for ItemAction
	// clients, and blank for Object/BlockStore clients.
	scope string
}

func newClientStore() *clientStore {
	return &clientStore{
		clients: make(map[clientKey]map[string]pluginClient),
		lock:    &sync.RWMutex{},
	}
}

// clientStore is a repository of active plugin clients.
type clientStore struct {
	// clients is a nested map, keyed first by clientKey (a
	// combo of kind and "scope"), and second by plugin name.
	// This enables easy listing of all clients for a given
	// kind and scope (e.g. all BackupItemActions for a given
	// backup), and efficient lookup by kind+name+scope (e.g.
	// the AWS ObjectStore.)
	clients map[clientKey]map[string]pluginClient
	lock    *sync.RWMutex
}

// get returns a plugin client for the given kind/name/scope, or an error if none
// is found.
func (s *clientStore) get(kind PluginKind, name, scope string) (pluginClient, error) {
	s.lock.RLock()
	defer s.lock.RUnlock()

	if forScope, found := s.clients[clientKey{kind, scope}]; found {
		if client, found := forScope[name]; found {
			return client, nil
		}
	}

	return nil, errors.New("client not found")
}

// list returns all plugin clients for the given kind/scope, or an
// error if none are found.
func (s *clientStore) list(kind PluginKind, scope string) ([]pluginClient, error) {
	s.lock.RLock()
	defer s.lock.RUnlock()

	if forScope, found := s.clients[clientKey{kind, scope}]; found {
		var clients []pluginClient

		for _, client := range forScope {
			clients = append(clients, client)
		}

		return clients, nil
	}

	return nil, errors.New("clients not found")
}

// listAll returns all plugin clients for all kinds/scopes, or a
// zero-valued slice if there are none.
func (s *clientStore) listAll() []pluginClient {
	s.lock.RLock()
	defer s.lock.RUnlock()

	var clients []pluginClient
	for _, pluginsByName := range s.clients {
		for name := range pluginsByName {
			clients = append(clients, pluginsByName[name])
		}
	}

	return clients
}

// add stores a plugin client for the given kind/name/scope.
func (s *clientStore) add(client pluginClient, kind PluginKind, name, scope string) {
	s.lock.Lock()
	defer s.lock.Unlock()

	key := clientKey{kind, scope}

	if _, found := s.clients[key]; !found {
		s.clients[key] = make(map[string]pluginClient)
	}

	s.clients[key][name] = client
}

// delete removes the client with the given kind/name/scope from the store.
func (s *clientStore) delete(kind PluginKind, name, scope string) {
	s.lock.Lock()
	defer s.lock.Unlock()

	if forScope, found := s.clients[clientKey{kind, scope}]; found {
		delete(forScope, name)
	}
}

// deleteAll removes all clients with the given kind/scope from
// the store.
func (s *clientStore) deleteAll(kind PluginKind, scope string) {
	s.lock.Lock()
	defer s.lock.Unlock()

	delete(s.clients, clientKey{kind, scope})
}

// clear removes all clients for all kinds/scopes from the store.
func (s *clientStore) clear() {
	s.lock.Lock()
	defer s.lock.Unlock()

	s.clients = make(map[clientKey]map[string]pluginClient)
}
