package rediczar

import (
	"context"
	"time"
)

// ThickClient is a redis client supporting redis commands, pipelining, and advanced operations
type ThickClient interface {
	// Commands support all redis commands
	Commands

	// Cached can cache any arbitrary type that encodes. Use other Cached* methods cache common data types (like primitives)
	// The type of `res` must be a pointer to the return type of `fn` (datasource function). This pattern of caching is called cache-aside.
	// If the datasource returns nil, the value is cached (negative caching).
	//
	// Cached will make every attempt to succeed if there is a redis or serialization error. If the returned error is nil, then res is
	// a value loaded from cache or the data source.
	//
	// A TTL of 0 means the value from the datasource should not be cached.
	//
	// Type examples for `res` and `fn`. Usage will normally pass in the address of `res`
	//
	//  * `res` is type *int, `fn` returns 10
	//  * `res` is type *Foo, `fn` returns Foo
	//  * `res` is type **Foo, `fn` returns nil or *Foo
	//
	// Usage example:
	//
	//	var u user
	//	err := cache.Cached(ctx, "user:123", 10*time.Minute, &u, func() (interface{}, error) {
	//		return user{ID: "123", Login: "kappa"}, nil // This would be retrieved from a service call
	//	})
	Cached(ctx context.Context, key string, ttl time.Duration, res interface{}, fn func() (interface{}, error)) error

	// CachedBool is a type-safe convenience function to caches booleans
	CachedBool(ctx context.Context, key string, ttl time.Duration, fn func() (bool, error)) (bool, error)

	// CachedBoolDifferentTTLs works like CachedBool but lets the caller specify a different cache TTL for true or false values
	CachedBoolDifferentTTLs(ctx context.Context, key string, ttls CachedBoolTTLs, fn func() (bool, error)) (bool, error)

	// CachedInt is a type-safe convenience function to cache ints
	CachedInt(ctx context.Context, key string, ttl time.Duration, fn func() (int, error)) (int, error)

	// CachedInt64 is a type-safe convenience function to cache int64s
	CachedInt64(ctx context.Context, key string, ttl time.Duration, fn func() (int64, error)) (int64, error)

	// CachedString is a type-safe convenience function to cache strings and is optimized to skip encoding. It's highly recommended
	// to use this over Cached when caching string values
	CachedString(ctx context.Context, key string, ttl time.Duration, fn func() (string, error)) (string, error)

	// CachedIntSlice is a type-safe convenience function to cache int slices
	CachedIntSlice(ctx context.Context, key string, ttl time.Duration, fn func() ([]int, error)) ([]int, error)

	// CachedInt64Slice is a type-safe convenience function to cache int64 slices
	CachedInt64Slice(ctx context.Context, key string, ttl time.Duration, fn func() ([]int64, error)) ([]int64, error)

	// CachedStringSlice is a type-safe convenience function to cache string slices
	CachedStringSlice(ctx context.Context, key string, ttl time.Duration, fn func() ([]string, error)) ([]string, error)

	// CachedGet gets the key and deserializes into res. The type of `res` must be a pointer.
	// If the cache value exists then nil is returned, otherwise redis.Nil is returned
	CachedGet(ctx context.Context, key string, res interface{}) error

	// CachedSet serializes obj and sets the key with the TTL. This forces the cache to update
	CachedSet(ctx context.Context, key string, ttl time.Duration, obj interface{}) error

	// IncrWithTTL increments and updates the expiry on the key, regardless of any existing expiry value. The new value
	// of the key is returned
	IncrWithTTL(ctx context.Context, key string, ttl time.Duration) (int64, error)

	// RateIncr increments and sets an expiry on the key if it was 0. The new value of the key is returned. This
	// can be used to rate limit over a non-overlapping interval
	RateIncr(ctx context.Context, key string, ttl time.Duration) (int64, error)

	// MRateIncr leverages redis pipelining to issue many RateIncr commands. The new value of the key is returned.
	MRateIncr(ctx context.Context, keys []string, ttl time.Duration) ([]int64, error)

	// SlidingWindowRateLimit returns whether the operation should be allowed. It will not allow more
	// than `maxElements` within the `ttl` duration. This can be used to rate limit over an overlapping interval
	SlidingWindowRateLimit(ctx context.Context, key string, ttl time.Duration, maxElements int64) (bool, error)

	// QuerySlidingWindow returns whether the operation should be allowed. It will not allow more
	// than `maxElements` within the `ttl` duration. This method is used to query the current
	// rate limiting state, and will *not* insert a new operation.
	QuerySlidingWindow(ctx context.Context, key string, ttl time.Duration, maxElements int64) (bool, error)

	// MSetNXWithTTL leverages redis pipelining to issue many SET commands with NX and TTL specified.
	// Pipelining is necessary since MSET alone does not support NX and TTL arguments. Booleans
	// are returned to indicate whether each key-pair was set.
	MSetNXWithTTL(ctx context.Context, ttl time.Duration, pairs ...interface{}) ([]bool, error)

	// MSetWithTTL leverages redis pipelining to issue many SET commands with TTL specified.
	// Pipelining is necessary since MSET alone does not support TTL arguments.
	MSetWithTTL(ctx context.Context, ttl time.Duration, pairs ...interface{}) error

	// PipelinedGet leverages redis pipelining to issue multiple GET commands and returns the values of specified keys
	// This can be used to perform a MGet across a cluster without client side hashing
	// If the key is not found for index i, then the value is nil, otherwise the value is a string
	PipelinedGet(ctx context.Context, keys ...string) ([]interface{}, error)

	// PipelinedDel leverages redis pipelining to issue multiple DEL commands and returns the
	// number of keys that were deleted
	// This can be used to perform DELs on multiple keys accross a cluster without client side hashing
	PipelinedDel(ctx context.Context, keys ...string) (int64, error)
}

// Logger logs values
type Logger interface {
	Log(vals ...interface{})
}

// Client is an implementation of ThickClient
type Client struct {
	// Commands is the Redis commands client and must be set
	Commands
	// Logger is optional to log internal errors
	Logger Logger
	// Encode serializes v into bytes. The default is json.Marshal.
	// This is used for Cache operations
	Encode func(v interface{}) ([]byte, error)
	// Decode deserializes the bytes and stores the results into the value. The default is json.Unmarshal.
	// This is used for Cache operations
	Decode func(b []byte, v interface{}) error
}

func (c *Client) log(vals ...interface{}) {
	if c.Logger != nil {
		c.Logger.Log(vals...)
	}
}

var _ ThickClient = (*Client)(nil)
