// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.

package redis

import (
	"context"
	"time"

	"github.com/gogf/gf/v2/container/gvar"
	"github.com/gogf/gf/v2/database/gredis"
	"github.com/gogf/gf/v2/os/gtime"
	"github.com/gogf/gf/v2/util/gconv"
)

// GroupGeneric provides generic functions of redis.
type GroupGeneric struct {
	Operation gredis.AdapterOperation
}

// GroupGeneric creates and returns GroupGeneric.
func (r *Redis) GroupGeneric() gredis.IGroupGeneric {
	return GroupGeneric{
		Operation: r.AdapterOperation,
	}
}

// Copy copies the value stored at the source key to the destination key.
//
// By default, the destination key is created in the logical database used by the connection.
// The DB option allows specifying an alternative logical database index for the destination key.
//
// The command returns an error when the destination key already exists.
//
// It returns:
// - 1 if source was copied.
// - 0 if source was not copied.
//
// https://redis.io/commands/copy/
func (r GroupGeneric) Copy(ctx context.Context, source, destination string, option ...gredis.CopyOption) (int64, error) {
	var usedOption interface{}
	if len(option) > 0 {
		usedOption = option[0]
	}
	v, err := r.Operation.Do(ctx, "Copy", mustMergeOptionToArgs(
		[]interface{}{source, destination}, usedOption,
	)...)
	return v.Int64(), err
}

// Exists returns if key exists.
// The user should be aware that if the same existing key is mentioned in the arguments multiple times,
// it will be counted multiple times.
//
// It returns the number of keys that exist from those specified as arguments.
//
// https://redis.io/commands/exists/
func (r GroupGeneric) Exists(ctx context.Context, keys ...string) (int64, error) {
	v, err := r.Operation.Do(ctx, "Exists", gconv.Interfaces(keys)...)
	return v.Int64(), err
}

// Type returns the string representation of the type of the value stored at key.
// The different types that can be returned are: string, list, set, zset, hash and stream.
//
// It returns type of key, or none when key does not exist.
//
// https://redis.io/commands/type/
func (r GroupGeneric) Type(ctx context.Context, key string) (string, error) {
	v, err := r.Operation.Do(ctx, "Type", key)
	return v.String(), err
}

// Unlink is very similar to DEL: it removes the specified keys. Just like DEL a key is ignored if it does not exist.
// However, the command performs the actual memory reclaiming in a different thread, so it is not blocking, while DEL is.
// This is where the command name comes from: the command just unlinks the keys from the keyspace.
// The actual removal will happen later asynchronously.
//
// It returns the number of keys that were unlinked.
//
// https://redis.io/commands/unlink/
func (r GroupGeneric) Unlink(ctx context.Context, keys ...string) (int64, error) {
	v, err := r.Operation.Do(ctx, "Unlink", gconv.Interfaces(keys)...)
	return v.Int64(), err
}

// Rename renames key to newKey. It returns an error when key does not exist.
// If newKey already exists it is overwritten, when this happens RENAME executes an implicit DEL operation,
// so if the deleted key contains a very big value it may cause high latency even if RENAME itself is usually a constant-time operation.
//
// In Cluster mode, both key and newKey must be in the same hash slot,
// meaning that in practice only keys that have the same hashtag can be reliably renamed in cluster.
//
// https://redis.io/commands/rename/
func (r GroupGeneric) Rename(ctx context.Context, key, newKey string) error {
	_, err := r.Operation.Do(ctx, "Rename", key, newKey)
	return err
}

// RenameNX renames key to newKey if newKey does not yet exist.
// It returns an error when key does not exist.
// In Cluster mode, both key and newKey must be in the same hash slot,
// meaning that in practice only keys that have the same hashtag can be reliably renamed in cluster.
//
// It returns:
// - 1 if key was renamed to newKey.
// - 0 if newKey already exists.
//
// https://redis.io/commands/renamenx/
func (r GroupGeneric) RenameNX(ctx context.Context, key, newKey string) (int64, error) {
	v, err := r.Operation.Do(ctx, "RenameNX", key, newKey)
	return v.Int64(), err
}

// Move moves key from the currently selected database (see SELECT) to the specified destination database.
// When key already exists in the destination database, or it does not exist in the source database,
// it does nothing.
// It is possible to use MOVE as a locking primitive because of this.
//
// It returns:
// - 1 if key was moved.
// - 0 if key was not moved.
//
// https://redis.io/commands/move/
func (r GroupGeneric) Move(ctx context.Context, key string, db int) (int64, error) {
	v, err := r.Operation.Do(ctx, "Move", key, db)
	return v.Int64(), err
}

// Del removes the specified keys.
// a key is ignored if it does not exist.
//
// It returns the number of keys that were removed.
//
// https://redis.io/commands/del/
func (r GroupGeneric) Del(ctx context.Context, keys ...string) (int64, error) {
	v, err := r.Operation.Do(ctx, "Del", gconv.Interfaces(keys)...)
	return v.Int64(), err
}

// RandomKey return a random key from the currently selected database.
//
// It returns the random key, or nil when the database is empty.
//
// https://redis.io/commands/randomkey/
func (r GroupGeneric) RandomKey(ctx context.Context) (string, error) {
	v, err := r.Operation.Do(ctx, "RandomKey")
	return v.String(), err
}

// DBSize return the number of keys in the currently-selected database.
//
// https://redis.io/commands/dbsize/
func (r GroupGeneric) DBSize(ctx context.Context) (int64, error) {
	v, err := r.Operation.Do(ctx, "DBSize")
	return v.Int64(), err
}

// Keys return all keys matching pattern.
//
// While the time complexity for this operation is O(N), the constant times are fairly low.
// For example, Redis running on an entry level laptop can scan a 1 million key database in 40 milliseconds.
//
// https://redis.io/commands/keys/
func (r GroupGeneric) Keys(ctx context.Context, pattern string) ([]string, error) {
	v, err := r.Operation.Do(ctx, "Keys", pattern)
	return v.Strings(), err
}

// Scan executes a single iteration of the SCAN command, returning a subset of keys matching the pattern along with the next cursor position.
// This method provides more efficient and safer way to iterate over large datasets compared to KEYS command.
//
// Users are responsible for controlling the iteration by managing the cursor.
//
// The `count` optional parameter advises Redis on the number of keys to return. While it's not a strict limit, it guides the operation's granularity.
//
// https://redis.io/commands/scan/
func (r GroupGeneric) Scan(ctx context.Context, cursor uint64, option ...gredis.ScanOption) (uint64, []string, error) {
	var usedOption interface{}
	if len(option) > 0 {
		usedOption = option[0].ToUsedOption()
	}

	v, err := r.Operation.Do(ctx, "Scan", mustMergeOptionToArgs(
		[]interface{}{cursor}, usedOption,
	)...)
	if err != nil {
		return 0, nil, err
	}

	nextCursor := gconv.Uint64(v.Slice()[0])
	keys := gconv.SliceStr(v.Slice()[1])

	return nextCursor, keys, nil
}

// FlushDB delete all the keys of the currently selected DB. This command never fails.
//
// https://redis.io/commands/flushdb/
func (r GroupGeneric) FlushDB(ctx context.Context, option ...gredis.FlushOp) error {
	_, err := r.Operation.Do(ctx, "FlushDB", gconv.Interfaces(option)...)
	return err
}

// FlushAll delete all the keys of all the existing databases, not just the currently selected one.
// This command never fails.
// By default, FlushAll will synchronously flush all the databases.
//
// It is possible to use one of the following modifiers to dictate the flushing mode explicitly:
// ASYNC: flushes the databases asynchronously
// SYNC: flushes the databases synchronously
//
// Note: an asynchronous FlushAll command only deletes keys that were present at the time the command was invoked.
// Keys created during an asynchronous flush will be unaffected.
//
// https://redis.io/commands/flushall/
func (r GroupGeneric) FlushAll(ctx context.Context, option ...gredis.FlushOp) error {
	_, err := r.Operation.Do(ctx, "FlushAll", gconv.Interfaces(option)...)
	return err
}

// Expire sets a timeout on key.
// After the timeout has expired, the key will automatically be deleted.
//
// It returns:
// - 1 if the timeout was set.
// - 0 if the timeout was not set. e.g. key doesn't exist, or operation skipped due to the provided arguments.
//
// https://redis.io/commands/expire/
func (r GroupGeneric) Expire(ctx context.Context, key string, seconds int64, option ...gredis.ExpireOption) (int64, error) {
	var usedOption interface{}
	if len(option) > 0 {
		usedOption = option[0]
	}
	v, err := r.Operation.Do(ctx, "Expire", mustMergeOptionToArgs(
		[]interface{}{key, seconds}, usedOption,
	)...)
	return v.Int64(), err
}

// ExpireAt has the same effect and semantic as EXPIRE, but instead of specifying the number of
// seconds representing the TTL (time to live), it takes an absolute Unix timestamp (seconds since
// January 1, 1970).
// A timestamp in the past will delete the key immediately.
//
// It returns:
// - 1 if the timeout was set.
// - 0 if the timeout was not set. e.g. key doesn't exist, or operation skipped due to the provided arguments.
//
// https://redis.io/commands/expireat/
func (r GroupGeneric) ExpireAt(ctx context.Context, key string, time time.Time, option ...gredis.ExpireOption) (int64, error) {
	var usedOption interface{}
	if len(option) > 0 {
		usedOption = option[0]
	}
	v, err := r.Operation.Do(ctx, "ExpireAt", mustMergeOptionToArgs(
		[]interface{}{key, gtime.New(time).Timestamp()}, usedOption,
	)...)
	return v.Int64(), err
}

// ExpireTime returns the absolute time at which the given key will expire.
//
// It returns:
// - -1 if the key exists but has no associated expiration time.
// - -2 if the key does not exist.
//
// https://redis.io/commands/expiretime/
func (r GroupGeneric) ExpireTime(ctx context.Context, key string) (*gvar.Var, error) {
	return r.Operation.Do(ctx, "ExpireTime", key)
}

// TTL returns the remaining time to live of a key that has a timeout.
// This introspection capability allows a Redis client to check how many seconds a given key
// will continue to be part of the dataset.
// In Redis 2.6 or older the command returns -1 if the key does not exist or if the key exist but has
// no associated expire.
//
// Starting with Redis 2.8 the return value in case of error changed:
//
// The command returns -2 if the key does not exist.
// The command returns -1 if the key exists but has no associated expire.
// See also the PTTL command that returns the same information with milliseconds resolution
// (Only available in Redis 2.6 or greater).
//
// It returns TTL in seconds, or a negative value in order to signal an error (see the description above).
//
// https://redis.io/commands/ttl/
func (r GroupGeneric) TTL(ctx context.Context, key string) (int64, error) {
	v, err := r.Operation.Do(ctx, "TTL", key)
	return v.Int64(), err
}

// Persist removes the existing timeout on key, turning the key from volatile (a key with an expire set)
// to persistent (a key that will never expire as no timeout is associated).
//
// It returns:
// - 1 if the timeout was removed.
// - 0 if key does not exist or does not have an associated timeout.
//
// https://redis.io/commands/persist/
func (r GroupGeneric) Persist(ctx context.Context, key string) (int64, error) {
	v, err := r.Operation.Do(ctx, "Persist", key)
	return v.Int64(), err
}

// PExpire works exactly like EXPIRE but the time to live of the key is specified in milliseconds
// instead of seconds.
//
// It returns:
// - 1 if the timeout was set.
// - 0 if the timeout was not set. e.g. key doesn't exist, or operation skipped due to the provided arguments.
//
// https://redis.io/commands/pexpire/
func (r GroupGeneric) PExpire(ctx context.Context, key string, milliseconds int64, option ...gredis.ExpireOption) (int64, error) {
	var usedOption interface{}
	if len(option) > 0 {
		usedOption = option[0]
	}
	v, err := r.Operation.Do(ctx, "PExpire", mustMergeOptionToArgs(
		[]interface{}{key, milliseconds}, usedOption,
	)...)
	return v.Int64(), err
}

// PExpireAt has the same effect and semantic as ExpireAt, but the Unix time at which the key will
// expire is specified in milliseconds instead of seconds.
//
// https://redis.io/commands/pexpireat/
func (r GroupGeneric) PExpireAt(ctx context.Context, key string, time time.Time, option ...gredis.ExpireOption) (int64, error) {
	var usedOption interface{}
	if len(option) > 0 {
		usedOption = option[0]
	}
	v, err := r.Operation.Do(ctx, "PExpireAt", mustMergeOptionToArgs(
		[]interface{}{key, gtime.New(time).TimestampMilli()}, usedOption,
	)...)
	return v.Int64(), err
}

// PExpireTime returns the expiration time of given `key`.
//
// It returns:
// - -1 if the key exists but has no associated expiration time.
// - -2 if the key does not exist.
//
// https://redis.io/commands/pexpiretime/
func (r GroupGeneric) PExpireTime(ctx context.Context, key string) (*gvar.Var, error) {
	return r.Operation.Do(ctx, "PExpireTime", key)
}

// PTTL like TTL this command returns the remaining time to live of a key that has an expired set,
// with the sole difference that TTL returns the amount of remaining time in seconds while PTTL
// returns it in milliseconds.
//
// In Redis 2.6 or older the command returns -1 if the key does not exist or if the key exist but has
// no associated expire.
//
// It returns TTL in milliseconds, or a negative value in order to signal an error (see the description above).
//
//	https://redis.io/commands/pttl/
func (r GroupGeneric) PTTL(ctx context.Context, key string) (int64, error) {
	v, err := r.Operation.Do(ctx, "PTTL", key)
	return v.Int64(), err
}
