package memory

import (
	"errors"
	"fmt"
	"github.com/apache/dubbo-kubernetes/pkg/config"
	"github.com/apache/dubbo-kubernetes/pkg/config/schema/collection"
	"github.com/apache/dubbo-kubernetes/sail/pkg/model"
	"sync"
	"time"
)

var (
	errNotFound      = errors.New("item not found")
	errAlreadyExists = errors.New("item already exists")
	// TODO: can we make this compatible with kerror.IsConflict without imports the library?
	errConflict = errors.New("conflicting resource version, try again")
)

// Make creates an in-memory config store from a config schemas
// It is with validation
func Make(schemas collection.Schemas) model.ConfigStore {
	return newStore(schemas, false)
}

type store struct {
	schemas        collection.Schemas
	data           map[config.GroupVersionKind]map[string]map[string]any
	skipValidation bool
	mutex          sync.RWMutex
}

func newStore(schemas collection.Schemas, skipValidation bool) model.ConfigStore {
	out := store{
		schemas:        schemas,
		data:           make(map[config.GroupVersionKind]map[string]map[string]any),
		skipValidation: skipValidation,
	}
	for _, s := range schemas.All() {
		out.data[s.GroupVersionKind()] = make(map[string]map[string]any)
	}
	return &out
}

func (cr *store) Schemas() collection.Schemas {
	return cr.schemas
}
func (cr *store) Get(kind config.GroupVersionKind, name, namespace string) *config.Config {
	cr.mutex.RLock()
	defer cr.mutex.RUnlock()
	_, ok := cr.data[kind]
	if !ok {
		return nil
	}

	ns, exists := cr.data[kind][namespace]
	if !exists {
		return nil
	}

	out, exists := ns[name]
	if !exists {
		return nil
	}
	config := out.(config.Config)

	return &config
}

func (cr *store) List(kind config.GroupVersionKind, namespace string) []config.Config {
	cr.mutex.RLock()
	defer cr.mutex.RUnlock()
	data, exists := cr.data[kind]
	if !exists {
		return nil
	}

	var size int
	if namespace == "" {
		for _, ns := range data {
			size += len(ns)
		}
	} else {
		size = len(data[namespace])
	}

	out := make([]config.Config, 0, size)
	if namespace == "" {
		for _, ns := range data {
			for _, value := range ns {
				out = append(out, value.(config.Config))
			}
		}
	} else {
		ns, exists := data[namespace]
		if !exists {
			return nil
		}
		for _, value := range ns {
			out = append(out, value.(config.Config))
		}
	}
	return out
}

func (cr *store) Delete(kind config.GroupVersionKind, name, namespace string, resourceVersion *string) error {
	cr.mutex.Lock()
	defer cr.mutex.Unlock()
	data, ok := cr.data[kind]
	if !ok {
		return fmt.Errorf("unknown type %v", kind)
	}
	ns, exists := data[namespace]
	if !exists {
		return errNotFound
	}

	_, exists = ns[name]
	if !exists {
		return errNotFound
	}

	delete(ns, name)
	return nil
}

func (cr *store) Create(cfg config.Config) (string, error) {
	cr.mutex.Lock()
	defer cr.mutex.Unlock()
	kind := cfg.GroupVersionKind
	s, ok := cr.schemas.FindByGroupVersionKind(kind)
	if !ok {
		return "", fmt.Errorf("unknown type %v", kind)
	}
	if !cr.skipValidation {
		if _, err := s.ValidateConfig(cfg); err != nil {
			return "", err
		}
	}
	ns, exists := cr.data[kind][cfg.Namespace]
	if !exists {
		ns = map[string]any{}
		cr.data[kind][cfg.Namespace] = ns
	}

	_, exists = ns[cfg.Name]

	if !exists {
		tnow := time.Now()
		if cfg.ResourceVersion == "" {
			cfg.ResourceVersion = tnow.String()
		}
		// Set the creation timestamp, if not provided.
		if cfg.CreationTimestamp.IsZero() {
			cfg.CreationTimestamp = tnow
		}

		ns[cfg.Name] = cfg
		return cfg.ResourceVersion, nil
	}
	return "", errAlreadyExists
}

func (cr *store) Update(cfg config.Config) (string, error) {
	cr.mutex.Lock()
	defer cr.mutex.Unlock()
	kind := cfg.GroupVersionKind
	s, ok := cr.schemas.FindByGroupVersionKind(kind)
	if !ok {
		return "", fmt.Errorf("unknown type %v", kind)
	}
	if !cr.skipValidation {
		if _, err := s.ValidateConfig(cfg); err != nil {
			return "", err
		}
	}

	ns, exists := cr.data[kind][cfg.Namespace]
	if !exists {
		return "", errNotFound
	}

	existing, exists := ns[cfg.Name]
	if !exists {
		return "", errNotFound
	}
	if hasConflict(existing.(config.Config), cfg) {
		return "", errConflict
	}
	// TODO Annotations[ResourceVersion]
	if cfg.Annotations != nil && cfg.Annotations[""] != "" {
		cfg.ResourceVersion = cfg.Annotations[""]
		delete(cfg.Annotations, "")
	} else {
		cfg.ResourceVersion = time.Now().String()
	}

	ns[cfg.Name] = cfg
	return cfg.ResourceVersion, nil
}

func (cr *store) UpdateStatus(cfg config.Config) (string, error) {
	return cr.Update(cfg)
}

func (cr *store) Patch(orig config.Config, patchFn config.PatchFunc) (string, error) {
	cr.mutex.Lock()
	defer cr.mutex.Unlock()

	gvk := orig.GroupVersionKind
	s, ok := cr.schemas.FindByGroupVersionKind(gvk)
	if !ok {
		return "", fmt.Errorf("unknown type %v", gvk)
	}

	cfg, _ := patchFn(orig)
	if !cr.skipValidation {
		if _, err := s.ValidateConfig(cfg); err != nil {
			return "", err
		}
	}

	_, ok = cr.data[gvk]
	if !ok {
		return "", errNotFound
	}
	ns, exists := cr.data[gvk][orig.Namespace]
	if !exists {
		return "", errNotFound
	}

	rev := time.Now().String()
	cfg.ResourceVersion = rev
	ns[cfg.Name] = cfg

	return rev, nil
}

// hasConflict checks if the two resources have a conflict, which will block Update calls
func hasConflict(existing, replacement config.Config) bool {
	if replacement.ResourceVersion == "" {
		// We don't care about resource version, so just always overwrite
		return false
	}
	// We set a resource version but its not matched, it is a conflict
	if replacement.ResourceVersion != existing.ResourceVersion {
		return true
	}
	return false
}
