package clientfake

import (
	"reflect"

	infra "github.com/tidbcloud/tidb-management-service/api/serverless_infra"
	infraclient "github.com/tidbcloud/tidb-management-service/api/serverless_infra/client"
	infrafake "github.com/tidbcloud/tidb-management-service/api/serverless_infra/client/fake"
	"github.com/tidbcloud/tidb-management-service/api/serverless_infra/meta"
)

type indexKey struct {
	index string
	value any
}

type ClientBuilder struct {
	objDBMap             map[meta.ObjectKey]infra.Object
	objIndexDBMap        map[any][]any
	indexExtractValueMap map[string]func(obj infra.Object) (value, dst any)
	patchFunc            func(set map[string]any, obj infra.Object) error
}

func (f *ClientBuilder) WithIndex(index string, extractValue func(obj infra.Object) (value, dst any)) *ClientBuilder {
	f.indexExtractValueMap[index] = extractValue
	return f
}

func (f *ClientBuilder) WithPatch(patchFunc func(set map[string]any, obj infra.Object) error) *ClientBuilder {
	f.patchFunc = patchFunc
	return f
}

func (f *ClientBuilder) defaultGetFunc() func(obj infra.Object) error {
	return func(obj infra.Object) error {
		objDB := f.objDBMap[meta.ObjectKey{Namespace: obj.GetObjectMeta().Namespace, Name: obj.GetObjectMeta().Name}]
		if objDB == nil {
			return infraclient.ErrNotFound
		}
		objDB = objDB.DeepCopyObject()
		objDBVal, objVal := reflect.ValueOf(objDB), reflect.ValueOf(obj)
		reflect.Indirect(objVal).Set(reflect.Indirect(objDBVal))
		return nil
	}
}

func (f *ClientBuilder) defaultGetIndexFunc() func(value any, index string, dst any) error {
	return func(value any, index string, dst any) error {
		dstVal := reflect.ValueOf(dst)
		if dstVal.Kind() != reflect.Pointer {
			return infraclient.ErrTypeInvalid
		}
		if !dstVal.Elem().IsValid() {
			return infraclient.ErrValueInvalid
		}

		dstDB := f.objIndexDBMap[indexKey{index, value}]
		switch dstVal.Elem().Kind() {
		case reflect.Slice:
			if len(dstDB) > 0 {
				dstElem := reflect.Indirect(dstVal)
				elemType := dstElem.Type().Elem()
				newSlice := reflect.MakeSlice(dstElem.Type(), len(dstDB), len(dstDB))
				for i, v := range dstDB {
					newSlice.Index(i).Set(reflect.ValueOf(v).Convert(elemType))
				}
				dstElem.Set(newSlice)
			}
		default:
			if len(dstDB) == 0 {
				return infraclient.ErrNotFound
			}
			if len(dstDB) > 1 {
				return infraclient.ErrTooMany
			}
			dstDBVal := reflect.ValueOf(dstDB[0])
			reflect.Indirect(dstVal).Set(reflect.Indirect(dstDBVal))
		}
		return nil
	}
}

func (f *ClientBuilder) defaultPatchFunc() func(set map[string]any, obj infra.Object) error {
	return func(set map[string]any, obj infra.Object) error {
		oldObj := obj.DeepCopyObject()
		objectMeta := obj.GetObjectMeta()
		objKey := meta.ObjectKey{Namespace: objectMeta.Namespace, Name: objectMeta.Name}
		if _, ok := f.objDBMap[objKey]; !ok {
			return infraclient.ErrNotFound
		}

		if err := f.patchFunc(set, obj); err != nil {
			return err
		}
		objectMeta.ResourceVersion++
		objectMeta.UpdateTimestamp = meta.Now()
		f.objDBMap[objKey] = obj.DeepCopyObject()
		for index, extractValue := range f.indexExtractValueMap {
			newDst := make([]any, 0)
			value, dst := extractValue(oldObj)
			for _, dstDB := range f.objIndexDBMap[indexKey{index, value}] {
				if dstDB != dst {
					newDst = append(newDst, dstDB)
				}
			}
			if len(newDst) > 0 {
				f.objIndexDBMap[indexKey{index, value}] = newDst
			} else {
				delete(f.objIndexDBMap, indexKey{index, value})
			}
		}
		for index, extractValue := range f.indexExtractValueMap {
			value, dst := extractValue(obj)
			f.objIndexDBMap[indexKey{index, value}] = append(f.objIndexDBMap[indexKey{index, value}], dst)
		}
		return nil
	}
}

func (f *ClientBuilder) defaultCreateFunc() func(obj infra.Object) error {
	return func(obj infra.Object) error {
		objectMeta := obj.GetObjectMeta()
		objKey := meta.ObjectKey{Namespace: objectMeta.Namespace, Name: objectMeta.Name}
		if _, ok := f.objDBMap[objKey]; ok {
			return infraclient.ErrAlreadyExists
		}
		objectMeta.ResourceVersion = 1
		objectMeta.UpdateTimestamp = meta.Now()
		if objectMeta.CreationTimestamp.IsZero() {
			objectMeta.CreationTimestamp = objectMeta.UpdateTimestamp
		}
		f.objDBMap[objKey] = obj.DeepCopyObject()

		for index, extractValue := range f.indexExtractValueMap {
			value, dst := extractValue(obj)
			f.objIndexDBMap[indexKey{index, value}] = append(f.objIndexDBMap[indexKey{index, value}], dst)
		}
		return nil
	}
}

func (f *ClientBuilder) defaultUpdateFunc() func(obj infra.Object) error {
	return func(obj infra.Object) error {
		objectMeta := obj.GetObjectMeta()
		objKey := meta.ObjectKey{Namespace: objectMeta.Namespace, Name: objectMeta.Name}
		oldObj, ok := f.objDBMap[objKey]
		if !ok {
			return infraclient.ErrConflict // same as underlay implement
		}

		objectMeta.ResourceVersion++
		objectMeta.UpdateTimestamp = meta.Now()
		if objectMeta.CreationTimestamp.IsZero() {
			objectMeta.CreationTimestamp = objectMeta.UpdateTimestamp
		}
		f.objDBMap[objKey] = obj.DeepCopyObject()
		for index, extractValue := range f.indexExtractValueMap {
			newDst := make([]any, 0)
			value, dst := extractValue(oldObj)
			for _, dstDB := range f.objIndexDBMap[indexKey{index, value}] {
				if dstDB != dst {
					newDst = append(newDst, dstDB)
				}
			}
			if len(newDst) > 0 {
				f.objIndexDBMap[indexKey{index, value}] = newDst
			} else {
				delete(f.objIndexDBMap, indexKey{index, value})
			}
		}
		for index, extractValue := range f.indexExtractValueMap {
			value, dst := extractValue(obj)
			f.objIndexDBMap[indexKey{index, value}] = append(f.objIndexDBMap[indexKey{index, value}], dst)
		}
		return nil
	}
}

func (f *ClientBuilder) defaultDeleteFunc() func(obj infra.Object, force bool) error {
	return func(obj infra.Object, force bool) error {
		objectMeta := obj.GetObjectMeta()
		if force {
			if objectMeta.DeletionTimestamp == nil {
				return infraclient.ErrDeleteCond
			}
			delete(f.objDBMap, meta.ObjectKey{Namespace: objectMeta.Namespace, Name: objectMeta.Name})

			for index, extractValue := range f.indexExtractValueMap {
				newDst := make([]any, 0)
				value, dst := extractValue(obj)
				for _, dstDB := range f.objIndexDBMap[indexKey{index, value}] {
					if dstDB != dst {
						newDst = append(newDst, dstDB)
					}
				}
				if len(newDst) > 0 {
					f.objIndexDBMap[indexKey{index, value}] = newDst
				} else {
					delete(f.objIndexDBMap, indexKey{index, value})
				}
			}
			return nil
		}

		if objectMeta.DeletionTimestamp != nil {
			return nil
		}
		objVal := reflect.ValueOf(obj)
		if objVal.Kind() != reflect.Pointer {
			return infraclient.ErrTypeInvalid
		}
		if !objVal.Elem().IsValid() {
			return infraclient.ErrValueInvalid
		}
		if _, ok := f.objDBMap[meta.ObjectKey{Namespace: objectMeta.Namespace, Name: objectMeta.Name}]; ok {
			now := meta.Now()
			objectMeta.ResourceVersion++
			objectMeta.UpdateTimestamp = now
			objectMeta.DeletionTimestamp = &now
			f.objDBMap[meta.ObjectKey{Namespace: objectMeta.Namespace, Name: objectMeta.Name}] = obj.DeepCopyObject()
		}
		return nil
	}
}

// Build builds and returns a new fake client.
func (f *ClientBuilder) Build() infraclient.Client {
	for _, obj := range f.objDBMap {
		for index, extractValue := range f.indexExtractValueMap {
			value, dst := extractValue(obj)
			f.objIndexDBMap[indexKey{index, value}] = append(f.objIndexDBMap[indexKey{index, value}], dst)
		}
	}

	return &infrafake.Client{
		GetFunc:      f.defaultGetFunc(),
		GetIndexFunc: f.defaultGetIndexFunc(),
		PatchFunc:    f.defaultPatchFunc(),
		CreateFunc:   f.defaultCreateFunc(),
		UpdateFunc:   f.defaultUpdateFunc(),
		DeleteFunc:   f.defaultDeleteFunc(),
	}
}

func NewInfraClient(infraObjs ...infra.Object) *ClientBuilder {
	objDBMap := make(map[meta.ObjectKey]infra.Object, len(infraObjs))
	for _, obj := range infraObjs {
		objDBMap[meta.ObjectKey{Namespace: obj.GetObjectMeta().Namespace, Name: obj.GetObjectMeta().Name}] = obj
	}
	return &ClientBuilder{
		objDBMap:             objDBMap,
		objIndexDBMap:        map[any][]any{},
		indexExtractValueMap: map[string]func(obj infra.Object) (value, dst any){},
	}
}
