package incrversion

import (
	"context"
	"reflect"
	"sort"

	"github.com/openimsdk/openim-sdk-core/v3/pkg/db/db_interface"
	"github.com/openimsdk/openim-sdk-core/v3/pkg/db/model_struct"
	"github.com/openimsdk/tools/errs"
	"github.com/openimsdk/tools/log"
	"github.com/openimsdk/tools/utils/datautil"
	"gorm.io/gorm"
)

type VersionSynchronizer[V, R any] struct {
	Ctx                context.Context
	DB                 db_interface.VersionSyncModel
	TableName          string
	EntityID           string
	Key                func(V) string
	Local              func() ([]V, error)
	ServerVersion      func() R
	Server             func(version *model_struct.LocalVersionSync) (R, error)
	Full               func(resp R) bool
	Version            func(resp R) (string, uint64)
	Delete             func(resp R) []string
	Update             func(resp R) []V
	Insert             func(resp R) []V
	ExtraData          func(resp R) any
	ExtraDataProcessor func(ctx context.Context, data any) error
	Syncer             func(server, local []V) error
	FullSyncer         func(ctx context.Context) error
	FullID             func(ctx context.Context) ([]string, error)
}

func (o *VersionSynchronizer[V, R]) getVersionInfo() (*model_struct.LocalVersionSync, error) {
	versionInfo, err := o.DB.GetVersionSync(o.Ctx, o.TableName, o.EntityID)
	if err != nil && errs.Unwrap(err) != gorm.ErrRecordNotFound {
		log.ZWarn(o.Ctx, "get version info", err)
		return nil, err

	}
	return versionInfo, nil
}

func (o *VersionSynchronizer[V, R]) updateVersionInfo(lvs *model_struct.LocalVersionSync, resp R) error {
	lvs.Table = o.TableName
	lvs.EntityID = o.EntityID
	lvs.VersionID, lvs.Version = o.Version(resp)
	return o.DB.SetVersionSync(o.Ctx, lvs)
}
func judgeInterfaceIsNil(data any) bool {
	return reflect.ValueOf(data).Kind() == reflect.Ptr && reflect.ValueOf(data).IsNil()
}

func (o *VersionSynchronizer[V, R]) Sync() error {
	var lvs *model_struct.LocalVersionSync
	var resp R
	var extraData any
	if o.ServerVersion == nil {
		var err error
		lvs, err = o.getVersionInfo()
		if err != nil {
			return err
		}
		resp, err = o.Server(lvs)
		if err != nil {
			return err
		}
	} else {
		var err error
		lvs, err = o.getVersionInfo()
		if err != nil {
			return err
		}
		resp = o.ServerVersion()
	}
	delIDs := o.Delete(resp)
	changes := o.Update(resp)
	insert := o.Insert(resp)
	if o.ExtraData != nil {
		temp := o.ExtraData(resp)
		if !judgeInterfaceIsNil(temp) {
			extraData = temp
		}
	}
	if len(delIDs) == 0 && len(changes) == 0 && len(insert) == 0 && !o.Full(resp) && extraData == nil {
		log.ZDebug(o.Ctx, "no data to sync", "table", o.TableName, "entityID", o.EntityID)
		return nil
	}

	if o.Full(resp) {
		err := o.FullSyncer(o.Ctx)
		if err != nil {
			return err
		}
		lvs.UIDList, err = o.FullID(o.Ctx)
		if err != nil {
			return err
		}
	} else {
		if len(delIDs) > 0 {
			lvs.UIDList = DeleteElements(lvs.UIDList, delIDs)
		}
		if len(insert) > 0 {
			lvs.UIDList = append(lvs.UIDList, datautil.Slice(insert, o.Key)...)

		}
		local, err := o.Local()
		if err != nil {
			return err
		}
		kv := datautil.SliceToMapAny(local, func(v V) (string, V) {
			return o.Key(v), v
		})

		changes = append(changes, insert...)

		for i, change := range changes {
			key := o.Key(change)
			kv[key] = changes[i]
		}

		for _, id := range delIDs {
			delete(kv, id)
		}
		server := datautil.Values(kv)
		if err := o.Syncer(server, local); err != nil {
			return err
		}
		if extraData != nil && o.ExtraDataProcessor != nil {
			if err := o.ExtraDataProcessor(o.Ctx, extraData); err != nil {
				return err
			}

		}
	}
	return o.updateVersionInfo(lvs, resp)
}

func (o *VersionSynchronizer[V, R]) CheckVersionSync() error {
	lvs, err := o.getVersionInfo()
	if err != nil {
		return err
	}
	var extraData any
	resp := o.ServerVersion()
	delIDs := o.Delete(resp)
	changes := o.Update(resp)
	insert := o.Insert(resp)
	versionID, version := o.Version(resp)
	if o.ExtraData != nil {
		temp := o.ExtraData(resp)
		if !judgeInterfaceIsNil(temp) {
			extraData = temp
		}
	}
	if len(delIDs) == 0 && len(changes) == 0 && len(insert) == 0 && !o.Full(resp) && extraData == nil {
		log.ZWarn(o.Ctx, "exception no data to sync", errs.New("notification no data"), "table", o.TableName, "entityID", o.EntityID)
		return nil
	}
	log.ZDebug(o.Ctx, "check version sync", "table", o.TableName, "entityID", o.EntityID, "versionID", versionID, "localVersionID", lvs.VersionID, "version", version, "localVersion", lvs.Version)
	/// If the version unique ID cannot correspond with the local version,
	// it indicates that the data might have been tampered with or an exception has occurred.
	//Trigger the complete client-server incremental synchronization.
	if versionID != lvs.VersionID {
		log.ZDebug(o.Ctx, "version id not match", errs.New("version id not match"), "versionID", versionID, "localVersionID", lvs.VersionID)
		o.ServerVersion = nil
		return o.Sync()
	}
	if lvs.Version+1 == version {
		if len(delIDs) > 0 {
			lvs.UIDList = DeleteElements(lvs.UIDList, delIDs)
		}
		if len(insert) > 0 {
			lvs.UIDList = append(lvs.UIDList, datautil.Slice(insert, o.Key)...)

		}
		local, err := o.Local()
		if err != nil {
			return err
		}
		kv := datautil.SliceToMapAny(local, func(v V) (string, V) {
			return o.Key(v), v
		})
		changes = append(changes, insert...)
		for i, change := range changes {
			key := o.Key(change)
			kv[key] = changes[i]
		}

		for _, id := range delIDs {
			delete(kv, id)
		}
		server := datautil.Values(kv)
		if err := o.Syncer(server, local); err != nil {
			return err
		}
		if extraData != nil && o.ExtraDataProcessor != nil {
			if err := o.ExtraDataProcessor(o.Ctx, extraData); err != nil {
				return err
			}

		}
		return o.updateVersionInfo(lvs, resp)
	} else if version <= lvs.Version {
		log.ZWarn(o.Ctx, "version less than local version", errs.New("version less than local version"),
			"table", o.TableName, "entityID", o.EntityID, "version", version, "localVersion", lvs.Version)
		return nil
	} else {
		// If the version number has a gap with the local version number,
		//it indicates that some pushed data might be missing.
		//Trigger the complete client-server incremental synchronization.
		o.ServerVersion = nil
		return o.Sync()
	}
}

// DeleteElements 删除切片中包含在另一个切片中的元素，并保持切片顺序
func DeleteElements[E comparable](es []E, toDelete []E) []E {
	// 将要删除的元素存储在哈希集合中
	deleteSet := make(map[E]struct{}, len(toDelete))
	for _, e := range toDelete {
		deleteSet[e] = struct{}{}
	}

	// 通过一个索引 j 来跟踪新的切片位置
	j := 0
	for _, e := range es {
		if _, found := deleteSet[e]; !found {
			es[j] = e
			j++
		}
	}
	return es[:j]
}

// DeleteElement 删除切片中的指定元素，并保持切片顺序
func DeleteElement[E comparable](es []E, element E) []E {
	j := 0
	for _, e := range es {
		if e != element {
			es[j] = e
			j++
		}
	}
	return es[:j]
}

// Slice Converts slice types in batches and sorts the resulting slice using a custom comparator
func Slice[E any, T any](es []E, fn func(e E) T, less func(a, b T) bool) []T {
	// 转换切片
	v := make([]T, len(es))
	for i := 0; i < len(es); i++ {
		v[i] = fn(es[i])
	}

	// 排序切片
	sort.Slice(v, func(i, j int) bool {
		return less(v[i], v[j])
	})

	return v
}
