package entity

import (
	"fmt"
	"time"

	"github.com/avast/retry-go/v4"
	"github.com/tidbcloud/pkgv2/bizerr"
	"gorm.io/gorm"
)

/*
The Versioning mechanism implement the Compare-and-swap which refer to https://en.wikipedia.org/wiki/Compare-and-swap
COPY from wikipedia
	In computer science, compare-and-swap (CAS) is an atomic instruction used in multithreading to achieve synchronization.
	It compares the contents of a memory location with a given value and, only if they are the same, modifies the contents
	of that memory location to a new given value. This is done as a single atomic operation. The atomicity guarantees that
	the new value is calculated based on up-to-date information.
END of COPY

We provide two kinds of function here, and they will be used in different scenarios.
1.SaveWithVersioning for simple CAS
2.RetrySaveWithVersioning for CAS auto-retry

Scenario 1 - usage of SaveWithVersioning
If a field is calculated by other fields in same record, if CAS failed, you should re-calculated by latest record, so
you should use SaveWithVersioning inside. The pseudo-code is as follows
```
func RestartCalculation() {
	for {
		latest := getLatest()
		newRecord := calculateBy(latest)
		err := SaveWithVersioning(wdb, newRecord)
		if err == nil {
			break
		}
		log.Warn(err, "update xxx error")
	}
}
```

Scenario 2 - usage of RetrySaveWithVersioning
If a field is not calculated by other fields in same record，latest record and old record are not different for the field
calculation. Just CAS retry directly to reduce noise and make sure not override latest changes of other fields.
The pseudo-code is as follows
```
func RetryDirectly() error{
	latest := getLatest()
	newRecord := calculateBy(latest)
	err :=  RetrySaveWithVersioning(wdb, newRecord)
	log.Error(err, "update xxx error")
	return err
}
```
*/

var (
	defaultRetryTimes    uint = 3
	defaultRetryInterval      = 100 * time.Millisecond
)

func SaveWithVersioning(wdb *gorm.DB, value interface{}) bizerr.BizError {
	dst, ok := value.(IdAndVersioning)
	if !ok {
		return bizerr.WrapGormError(wdb.Save(value).Error, "save to db")
	}

	versioning := dst.GetVersioning()
	if dst.GetID() == 0 { // new record try to create
		return bizerr.WrapGormError(wdb.Create(value).Error, "create to db")
	} else { // try to update
		result := wdb.Model(value).Select("*").Where("versioning=?", versioning).Updates(value)
		if err := result.Error; err != nil {
			return bizerr.WrapGormError(err, "update to db")
		}
		if result.RowsAffected == 0 {
			return bizerr.New(bizerr.ErrCodeOptimisticLockConflict, "version conflict,current=%d", versioning)
		}
		return nil
	}
}

func DefaultRetrySaveWithVersioning(wdb *gorm.DB,
	query func(wdb *gorm.DB) (value interface{}, err error),
	update func(latest interface{}) (updated interface{}, err error),
) error {
	return RetrySaveWithVersioning(wdb, query, update, defaultRetryTimes, defaultRetryInterval)
}

func RetrySaveWithVersioning(wdb *gorm.DB,
	query func(wdb *gorm.DB) (value interface{}, err error),
	update func(latest interface{}) (updated interface{}, err error),
	retryTimes uint,
	retryInterval time.Duration,
) error {
	return retry.Do(func() error { return updateLatest(wdb, query, update) },
		retry.DelayType(retry.FixedDelay),
		retry.Delay(retryInterval),
		retry.Attempts(retryTimes),
		retry.LastErrorOnly(true))
}

func updateLatest(wdb *gorm.DB,
	query func(wdb *gorm.DB) (value interface{}, err error),
	update func(latest interface{}) (updated interface{}, err error),
) error {
	latest, err := query(wdb)
	if err != nil {
		return fmt.Errorf("query error: %w", err)
	}
	if latest == nil {
		return fmt.Errorf("query get nil record")
	}
	val, err := update(latest)
	if err != nil {
		return fmt.Errorf("update error: %w", err)
	}
	if val == nil {
		return nil
	}
	err = SaveWithVersioning(wdb, val)
	if err != nil {
		return fmt.Errorf("save error: %w", err)
	}
	return nil
}
