package dataplane

import (
	"context"
	"sync"

	"helm.sh/helm/v3/pkg/action"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/util/sets"
	"k8s.io/client-go/rest"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

type manager struct {
	lock sync.RWMutex
	// host to client
	cache map[string]cacheItem

	// name to host
	nameToHost  map[string]string
	hostToNames map[string]sets.Set[string]

	ctx context.Context

	s *runtime.Scheme
}

type cacheItem struct {
	cancelFn context.CancelFunc
	client   client.Client
	cfg      *action.Configuration
	restCfg  *rest.Config
}

// NewK8sClientManager creates a new K8sClientManager.
// Note: it's a low level manager, you should not use it directly in most cases.
// Instead, you should use EnvProviderManager to get EnvProvider and then get Client from it.
// The EnvProviderManager is a higher level manager which include the Dry run feature and
// is more user friendly for development.
func NewK8sClientManager(s *runtime.Scheme) K8sClientManager {
	return &manager{
		cache:       make(map[string]cacheItem),
		s:           s,
		nameToHost:  map[string]string{},
		hostToNames: map[string]sets.Set[string]{},
		ctx:         context.Background(),
	}
}

func (m *manager) IsRegistered(name string) bool {
	_, ok := m.getCacheItem(name)
	return ok
}

func (m *manager) Register(name string, cfg *rest.Config) error {
	m.lock.Lock()
	defer m.lock.Unlock()

	item, ok := m.cache[cfg.Host]
	if ok {
		m.update(name, cfg.Host, nil)
		return nil
	}

	ctx, cancel := context.WithCancel(m.ctx)
	c, err := newClient(ctx, cfg, m.s)
	if err != nil {
		cancel()
		return err
	}
	helmCfg, err := newHelmConfig(cfg)
	if err != nil {
		cancel()
		return err
	}

	item.client = c
	item.cancelFn = cancel
	item.cfg = helmCfg
	item.restCfg = cfg

	m.update(name, cfg.Host, &item)

	return nil
}

func (m *manager) Deregister(name string) {
	m.lock.Lock()
	defer m.lock.Unlock()

	oldHost, ok := m.nameToHost[name]
	if !ok {
		return
	}
	m.delete(name, oldHost)
}

func (m *manager) GetClient(name string) client.Client {
	item, ok := m.getCacheItem(name)
	if !ok {
		panic("client must be registered before get: " + name)
	}

	return item.client
}

func (m *manager) GetRestConfig(name string) *rest.Config {
	item, ok := m.getCacheItem(name)
	if !ok {
		panic("cfg must be registered before get: " + name)
	}

	return item.restCfg
}

func (m *manager) GetHelmConfig(name string) *action.Configuration {
	item, ok := m.getCacheItem(name)
	if !ok {
		panic("cfg must be registered before get: " + name)
	}

	return item.cfg
}

func (m *manager) getCacheItem(name string) (*cacheItem, bool) {
	m.lock.RLock()
	defer m.lock.RUnlock()
	host, ok := m.nameToHost[name]
	if !ok {
		return nil, false
	}

	item, ok := m.cache[host]
	return &item, ok
}

func (m *manager) update(name, host string, item *cacheItem) {
	oldHost, ok := m.nameToHost[name]
	if ok && oldHost != host {
		m.delete(name, oldHost)
	}

	m.nameToHost[name] = host
	s, ok := m.hostToNames[host]
	if !ok {
		s = sets.New[string]()
	}
	s.Insert(name)
	m.hostToNames[host] = s

	if item != nil {
		m.cache[host] = *item
	}
}

func (m *manager) delete(name, host string) {
	s, ok := m.hostToNames[host]
	if ok {
		s = s.Delete(name)
	}
	if s.Len() == 0 {
		delete(m.hostToNames, host)
		item, ok := m.cache[host]
		if ok {
			// call cancel fn to stop client cache
			item.cancelFn()
		}
		delete(m.cache, host)
	} else {
		m.hostToNames[host] = s
	}

}
