package lock

import (
	"context"
	"errors"
	"cache"
	"time"

	"github.com/go-redis/redis/v8"
	"github.com/google/uuid"
)

const (
	DefaultExpirationTime = 5 * time.Second
	RenewInterval         = DefaultExpirationTime / 2 // 续租间隔为锁过期时间的一半
	LuaCheckAndDelKey     = `
	if(redis.call('get',KEYS[1])==ARGV[1]) then
		return redis.call('del',KEYS[1])
	else
		return 0
	end
`
	LuaCheckAndRenewKey = `
	if(redis.call('get',KEYS[1])==ARGV[1]) then
		return redis.call('expire',KEYS[1],ARGV[2])
	else
		return 0
	end
`
)

type RedisLock struct {
	key        string
	val        string
	expiration time.Duration
	cli        *redis.Client
	script     *redis.Script  // 用于解锁的 Lua 脚本
	renewScript *redis.Script // 用于续租的 Lua 脚本
	stopChan   chan struct{}  // 用于停止续租 goroutine
}

func NewRedisLock(key string) *RedisLock {
	val := uuid.New().String()

	return &RedisLock{
		key:        key,
		val:        val,
		expiration: DefaultExpirationTime,
		cli:        cache.RedisV8Client,
		script:     redis.NewScript(LuaCheckAndDelKey),
		renewScript: redis.NewScript(LuaCheckAndRenewKey),
		stopChan:   make(chan struct{}),
	}
}

func (r *RedisLock) Lock(ctx context.Context) (bool, error) {
	success, err := r.cli.SetNX(ctx, r.key, r.val, r.expiration).Result()
	if err != nil {
		return false, err
	}
	if success {
		go r.startRenewal(ctx) // 启动续租 goroutine
	}
	return success, nil
}

func (r *RedisLock) Unlock(ctx context.Context) error {
	close(r.stopChan) // 停止续租 goroutine

	res, err := r.script.Run(ctx, r.cli, []string{r.key}, r.val).Int64()
	if err != nil {
		return err
	}
	if res != 1 {
		return errors.New("unlock failed: the lock has been lost or the value does not match")
	}
	return nil
}

// 启动续租 goroutine
func (r *RedisLock) startRenewal(ctx context.Context) {
	ticker := time.NewTicker(RenewInterval)
	defer ticker.Stop()

	for {
		select {
		case <-ticker.C:
			// 使用 Lua 脚本原子化检查锁是否持有并续租
			res, err := r.renewScript.Run(ctx, r.cli, []string{r.key}, r.val, int(r.expiration/time.Second)).Result()
			if err != nil || res == 0 {
				return // 锁已丢失或续租失败,停止续租,打印错误日志
			}
		case <-r.stopChan:
			return // 收到停止信号,退出续租 goroutine
		}
	}
}