packages feed

hedis-0.16.0: src/Database/Redis/ManualCommands.hs

{-# LANGUAGE CPP, OverloadedStrings, RecordWildCards, FlexibleContexts #-}

module Database.Redis.ManualCommands where

import Prelude hiding (min, max)
import Data.ByteString (ByteString, empty, append)
import qualified Data.ByteString.Char8 as Char8
import qualified Data.ByteString as BS
import Data.List.NonEmpty (NonEmpty(..))
import qualified Data.List.NonEmpty as NE
import Data.Maybe (maybeToList, catMaybes, fromMaybe)
#if __GLASGOW_HASKELL__ < 808
import Data.Semigroup ((<>))
#endif



import Database.Redis.Core
import Database.Redis.Protocol
import Database.Redis.Types
import qualified Database.Redis.Cluster.Command as CMD

-- |Inspect the internals of Redis objects (<http://redis.io/commands/object>). The Redis command @OBJECT@ is split up into 'objectRefcount', 'objectEncoding', 'objectIdletime'. Since Redis 2.2.3
objectRefcount
    :: (RedisCtx m f)
    => ByteString -- ^ key
    -> m (f Integer)
objectRefcount key = sendRequest ["OBJECT", "refcount", key]

objectIdletime
    :: (RedisCtx m f)
    => ByteString -- ^ key
    -> m (f Integer)
objectIdletime key = sendRequest ["OBJECT", "idletime", key]

-- |Inspect the internals of Redis objects (<http://redis.io/commands/object>). The Redis command @OBJECT@ is split up into 'objectRefcount', 'objectEncoding', 'objectIdletime'. Since Redis 2.2.3
objectEncoding
    :: (RedisCtx m f)
    => ByteString -- ^ key
    -> m (f ByteString)
objectEncoding key = sendRequest ["OBJECT", "encoding", key]

-- |Insert an element before or after another element in a list (<http://redis.io/commands/linsert>). The Redis command @LINSERT@ is split up into 'linsertBefore', 'linsertAfter'. Since Redis 2.2.0
linsertBefore
    :: (RedisCtx m f)
    => ByteString -- ^ key
    -> ByteString -- ^ pivot
    -> ByteString -- ^ value
    -> m (f Integer)
linsertBefore key pivot value =
    sendRequest ["LINSERT", key, "BEFORE", pivot, value]

-- |Insert an element before or after another element in a list (<http://redis.io/commands/linsert>). The Redis command @LINSERT@ is split up into 'linsertBefore', 'linsertAfter'. Since Redis 2.2.0
linsertAfter
    :: (RedisCtx m f)
    => ByteString -- ^ key
    -> ByteString -- ^ pivot
    -> ByteString -- ^ value
    -> m (f Integer)
linsertAfter key pivot value =
        sendRequest ["LINSERT", encode key, "AFTER", encode pivot, encode value]

-- |Determine the type stored at key (<http://redis.io/commands/type>). Since Redis 1.0.0
getType
    :: (RedisCtx m f)
    => ByteString -- ^ key
    -> m (f RedisType)
getType key = sendRequest ["TYPE", key]

-- |A single entry from the slowlog.
data Slowlog = Slowlog
    { slowlogId        :: Integer
      -- ^ A unique progressive identifier for every slow log entry.
    , slowlogTimestamp :: Integer
      -- ^ The unix timestamp at which the logged command was processed.
    , slowlogMicros    :: Integer
      -- ^ The amount of time needed for its execution, in microseconds.
    , slowlogCmd       :: [ByteString]
      -- ^ The command and it's arguments.
    , slowlogClientIpAndPort :: Maybe ByteString
    , slowlogClientName :: Maybe ByteString
    } deriving (Show, Eq)

instance RedisResult Slowlog where
    decode (MultiBulk (Just [logId,timestamp,micros,cmd])) = do
        slowlogId        <- decode logId
        slowlogTimestamp <- decode timestamp
        slowlogMicros    <- decode micros
        slowlogCmd       <- decode cmd
        let slowlogClientIpAndPort = Nothing
            slowlogClientName = Nothing
        return Slowlog{..}
    decode (MultiBulk (Just [logId,timestamp,micros,cmd,ip,cname])) = do
        slowlogId        <- decode logId
        slowlogTimestamp <- decode timestamp
        slowlogMicros    <- decode micros
        slowlogCmd       <- decode cmd
        slowlogClientIpAndPort <- Just <$> decode ip
        slowlogClientName <- Just <$> decode cname
        return Slowlog{..}
    decode r = Left r

slowlogGet
    :: (RedisCtx m f)
    => Integer -- ^ cnt
    -> m (f [Slowlog])
slowlogGet n = sendRequest ["SLOWLOG", "GET", encode n]

slowlogLen :: (RedisCtx m f) => m (f Integer)
slowlogLen = sendRequest ["SLOWLOG", "LEN"]

slowlogReset :: (RedisCtx m f) => m (f Status)
slowlogReset = sendRequest ["SLOWLOG", "RESET"]

-- |Return a range of members in a sorted set, by index (<http://redis.io/commands/zrange>). The Redis command @ZRANGE@ is split up into 'zrange', 'zrangeWithscores'. Since Redis 1.2.0
zrange
    :: (RedisCtx m f)
    => ByteString -- ^ key
    -> Integer -- ^ start
    -> Integer -- ^ stop
    -> m (f [ByteString])
zrange key start stop =
    sendRequest ["ZRANGE", encode key, encode start, encode stop]

-- |Return a range of members in a sorted set, by index (<http://redis.io/commands/zrange>). The Redis command @ZRANGE@ is split up into 'zrange', 'zrangeWithscores'. Since Redis 1.2.0
zrangeWithscores
    :: (RedisCtx m f)
    => ByteString -- ^ key
    -> Integer -- ^ start
    -> Integer -- ^ stop
    -> m (f [(ByteString, Double)])
zrangeWithscores key start stop =
    sendRequest ["ZRANGE", encode key, encode start, encode stop, "WITHSCORES"]

zrevrange
    :: (RedisCtx m f)
    => ByteString -- ^ key
    -> Integer -- ^ start
    -> Integer -- ^ stop
    -> m (f [ByteString])
zrevrange key start stop =
    sendRequest ["ZREVRANGE", encode key, encode start, encode stop]

zrevrangeWithscores
    :: (RedisCtx m f)
    => ByteString -- ^ key
    -> Integer -- ^ start
    -> Integer -- ^ stop
    -> m (f [(ByteString, Double)])
zrevrangeWithscores key start stop =
    sendRequest ["ZREVRANGE", encode key, encode start, encode stop
                ,"WITHSCORES"]

zrangebyscore
    :: (RedisCtx m f)
    => ByteString -- ^ key
    -> Double -- ^ min
    -> Double -- ^ max
    -> m (f [ByteString])
zrangebyscore key min max =
    sendRequest ["ZRANGEBYSCORE", encode key, encode min, encode max]

zrangebyscoreWithscores
    :: (RedisCtx m f)
    => ByteString -- ^ key
    -> Double -- ^ min
    -> Double -- ^ max
    -> m (f [(ByteString, Double)])
zrangebyscoreWithscores key min max =
    sendRequest ["ZRANGEBYSCORE", encode key, encode min, encode max
                ,"WITHSCORES"]

zrangebyscoreLimit
    :: (RedisCtx m f)
    => ByteString -- ^ key
    -> Double -- ^ min
    -> Double -- ^ max
    -> Integer -- ^ offset
    -> Integer -- ^ count
    -> m (f [ByteString])
zrangebyscoreLimit key min max offset count =
    sendRequest ["ZRANGEBYSCORE", encode key, encode min, encode max
                ,"LIMIT", encode offset, encode count]

zrangebyscoreWithscoresLimit
    :: (RedisCtx m f)
    => ByteString -- ^ key
    -> Double -- ^ min
    -> Double -- ^ max
    -> Integer -- ^ offset
    -> Integer -- ^ count
    -> m (f [(ByteString, Double)])
zrangebyscoreWithscoresLimit key min max offset count =
    sendRequest ["ZRANGEBYSCORE", encode key, encode min, encode max
                ,"WITHSCORES","LIMIT", encode offset, encode count]

zrevrangebyscore
    :: (RedisCtx m f)
    => ByteString -- ^ key
    -> Double -- ^ max
    -> Double -- ^ min
    -> m (f [ByteString])
zrevrangebyscore key min max =
    sendRequest ["ZREVRANGEBYSCORE", encode key, encode min, encode max]

zrevrangebyscoreWithscores
    :: (RedisCtx m f)
    => ByteString -- ^ key
    -> Double -- ^ max
    -> Double -- ^ min
    -> m (f [(ByteString, Double)])
zrevrangebyscoreWithscores key min max =
    sendRequest ["ZREVRANGEBYSCORE", encode key, encode min, encode max
                ,"WITHSCORES"]

zrevrangebyscoreLimit
    :: (RedisCtx m f)
    => ByteString -- ^ key
    -> Double -- ^ max
    -> Double -- ^ min
    -> Integer -- ^ offset
    -> Integer -- ^ count
    -> m (f [ByteString])
zrevrangebyscoreLimit key min max offset count =
    sendRequest ["ZREVRANGEBYSCORE", encode key, encode min, encode max
                ,"LIMIT", encode offset, encode count]

zrevrangebyscoreWithscoresLimit
    :: (RedisCtx m f)
    => ByteString -- ^ key
    -> Double -- ^ max
    -> Double -- ^ min
    -> Integer -- ^ offset
    -> Integer -- ^ count
    -> m (f [(ByteString, Double)])
zrevrangebyscoreWithscoresLimit key min max offset count =
    sendRequest ["ZREVRANGEBYSCORE", encode key, encode min, encode max
                ,"WITHSCORES","LIMIT", encode offset, encode count]

-- |Options for the 'sort' command.
data SortOpts = SortOpts
    { sortBy     :: Maybe ByteString
    , sortLimit  :: (Integer,Integer)
    , sortGet    :: [ByteString]
    , sortOrder  :: SortOrder
    , sortAlpha  :: Bool
    } deriving (Show, Eq)

-- |Redis default 'SortOpts'. Equivalent to omitting all optional parameters.
--
-- @
-- SortOpts
--     { sortBy    = Nothing -- omit the BY option
--     , sortLimit = (0,-1)  -- return entire collection
--     , sortGet   = []      -- omit the GET option
--     , sortOrder = Asc     -- sort in ascending order
--     , sortAlpha = False   -- sort numerically, not lexicographically
--     }
-- @
--
defaultSortOpts :: SortOpts
defaultSortOpts = SortOpts
    { sortBy    = Nothing
    , sortLimit = (0,-1)
    , sortGet   = []
    , sortOrder = Asc
    , sortAlpha = False
    }

data SortOrder = Asc | Desc deriving (Show, Eq)

-- |Sort the elements in a list, set or sorted set (<http://redis.io/commands/sort>). The Redis command @SORT@ is split up into 'sort', 'sortStore'. Since Redis 1.0.0
sortStore
    :: (RedisCtx m f)
    => ByteString -- ^ key
    -> ByteString -- ^ destination
    -> SortOpts
    -> m (f Integer)
sortStore key dest = sortInternal key (Just dest)

-- |Sort the elements in a list, set or sorted set (<http://redis.io/commands/sort>). The Redis command @SORT@ is split up into 'sort', 'sortStore'. Since Redis 1.0.0
sort
    :: (RedisCtx m f)
    => ByteString -- ^ key
    -> SortOpts
    -> m (f [ByteString])
sort key = sortInternal key Nothing

sortInternal
    :: (RedisResult a, RedisCtx m f)
    => ByteString -- ^ key
    -> Maybe ByteString -- ^ destination
    -> SortOpts
    -> m (f a)
sortInternal key destination SortOpts{..} = sendRequest $
    concat [["SORT", encode key], by, limit, get, order, alpha, store]
  where
    by    = maybe [] (\pattern -> ["BY", pattern]) sortBy
    limit = let (off,cnt) = sortLimit in ["LIMIT", encode off, encode cnt]
    get   = concatMap (\pattern -> ["GET", pattern]) sortGet
    order = case sortOrder of Desc -> ["DESC"]; Asc -> ["ASC"]
    alpha = ["ALPHA" | sortAlpha]
    store = maybe [] (\dest -> ["STORE", dest]) destination


data Aggregate = Sum | Min | Max deriving (Show,Eq)

zunionstore
    :: (RedisCtx m f)
    => ByteString -- ^ destination
    -> [ByteString] -- ^ keys
    -> Aggregate
    -> m (f Integer)
zunionstore dest keys =
    zstoreInternal "ZUNIONSTORE" dest keys []

zunionstoreWeights
    :: (RedisCtx m f)
    => ByteString -- ^ destination
    -> [(ByteString,Double)] -- ^ weighted keys
    -> Aggregate
    -> m (f Integer)
zunionstoreWeights dest kws =
    let (keys,weights) = unzip kws
    in zstoreInternal "ZUNIONSTORE" dest keys weights

-- |Intersect multiple sorted sets and store the resulting sorted set in a new key (<http://redis.io/commands/zinterstore>). The Redis command @ZINTERSTORE@ is split up into 'zinterstore', 'zinterstoreWeights'. Since Redis 2.0.0
zinterstore
    :: (RedisCtx m f)
    => ByteString -- ^ destination
    -> NonEmpty ByteString -- ^ keys
    -> Aggregate
    -> m (f Integer)
zinterstore dest (key_:|keys_) =
    zstoreInternal "ZINTERSTORE" dest (key_:keys_) []

-- |Intersect multiple sorted sets and store the resulting sorted set in a new key (<http://redis.io/commands/zinterstore>). The Redis command @ZINTERSTORE@ is split up into 'zinterstore', 'zinterstoreWeights'. Since Redis 2.0.0
zinterstoreWeights
    :: (RedisCtx m f)
    => ByteString -- ^ destination
    -> NonEmpty (ByteString,Double) -- ^ weighted keys
    -> Aggregate
    -> m (f Integer)
zinterstoreWeights dest kws =
    let (keys,weights) = unzip (NE.toList kws)
    in zstoreInternal "ZINTERSTORE" dest keys weights

zstoreInternal
    :: (RedisCtx m f)
    => ByteString -- ^ cmd
    -> ByteString -- ^ destination
    -> [ByteString] -- ^ keys
    -> [Double] -- ^ weights
    -> Aggregate
    -> m (f Integer)
zstoreInternal cmd dest keys weights aggregate = sendRequest $
    concat [ [cmd, dest, encode . toInteger $ length keys ], keys
           , if null weights then [] else "WEIGHTS" : map encode weights
           , ["AGGREGATE", aggregate']
           ]
  where
    aggregate' = case aggregate of
        Sum -> "SUM"
        Min -> "MIN"
        Max -> "MAX"

-- |Execute a Lua script server side (<http://redis.io/commands/eval>). Since Redis 2.6.0
eval
    :: (RedisCtx m f, RedisResult a)
    => ByteString -- ^ script
    -> [ByteString] -- ^ keys
    -> [ByteString] -- ^ args
    -> m (f a)
eval script keys args =
    sendRequest $ ["EVAL", script, encode numkeys] ++ keys ++ args
  where
    numkeys = toInteger (length keys)

-- | Works like 'eval', but sends the SHA1 hash of the script instead of the script itself.
-- Fails if the server does not recognise the hash, in which case, 'eval' should be used instead.
evalsha
    :: (RedisCtx m f, RedisResult a)
    => ByteString -- ^ base16-encoded sha1 hash of the script
    -> [ByteString] -- ^ keys
    -> [ByteString] -- ^ args
    -> m (f a)
evalsha script keys args =
    sendRequest $ ["EVALSHA", script, encode numkeys] ++ keys ++ args
  where
    numkeys = toInteger (length keys)

bitcount
    :: (RedisCtx m f)
    => ByteString -- ^ key
    -> m (f Integer)
bitcount key = sendRequest ["BITCOUNT", key]

bitcountRange
    :: (RedisCtx m f)
    => ByteString -- ^ key
    -> Integer -- ^ start
    -> Integer -- ^ end
    -> m (f Integer)
bitcountRange key start end =
    sendRequest ["BITCOUNT", key, encode start, encode end]

bitopAnd
    :: (RedisCtx m f)
    => ByteString -- ^ destkey
    -> [ByteString] -- ^ srckeys
    -> m (f Integer)
bitopAnd dst srcs = bitop "AND" (dst:srcs)

bitopOr
    :: (RedisCtx m f)
    => ByteString -- ^ destkey
    -> [ByteString] -- ^ srckeys
    -> m (f Integer)
bitopOr dst srcs = bitop "OR" (dst:srcs)

bitopXor
    :: (RedisCtx m f)
    => ByteString -- ^ destkey
    -> [ByteString] -- ^ srckeys
    -> m (f Integer)
bitopXor dst srcs = bitop "XOR" (dst:srcs)

bitopNot
    :: (RedisCtx m f)
    => ByteString -- ^ destkey
    -> ByteString -- ^ srckey
    -> m (f Integer)
bitopNot dst src = bitop "NOT" [dst, src]

bitop
    :: (RedisCtx m f)
    => ByteString -- ^ operation
    -> [ByteString] -- ^ keys
    -> m (f Integer)
bitop op ks = sendRequest $ "BITOP" : op : ks

-- |Atomically transfer a key from a Redis instance to another one (<http://redis.io/commands/migrate>). The Redis command @MIGRATE@ is split up into 'migrate', 'migrateMultiple'. Since Redis 2.6.0
migrate
    :: (RedisCtx m f)
    => ByteString -- ^ host
    -> ByteString -- ^ port
    -> ByteString -- ^ key
    -> Integer -- ^ destinationDb
    -> Integer -- ^ timeout
    -> m (f Status)
migrate host port key destinationDb timeout =
  sendRequest ["MIGRATE", host, port, key, encode destinationDb, encode timeout]

data MigrateAuth
  = MigrateAuth ByteString
  | MigrateAuth2 ByteString ByteString
  deriving (Show, Eq)

-- |Options for the 'migrate' command.
data MigrateOpts = MigrateOpts
    { migrateCopy    :: Bool
    , migrateReplace :: Bool
    , migrateAuth :: Maybe MigrateAuth
    } deriving (Show, Eq)

-- |Redis default 'MigrateOpts'. Equivalent to omitting all optional parameters.
--
-- @
-- MigrateOpts
--     { migrateCopy    = False -- remove the key from the local instance
--     , migrateReplace = False -- don't replace existing key on the remote instance
--     , migrateAuth = Nothing
--     }
-- @
--
defaultMigrateOpts :: MigrateOpts
defaultMigrateOpts = MigrateOpts
    { migrateCopy    = False
    , migrateReplace = False
    , migrateAuth = Nothing
    }

-- |Atomically transfer a key from a Redis instance to another one (<http://redis.io/commands/migrate>). The Redis command @MIGRATE@ is split up into 'migrate', 'migrateMultiple'. Since Redis 2.6.0
migrateMultiple
    :: (RedisCtx m f)
    => ByteString   -- ^ host
    -> ByteString   -- ^ port
    -> Integer      -- ^ destinationDb
    -> Integer      -- ^ timeout
    -> MigrateOpts
    -> [ByteString] -- ^ keys
    -> m (f Status)
migrateMultiple host port destinationDb timeout MigrateOpts{..} keys =
    sendRequest $
    concat [["MIGRATE", host, port, empty, encode destinationDb, encode timeout],
            auth_, copy, replace, keys]
  where
    copy = ["COPY" | migrateCopy]
    replace = ["REPLACE" | migrateReplace]
    auth_ = case migrateAuth of
     Nothing -> []
     Just (MigrateAuth pass)  -> ["AUTH", pass]
     Just (MigrateAuth2 user pass)  -> ["AUTH2", user, pass]


-- |Create a key using the provided serialized value, previously obtained using DUMP (<http://redis.io/commands/restore>). The Redis command @RESTORE@ is split up into 'restore', 'restoreReplace'. Since Redis 2.6.0
restore
    :: (RedisCtx m f)
    => ByteString -- ^ key
    -> Integer -- ^ timeToLive
    -> ByteString -- ^ serializedValue
    -> m (f Status)
restore key timeToLive serializedValue =
  sendRequest ["RESTORE", key, encode timeToLive, serializedValue]

data RestoreOpts = RestoreOpts
  { restoreOptsReplace :: Bool
  , restoreOptsAbsTTL :: Bool
  , restoreOptsIdle  :: Maybe Integer
  , restoreOptsFreq :: Maybe Integer
  }

-- |Create a key using the provided serialized value, previously obtained using DUMP (<http://redis.io/commands/restore>). The Redis command @RESTORE@ is split up into 'restore', 'restoreReplace'. Since Redis 2.6.0
restoreOpts
    :: (RedisCtx m f)
    => ByteString -- ^ key
    -> Integer -- ^ timeToLive
    -> ByteString -- ^ serializedValue
    -> RestoreOpts -- ^ restore options
    -> m (f Status)
restoreOpts key timeToLive serializedValue RestoreOpts{..} =
  sendRequest ("RESTORE": key: encode timeToLive: serializedValue:rest) where
  rest =  replace <> absttl <> idle <> freq
  replace = ["REPLACE" | restoreOptsReplace]
  absttl  = ["ABSTTL" | restoreOptsAbsTTL]
  idle    = maybe [] (\i -> ["IDLE", encode i]) restoreOptsIdle
  freq    = maybe [] (\f -> ["FREQ", encode f]) restoreOptsFreq

-- |Create a key using the provided serialized value, previously obtained using DUMP (<http://redis.io/commands/restore>). The Redis command @RESTORE@ is split up into 'restore', 'restoreReplace'. Since Redis 2.6.0

restoreReplace
    :: (RedisCtx m f)
    => ByteString -- ^ key
    -> Integer -- ^ timeToLive
    -> ByteString -- ^ serializedValue
    -> m (f Status)
restoreReplace key timeToLive serializedValue =
  sendRequest ["RESTORE", key, encode timeToLive, serializedValue, "REPLACE"]


set
    :: (RedisCtx m f)
    => ByteString -- ^ key
    -> ByteString -- ^ value
    -> m (f Status)
set key value = sendRequest ["SET", key, value]


data Condition =
  Nx | -- ^ Only set the key if it does not already exist.
  Xx   -- ^ Only set the key if it already exists.
   deriving (Show, Eq)


instance RedisArg Condition where
  encode Nx = "NX"
  encode Xx = "XX"


data SetOpts = SetOpts
  { setSeconds           :: Maybe Integer -- ^ Set the specified expire time, in seconds.
  , setMilliseconds      :: Maybe Integer -- ^ Set the specified expire time, in milliseconds.
  , setUnixSeconds       :: Maybe Integer
  {- ^ Set the specified Unix time at which the key will expire, in seconds.

  Since Redis 6.2
  -}
  , setUnixMilliseconds  :: Maybe Integer
  {- ^ Set the specified Unix time at which the key will expire, in milliseconds.
  -}
  , setCondition         :: Maybe Condition -- ^ Set the key on condition
  , setKeepTTL           :: Bool
  {- ^ Retain the time to live associated with the key.

  Since Redis 6.0
  -}
  } deriving (Show, Eq)

internalSetOptsToArgs :: SetOpts -> [ByteString]
internalSetOptsToArgs SetOpts{..} = concat [ex, px, exat, pxat, keepttl, condition]
  where
    ex   = maybe [] (\s -> ["EX",   encode s]) setSeconds
    px   = maybe [] (\s -> ["PX",   encode s]) setMilliseconds
    exat = maybe [] (\s -> ["EXAT", encode s]) setUnixSeconds
    pxat = maybe [] (\s -> ["PXAT", encode s]) setUnixMilliseconds
    keepttl = ["KEEPTTL" | setKeepTTL]
    condition = map encode $ maybeToList setCondition

setOpts
    :: (RedisCtx m f)
    => ByteString -- ^ key
    -> ByteString -- ^ value
    -> SetOpts
    -> m (f Status)
setOpts key value opts = sendRequest $ ["SET", key, value] ++ internalSetOptsToArgs opts

setGet
    :: (RedisCtx m f)
    => ByteString -- ^ key
    -> ByteString -- ^ value
    -> m (f ByteString)
setGet key value = sendRequest ["SET", key, value, "GET"]

setGetOpts
    :: (RedisCtx m f)
    => ByteString -- ^ key
    -> ByteString -- ^ value
    -> SetOpts
    -> m (f ByteString)
setGetOpts key value opts = sendRequest $ ["SET", key, value, "GET"] ++ internalSetOptsToArgs opts


data DebugMode = Yes | Sync | No deriving (Show, Eq)


instance RedisArg DebugMode where
  encode Yes = "YES"
  encode Sync = "SYNC"
  encode No = "NO"

-- |Set the debug mode for executed scripts (<http://redis.io/commands/script-debug>). Since Redis 3.2.0
scriptDebug
    :: (RedisCtx m f)
    => DebugMode
    -> m (f Bool)
scriptDebug mode =
    sendRequest ["SCRIPT DEBUG", encode mode]

-- |Add one or more members to a sorted set, or update its score if it already exists (<http://redis.io/commands/zadd>). The Redis command @ZADD@ is split up into 'zadd', 'zaddOpts'. Since Redis 1.2.0
zadd
    :: (RedisCtx m f)
    => ByteString -- ^ key
    -> [(Double,ByteString)] -- ^ scoreMember
    -> m (f Integer)
zadd key scoreMembers =
  zaddOpts key scoreMembers defaultZaddOpts


data SizeCondition =
    CGT | -- ^  Only update existing elements if the new score is greater than the current score. This flag doesn't prevent adding new elements.
    CLT   -- ^  Only update existing elements if the new score is less than the current score. This flag doesn't prevent adding new elements.
    deriving (Show, Eq)

instance RedisArg SizeCondition where
  encode CGT = "GT"
  encode CLT = "LT"

-- |Add one or more members to a sorted set, or update its score if it already exists (<http://redis.io/commands/zadd>). The Redis command @ZADD@ is split up into 'zadd', 'zaddOpts'. Since Redis 1.2.0
data ZaddOpts = ZaddOpts
  { zaddCondition :: Maybe Condition -- ^ Add on condition
  , zaddSizeCondition :: Maybe SizeCondition
  {- ^ Only update existing elements on condition

  Since Redis 6.2
  -}
  , zaddChange    :: Bool -- ^ Modify the return value from the number of new elements added, to the total number of elements changed
  , zaddIncrement :: Bool -- ^ When this option is specified ZADD acts like ZINCRBY. Only one score-element pair can be specified in this mode.
  } deriving (Show, Eq)


-- |Redis default 'ZaddOpts'. Equivalent to omitting all optional parameters.
--
-- @
-- ZaddOpts
--     { zaddCondition = Nothing -- omit NX and XX options
--     , zaddChange    = False   -- don't modify the return value from the number of new elements added, to the total number of elements changed
--     , zaddIncrement = False   -- don't add like ZINCRBY
--     }
-- @
--
defaultZaddOpts :: ZaddOpts
defaultZaddOpts = ZaddOpts
  { zaddCondition = Nothing
  , zaddChange    = False
  , zaddIncrement = False
  , zaddSizeCondition = Nothing
  }


zaddOpts
    :: (RedisCtx m f)
    => ByteString            -- ^ key
    -> [(Double,ByteString)] -- ^ scoreMember
    -> ZaddOpts              -- ^ options
    -> m (f Integer)
zaddOpts key scoreMembers ZaddOpts{..} =
    sendRequest $ concat [["ZADD", key], condition, sizeCondition, change, increment, scores]
  where
    scores = concatMap (\(x,y) -> [encode x,encode y]) scoreMembers
    condition = map encode $ maybeToList zaddCondition
    sizeCondition = map encode $ maybeToList zaddSizeCondition
    change = ["CH" | zaddChange]
    increment = ["INCR" | zaddIncrement]


data ReplyMode = On | Off | Skip deriving (Show, Eq)


instance RedisArg ReplyMode where
  encode On = "ON"
  encode Off = "OFF"
  encode Skip = "SKIP"

-- |Instruct the server whether to reply to commands (<http://redis.io/commands/client-reply>). Since Redis 3.2
clientReply
    :: (RedisCtx m f)
    => ReplyMode
    -> m (f Bool)
clientReply mode =
    sendRequest ["CLIENT REPLY", encode mode]

-- |Get one or multiple random members from a set (<http://redis.io/commands/srandmember>). The Redis command @SRANDMEMBER@ is split up into 'srandmember', 'srandmemberN'. Since Redis 1.0.0
srandmember
    :: (RedisCtx m f)
    => ByteString -- ^ key
    -> m (f (Maybe ByteString))
srandmember key = sendRequest ["SRANDMEMBER", key]


-- |Get one or multiple random members from a set (<http://redis.io/commands/srandmember>). The Redis command @SRANDMEMBER@ is split up into 'srandmember', 'srandmemberN'. Since Redis 1.0.0
srandmemberN
    :: (RedisCtx m f)
    => ByteString -- ^ key
    -> Integer -- ^ count
    -> m (f [ByteString])
srandmemberN key count = sendRequest ["SRANDMEMBER", key, encode count]

-- |Remove and return one or multiple random members from a set (<http://redis.io/commands/spop>). The Redis command @SPOP@ is split up into 'spop', 'spopN'. Since Redis 1.0.0
spop
    :: (RedisCtx m f)
    => ByteString -- ^ key
    -> m (f (Maybe ByteString))
spop key = sendRequest ["SPOP", key]

-- |Remove and return one or multiple random members from a set (<http://redis.io/commands/spop>). The Redis command @SPOP@ is split up into 'spop', 'spopN'. Since Redis 1.0.0
spopN
    :: (RedisCtx m f)
    => ByteString -- ^ key
    -> Integer -- ^ count
    -> m (f [ByteString])
spopN key count = sendRequest ["SPOP", key, encode count]


info
    :: (RedisCtx m f)
    => m (f ByteString)
info = sendRequest ["INFO"]


infoSection
    :: (RedisCtx m f)
    => ByteString -- ^ section
    -> m (f ByteString)
infoSection section = sendRequest ["INFO", section]

-- |Determine if a key exists (<http://redis.io/commands/exists>). Since Redis 1.0.0
exists
    :: (RedisCtx m f)
    => ByteString -- ^ key
    -> m (f Bool)
exists key = sendRequest ["EXISTS", key]

newtype Cursor = Cursor ByteString deriving (Show, Eq)


instance RedisArg Cursor where
  encode (Cursor c) = encode c


instance RedisResult Cursor where
  decode (Bulk (Just s)) = Right $ Cursor s
  decode r               = Left r


cursor0 :: Cursor
cursor0 = Cursor "0"

-- |Incrementally iterate the keys space (<http://redis.io/commands/scan>). The Redis command @SCAN@ is split up into 'scan', 'scanOpts'. Since Redis 2.8.0
scan
    :: (RedisCtx m f)
    => Cursor
    -> m (f (Cursor, [ByteString])) -- ^ next cursor and values
scan cursor = scanOpts cursor defaultScanOpts Nothing


data ScanOpts = ScanOpts
  { scanMatch :: Maybe ByteString
  , scanCount :: Maybe Integer
  } deriving (Show, Eq)


-- |Redis default 'ScanOpts'. Equivalent to omitting all optional parameters.
--
-- @
-- ScanOpts
--     { scanMatch = Nothing -- don't match any pattern
--     , scanCount = Nothing -- don't set any requirements on number elements returned (works like value @COUNT 10@)
--     }
-- @
--
defaultScanOpts :: ScanOpts
defaultScanOpts = ScanOpts
  { scanMatch = Nothing
  , scanCount = Nothing
  }

-- | Incrementally iterate the keys space (<http://redis.io/commands/scan>). The Redis command @SCAN@ is split up into 'scan', 'scanOpts'. Since Redis 2.8.0
scanOpts
    :: (RedisCtx m f)
    => Cursor
    -> ScanOpts
    -> Maybe ByteString -- ^ types of the object to  scan
    -> m (f (Cursor, [ByteString])) -- ^ next cursor and values
scanOpts cursor opts mtype_  = sendRequest $ addScanOpts ["SCAN", encode cursor] opts
    ++ maybe [] (\type_  -> ["TYPE", type_]) mtype_


addScanOpts
    :: [ByteString] -- ^ main part of scan command
    -> ScanOpts
    -> [ByteString]
addScanOpts cmd ScanOpts{..} =
    concat [cmd, match, count]
  where
    prepend x y = [x, y]
    match       = maybe [] (prepend "MATCH") scanMatch
    count       = maybe [] ((prepend "COUNT").encode) scanCount

-- |Incrementally iterate Set elements (<http://redis.io/commands/sscan>). The Redis command @SSCAN@ is split up into 'sscan', 'sscanOpts'. Since Redis 2.8.0
sscan
    :: (RedisCtx m f)
    => ByteString -- ^ key
    -> Cursor
    -> m (f (Cursor, [ByteString])) -- ^ next cursor and values
sscan key cursor = sscanOpts key cursor defaultScanOpts

-- |Incrementally iterate Set elements (<http://redis.io/commands/sscan>). The Redis command @SSCAN@ is split up into 'sscan', 'sscanOpts'. Since Redis 2.8.0
sscanOpts
    :: (RedisCtx m f)
    => ByteString -- ^ key
    -> Cursor
    -> ScanOpts
    -> m (f (Cursor, [ByteString])) -- ^ next cursor and values
sscanOpts key cursor opts = sendRequest $ addScanOpts ["SSCAN", key, encode cursor] opts

-- |Incrementally iterate hash fields and associated values (<http://redis.io/commands/hscan>). The Redis command @HSCAN@ is split up into 'hscan', 'hscanOpts'. Since Redis 2.8.0
hscan
    :: (RedisCtx m f)
    => ByteString -- ^ key
    -> Cursor
    -> m (f (Cursor, [(ByteString, ByteString)])) -- ^ next cursor and values
hscan key cursor = hscanOpts key cursor defaultScanOpts

-- |Incrementally iterate hash fields and associated values (<http://redis.io/commands/hscan>). The Redis command @HSCAN@ is split up into 'hscan', 'hscanOpts'. Since Redis 2.8.0
hscanOpts
    :: (RedisCtx m f)
    => ByteString -- ^ key
    -> Cursor
    -> ScanOpts
    -> m (f (Cursor, [(ByteString, ByteString)])) -- ^ next cursor and values
hscanOpts key cursor opts = sendRequest $ addScanOpts ["HSCAN", key, encode cursor] opts


zscan
    :: (RedisCtx m f)
    => ByteString -- ^ key
    -> Cursor
    -> m (f (Cursor, [(ByteString, Double)])) -- ^ next cursor and values
zscan key cursor = zscanOpts key cursor defaultScanOpts


zscanOpts
    :: (RedisCtx m f)
    => ByteString -- ^ key
    -> Cursor
    -> ScanOpts
    -> m (f (Cursor, [(ByteString, Double)])) -- ^ next cursor and values
zscanOpts key cursor opts = sendRequest $ addScanOpts ["ZSCAN", key, encode cursor] opts

data RangeLex a = Incl a | Excl a | Minr | Maxr

instance RedisArg a => RedisArg (RangeLex a) where
  encode (Incl bs) = "[" `append` encode bs
  encode (Excl bs) = "(" `append` encode bs
  encode Minr      = "-"
  encode Maxr      = "+"

-- |Return a range of members in a sorted set, by lexicographical range (<http://redis.io/commands/zrangebylex>). Since Redis 2.8.9
zrangebylex::(RedisCtx m f) =>
    ByteString             -- ^ key
    -> RangeLex ByteString -- ^ min
    -> RangeLex ByteString -- ^ max
    -> m (f [ByteString])
zrangebylex key min max =
    sendRequest ["ZRANGEBYLEX", encode key, encode min, encode max]

zrangebylexLimit
    ::(RedisCtx m f)
    => ByteString -- ^ key
    -> RangeLex ByteString -- ^ min
    -> RangeLex ByteString -- ^ max
    -> Integer             -- ^ offset
    -> Integer             -- ^ count
    -> m (f [ByteString])
zrangebylexLimit key min max offset count  =
    sendRequest ["ZRANGEBYLEX", encode key, encode min, encode max,
                 "LIMIT", encode offset, encode count]

-- | Trimming strategy.
--
-- @since 0.16.0
data TrimStrategy
  = TrimMaxlen Integer
    -- ^ Evicts entries as long as the stream's length exceeds the specified threshold, where threshold is a positive integer.
  | TrimMinId ByteString
   {- ^  Evicts entries with IDs lower than threshold, where threshold is a stream ID.

   Since Redis 6.2: will fail if used on ealier versions.
   -}

-- | Type of the trimming.
--
-- @since 0.16.0
data TrimType
  = TrimExact {- ^ Exact trimming -}
  | TrimApprox (Maybe Integer) {- ^ Approximate trimming. Is faster, but may leave slightly more
    elements in the stream if they can't be immediately deleted.

    Additional parameter Specifies the maximal count of entries that will be evicted. When LIMIT and count aren't specified, the default value of 100 * the number of entries in a macro node will be implicitly used as the count, @Just 0@ removes the limit entirely.
    -}

data TrimOpts = TrimOpts
  { trimOptsStrategy :: TrimStrategy
  , trimOptsType :: TrimType
  }

-- | Converts trim options to the low level parameters
internalTrimArgToList :: TrimOpts -> [ByteString]
internalTrimArgToList TrimOpts{..} = trimArg ++ limitArg
  where trimArg = case trimOptsStrategy of
           TrimMaxlen max -> ("MAXLEN":approxArg:encode max:[])
           TrimMinId i -> ("MINID":approxArg:i:[])
        (approxArg, limitArg) = case trimOptsType of
            TrimExact -> ("=", [])
            TrimApprox limit -> ("~",  maybe [] (("LIMIT":) . (:[]) . encode) limit)

trimOpts :: TrimStrategy -> TrimType -> TrimOpts
trimOpts = TrimOpts

data XAddOpts = XAddOpts {
    xAddTrimOpts :: Maybe TrimOpts, -- ^ Call XTRIM right after XADD
    xAddnoMkStream :: Bool
    {- ^ Don't create a new stream if it doesn't exist

    @since Redis 6.2
    -}
}

defaultXAddOpts :: XAddOpts
defaultXAddOpts = XAddOpts {
    xAddTrimOpts = Nothing,
    xAddnoMkStream = False
}

-- |Add a value to a stream (<https://redis.io/commands/xadd>). The Redis command @XADD@ is split up into 'xadd', 'xaddOpts'. Since Redis 5.0.0
xaddOpts
    :: (RedisCtx m f)
    => ByteString -- ^ Stream name.
    -> ByteString -- ^ Message ID
    -> [(ByteString, ByteString)] -- ^ Message data (field, value)
    -> XAddOpts -- ^ Additional parameteers
    -> m (f ByteString) -- ^ ID of the added entry.
xaddOpts key entryId fieldValues opts = sendRequest $
    ["XADD", key] ++ noMkStreamArgs ++ trimArgs ++ [entryId] ++ fieldArgs
    where
        fieldArgs = concatMap (\(x,y) -> [x,y]) fieldValues
        noMkStreamArgs = ["NOMKSTREAM" | xAddnoMkStream opts]
        trimArgs = maybe [] (internalTrimArgToList) (xAddTrimOpts opts)

-- | /O(1)/ Adds a value to a stream (<https://redis.io/commands/xadd>). Since Redis 5.0.0
xadd
    :: (RedisCtx m f)
    => ByteString -- ^ Stream name
    -> ByteString -- ^ Message id
    -> [(ByteString, ByteString)] -- ^ Message data (field, value)
    -> m (f ByteString)
xadd key entryId fieldValues = xaddOpts key entryId fieldValues defaultXAddOpts

-- | Additional parameters.
newtype XAutoclaimOpts = XAutoclaimOpts {
    xAutoclaimCount :: Maybe Integer -- ^  The upper limit of the number of entries that the command attempts to claim (default: 100).
}

-- | Default 'XAutoclaimOpts' value.
--
-- Prefer to use this function over direct use of constructor to preserve
-- backwards compatibility.
--
-- Defaults to @[Count 100)@
defaultXAutoclaimOpts :: XAutoclaimOpts
defaultXAutoclaimOpts = XAutoclaimOpts {
    xAutoclaimCount = Nothing
}

-- | Result of the 'xautoclaim' family of calls
data XAutoclaimResult resultFormat = XAutoclaimResult {
    xAutoclaimResultId :: ByteString,
    -- ^ ID of message that should be used in the next 'xautoclaim' call as a start parameter.
    xAutoclaimClaimedMessages :: [resultFormat],
    -- ^ List of succesfully claimed messages.
    xAutoclaimDeletedMessages :: [ByteString]
    -- ^ List of the messages that are available in the PEL but already deleted from the stream.
} deriving (Show, Eq)

instance RedisResult a => RedisResult (XAutoclaimResult a) where
    decode (MultiBulk (Just [
        Bulk (Just xAutoclaimResultId) ,
        claimedMsg,
        deletedMsg])) = do
            xAutoclaimClaimedMessages <- decode claimedMsg
            xAutoclaimDeletedMessages <- decode deletedMsg
            Right XAutoclaimResult{..}
    decode (MultiBulk (Just [
        Bulk (Just xAutoclaimResultId) ,
        MultiBulk (Just [])
        ])) = do
            let xAutoclaimClaimedMessages = []
            let xAutoclaimDeletedMessages = []
            Right XAutoclaimResult{..}
    decode a = Left a

-- | Version of the autoclaim result that contains data of the messages.
type XAutoclaimStreamsResult = XAutoclaimResult StreamsRecord
-- | Version of the autoclaim result that contains only IDs.
type XAutoclaimJustIdsResult = XAutoclaimResult ByteString

-- | /O(1)/ Transfers ownership of pending stream entries that match
-- the specified criteria. The message should be pending for more than \<min-idle-time\>
-- milliseconds and ID should be greater than \<start\>.
--
-- @XAUTOCLAIM \<stream name\> \<consumer group name\> \<min idle time\> \<start\>@
--
-- This version of function  claims no more than 100 mesages, use 'xautoclaimOpt' to
-- override this behavior.
--
-- Since Redis 7.0: fails on ealier versions.
xautoclaim
    :: (RedisCtx m f)
    => ByteString -- ^ Stream name.
    -> ByteString -- ^ Consumer group name.
    -> ByteString -- ^ Consumer name.
    -> Integer -- ^ Min idle time (ms).
    -> ByteString -- ^ ID of the message to start.
    -> m (f XAutoclaimStreamsResult)
xautoclaim key group consumer min_idle_time start = xautoclaimOpts key group consumer min_idle_time start defaultXAutoclaimOpts

-- | /O(1) if count is small/. Transfers ownership of pending stream entries that match
-- the specified criteria. See 'xautoclaim' for details.
--
-- Allows to pass additional optional parameters to set limit.
--
-- @XAUTOCLAIM \<stream name\> \<consumer group name\> \<min idle time\> \<start\> COUNT \<count\>@
--
-- Since Redis 7.0: fails on the ealier versions.
xautoclaimOpts
    :: (RedisCtx m f)
    => ByteString -- ^ Stream name.
    -> ByteString -- ^ Consumer group name.
    -> ByteString -- ^ Consumer name.
    -> Integer -- ^ min idle time (ms).
    -> ByteString -- ^ start ID.
    -> XAutoclaimOpts -- ^ Additional parameters.
    -> m (f XAutoclaimStreamsResult)
xautoclaimOpts key group consumer min_idle_time start opts = sendRequest $
    ["XAUTOCLAIM", key, group, consumer, encode min_idle_time, start] ++ count
    where count  = maybe [] (("COUNT":) . (:[]) . encode) (xAutoclaimCount opts)

-- | /O(1)/ Transfers ownership of pending stream entries that match
-- the specified criteria. See 'xautoclaim' for more details about criteria.
--
-- This variant returns only id of the messages without data. This method
-- claims no more than 100 messages, see 'xautoclaimJustIdsOpts' for changing
-- this default.
--
-- @XAUTOCLAIM \<stream name\> \<consumer group name\> \<min idle time\> \<start\> JUSTID@
--
-- Since Redis 7.0: fails on the ealier versions.
xautoclaimJustIds
    :: (RedisCtx m f)
    => ByteString -- ^ Stream name.
    -> ByteString -- ^ Consumer group name.
    -> ByteString -- ^ Consumer name.
    -> Integer -- ^ Min idle time (ms).
    -> ByteString -- ^ start ID.
    -> m (f XAutoclaimJustIdsResult)
xautoclaimJustIds key group consumer min_idle_time start =
  xautoclaimJustIdsOpts key group consumer min_idle_time start defaultXAutoclaimOpts

-- | /O(1) if count is small/ Transfers ownership of pending stream entries that match
-- the specified criteria. See 'xautoclaim' for more details about criteria.
--
-- This variant returns only id of the messages without data and allows to set the maximum
-- number of messages to be claimed.
--
-- @XAUTOCLAIM \<stream name\> \<consumer group name\> \<min idle time\> \<start\> COUNT \<count\> JUSTID@
--
-- Since Redis 7.0: fails on the ealier versions.
xautoclaimJustIdsOpts
    :: (RedisCtx m f)
    => ByteString -- ^ Stream name.
    -> ByteString -- ^ Consumers group name.
    -> ByteString -- ^ Consumer namee.
    -> Integer -- ^ min idle time (ms).
    -> ByteString -- ^ Start ID.
    -> XAutoclaimOpts -- ^ Additional parametres.
    -> m (f XAutoclaimJustIdsResult)
xautoclaimJustIdsOpts key group consumer min_idle_time start opts = sendRequest $
    ["XAUTOCLAIM", key, group, consumer, encode min_idle_time, start] ++ count ++ ["JUSTID"]
    where count  = maybe [] (("COUNT":) . (:[]) . encode) (xAutoclaimCount opts)

data StreamsRecord = StreamsRecord
    { recordId :: ByteString
    , keyValues :: [(ByteString, ByteString)]
    } deriving (Show, Eq)

instance RedisResult StreamsRecord where
    decode (MultiBulk (Just [Bulk (Just recordId), MultiBulk (Just rawKeyValues)])) = do
        keyValuesList <- mapM decode rawKeyValues
        let keyValues = decodeKeyValues keyValuesList
        return StreamsRecord{..}
        where
            decodeKeyValues :: [ByteString] -> [(ByteString, ByteString)]
            decodeKeyValues (x:y:rest) = (x,y):decodeKeyValues rest
            decodeKeyValues _ = []
    decode a = Left a

data XReadOpts = XReadOpts
    { block :: Maybe Integer
    , recordCount :: Maybe Integer
    } deriving (Show, Eq)

-- |Redis default 'XReadOpts'. Equivalent to omitting all optional parameters.
--
-- @
-- XReadOpts
--     { block = Nothing -- Don't block waiting for more records
--     , recordCount    = Nothing   -- no record count
--     }
-- @
--
defaultXreadOpts :: XReadOpts
defaultXreadOpts = XReadOpts { block = Nothing, recordCount = Nothing }

data XReadResponse = XReadResponse
    { stream :: ByteString
    , records :: [StreamsRecord]
    } deriving (Show, Eq)

instance RedisResult XReadResponse where
    decode (MultiBulk (Just [Bulk (Just stream), MultiBulk (Just rawRecords)])) = do
        records <- mapM decode rawRecords
        return XReadResponse{..}
    decode a = Left a

xreadOpts
    :: (RedisCtx m f)
    => [(ByteString, ByteString)] -- ^ (stream, id) pairs
    -> XReadOpts -- ^ Options
    -> m (f (Maybe [XReadResponse]))
xreadOpts streamsAndIds opts = sendRequest $
    ["XREAD"] ++ (internalXreadArgs streamsAndIds opts)

internalXreadArgs :: [(ByteString, ByteString)] -> XReadOpts -> [ByteString]
internalXreadArgs streamsAndIds XReadOpts{..} =
    concat [blockArgs, countArgs, ["STREAMS"], streams, recordIds]
    where
        blockArgs = maybe [] (\blockMillis -> ["BLOCK", encode blockMillis]) block
        countArgs = maybe [] (\countRecords -> ["COUNT", encode countRecords]) recordCount
        streams = map (\(stream, _) -> stream) streamsAndIds
        recordIds = map (\(_, recordId) -> recordId) streamsAndIds


xread
    :: (RedisCtx m f)
    => [(ByteString, ByteString)] -- ^ (stream, id) pairs
    -> m( f (Maybe [XReadResponse]))
xread streamsAndIds = xreadOpts streamsAndIds defaultXreadOpts

data XReadGroupOpts = XReadGroupOpts
    { xReadGroupBlock :: Maybe Integer
    , xReadGroupCount :: Maybe Integer
    , xReadGroupNoAck :: Bool
    } deriving (Show, Eq)

defaultXReadGroupOpts :: XReadGroupOpts
defaultXReadGroupOpts = XReadGroupOpts
    { xReadGroupBlock = Nothing
    , xReadGroupCount = Nothing
    , xReadGroupNoAck = False
    }

xreadGroupOpts
    :: (RedisCtx m f)
    => ByteString -- ^ group name
    -> ByteString -- ^ consumer name
    -> [(ByteString, ByteString)] -- ^ (stream, id) pairs
    -> XReadGroupOpts -- ^ Options
    -> m (f (Maybe [XReadResponse]))
xreadGroupOpts groupName consumerName streamsAndIds XReadGroupOpts{..} = sendRequest $
    ["XREADGROUP", "GROUP", groupName, consumerName] ++ internalXreadGroupArgs
    where
        internalXreadGroupArgs = concat [countArgs, blockArgs, noAckArgs, ["STREAMS"], streams, recordIds]
        blockArgs = maybe [] (\blockMillis -> ["BLOCK", encode blockMillis]) xReadGroupBlock
        countArgs = maybe [] (\countRecords -> ["COUNT", encode countRecords]) xReadGroupCount
        noAckArgs = ["NOACK" | xReadGroupNoAck]
        streams = map (\(stream, _) -> stream) streamsAndIds
        recordIds = map (\(_, recordId) -> recordId) streamsAndIds

xreadGroup
    :: (RedisCtx m f)
    => ByteString -- ^ group name
    -> ByteString -- ^ consumer name
    -> [(ByteString, ByteString)] -- ^ (stream, id) pairs
    -> m (f (Maybe [XReadResponse]))
xreadGroup groupName consumerName streamsAndIds = xreadGroupOpts groupName consumerName streamsAndIds defaultXReadGroupOpts

-- | Additional parameters of the XGroupCreate
data XGroupCreateOpts = XGroupCreateOpts
    { xGroupCreateMkStream :: Bool -- ^ If a stream does not exist, create it automatically with length of 0
    , xGroupCreateEntriesRead :: Maybe ByteString
    {- ^ Enable consumer group lag tracking, specify an arbitrary ID.
     An arbitrary ID is any ID that isn't the ID of the stream's first entry,
     last entry, or zero (@"0-0"@) ID. Use it to find out how many entries
     are between the arbitrary ID (excluding it) and the stream's last entry.

     Since Redis 7.0, fails if set on the ealier versions.
    -}
    } deriving (Show, Eq)

-- | Specifies default group opts.
--
-- Prefer using this method over use of constructor to preserve backwards compatibility.
defaultXGroupCreateOpts :: XGroupCreateOpts
defaultXGroupCreateOpts = XGroupCreateOpts{
    xGroupCreateEntriesRead = Nothing,
    xGroupCreateMkStream = False
}

-- | /O(1)/ Creates consumer group.
--
-- Fails if called on with the stream name that does not exist, use 'xgroupCreateOpts'
-- to override this behavior.
xgroupCreate
    :: (RedisCtx m f)
    => ByteString -- ^ Stream name.
    -> ByteString -- ^ Consumer group name.
    -> ByteString -- ^ ID of the message to start reading with.
    -> m (f Status)
xgroupCreate stream groupName startId = xgroupCreateOpts stream groupName startId defaultXGroupCreateOpts

-- | /O(1)/ Creates consumer group, accepts additional parameters.
xgroupCreateOpts
    :: (RedisCtx m f)
    => ByteString -- ^ Stream name.
    -> ByteString -- ^ Consumer group name.
    -> ByteString -- ^ ID of the message to start reading with.
    -> XGroupCreateOpts -- ^ Additional parameters.
    -> m (f Status)
xgroupCreateOpts stream groupName startId opts = sendRequest $ ["XGROUP", "CREATE", stream, groupName, startId] ++ args
    where args = mkstream ++ entriesRead
          mkstream    = ["MKSTREAM" | xGroupCreateMkStream opts]
          entriesRead = maybe []  (("ENTRIESREAD":) . (:[])) (xGroupCreateEntriesRead opts)

-- | /O(1)/ Creates new consumer in the consumers group.
--
-- Since redis 6.2.0: fails on the ealier versions.
xgroupCreateConsumer
    :: (RedisCtx m f)
    => ByteString -- ^ Stream name.
    -> ByteString -- ^ Consumer group name.
    -> ByteString -- ^ Consumer name.
    -> m (f Bool) -- ^ Returns if the consumer was created or not.
xgroupCreateConsumer key group consumer = sendRequest ["XGROUP", "CREATECONSUMER", key, group, consumer]

-- | /O(1)/ Sets last delivered id for a consumer group.
xgroupSetId
    :: (RedisCtx m f)
    => ByteString -- ^ Stream name.
    -> ByteString -- ^ Consumr group name.
    -> ByteString -- ^ Message ID or @$@
    -> m (f Status)
xgroupSetId stream group messageId = xgroupSetIdOpts stream group messageId defaultXGroupSetIdOpts

-- | Additional parameters for the 'xgroupSetId' method
newtype XGroupSetIdOpts = XGroupSetIdOpts {
    xGroupSetIdEntriesRead :: Maybe ByteString
    {- ^ Enable consumer group lag tracking for an arbitrary ID. An arbitrary ID is any ID that isn't the ID of the stream's first entry, its last entry or the zero (@"0-0"@) ID

    @since Redis 7.0, fails if set to Just on ealier versions.
    -}
}

-- | Default value for the 'XGroupSetIdOpts'.
--
-- Prefer use this method over the raw constructor in order to preserve
-- backwards compatibility.
defaultXGroupSetIdOpts :: XGroupSetIdOpts
defaultXGroupSetIdOpts = XGroupSetIdOpts {xGroupSetIdEntriesRead = Nothing}

-- | /O(1)/ a variant of the 'xgroupSetId' that allowes to pass additional parameters.
xgroupSetIdOpts
    :: (RedisCtx m f)
    => ByteString -- ^ Stream name.
    -> ByteString -- ^ Consumer group name.
    -> ByteString -- ^ Message id or @$S
    -> XGroupSetIdOpts -- ^ Additional parameters.
    -> m (f Status)
xgroupSetIdOpts stream group messageId opts = sendRequest $ ["XGROUP", "SETID", stream, group, messageId] ++ entriesRead
    where entriesRead = maybe [] (("ENTRIESREAD":) . (:[])) (xGroupSetIdEntriesRead opts)

-- | /O(1)/ Delete consumer.
xgroupDelConsumer
    :: (RedisCtx m f)
    => ByteString -- ^ Stream name.
    -> ByteString -- ^ Consumer group name.
    -> ByteString -- ^ Consumer name.
    -> m (f Integer) -- ^ The number of pending messages owned by the consumer.
xgroupDelConsumer stream group consumer = sendRequest ["XGROUP", "DELCONSUMER", stream, group, consumer]

-- | /O(1)/ destroys a group.
xgroupDestroy
    :: (RedisCtx m f)
    => ByteString -- ^ Stream name.
    -> ByteString -- ^ Consumer group name.
    -> m (f Bool)  -- ^ Tells if the group was destroyed or not.
xgroupDestroy stream group = sendRequest ["XGROUP", "DESTROY", stream, group]

xack
    :: (RedisCtx m f)
    => ByteString -- ^ stream
    -> ByteString -- ^ group name
    -> [ByteString] -- ^ message IDs
    -> m (f Integer)
xack stream groupName messageIds = sendRequest $ ["XACK", stream, groupName] ++ messageIds

xrange
    :: (RedisCtx m f)
    => ByteString -- ^ stream
    -> ByteString -- ^ start
    -> ByteString -- ^ end
    -> Maybe Integer -- ^ COUNT
    -> m (f [StreamsRecord])
xrange stream start end count = sendRequest $ ["XRANGE", stream, start, end] ++ countArgs
    where countArgs = maybe [] (\c -> ["COUNT", encode c]) count

xrevRange
    :: (RedisCtx m f)
    => ByteString -- ^ stream
    -> ByteString -- ^ end
    -> ByteString -- ^ start
    -> Maybe Integer -- ^ COUNT
    -> m (f [StreamsRecord])
xrevRange stream end start count = sendRequest $ ["XREVRANGE", stream, end, start] ++ countArgs
    where countArgs = maybe [] (\c -> ["COUNT", encode c]) count

xlen
    :: (RedisCtx m f)
    => ByteString -- ^ stream
    -> m (f Integer)
xlen stream = sendRequest ["XLEN", stream]

data XPendingSummaryResponse = XPendingSummaryResponse
    { numPendingMessages :: Integer
    , smallestPendingMessageId :: ByteString
    , largestPendingMessageId :: ByteString
    , numPendingMessagesByconsumer :: [(ByteString, Integer)]
    } deriving (Show, Eq)

instance RedisResult XPendingSummaryResponse where
    decode (MultiBulk (Just [
        Integer numPendingMessages,
        Bulk (Just smallestPendingMessageId),
        Bulk (Just largestPendingMessageId),
        MultiBulk (Just [MultiBulk (Just rawGroupsAndCounts)])])) = do
            let groupsAndCounts = chunksOfTwo rawGroupsAndCounts
            numPendingMessagesByconsumer <- decodeGroupsAndCounts groupsAndCounts
            return XPendingSummaryResponse{..}
            where
                decodeGroupsAndCounts :: [(Reply, Reply)] -> Either Reply [(ByteString, Integer)]
                decodeGroupsAndCounts bs = sequence $ map decodeGroupCount bs
                decodeGroupCount :: (Reply, Reply) -> Either Reply (ByteString, Integer)
                decodeGroupCount (x, y) = do
                    decodedX <- decode x
                    decodedY <- decode y
                    return (decodedX, decodedY)
                chunksOfTwo (x:y:rest) = (x,y):chunksOfTwo rest
                chunksOfTwo _ = []
    decode a = Left a

-- | /O(N)/ N - number of message beign returned.
--
-- Get information about pending messages (https://redis.io/commands/xpending).
--
-- Since Redis 5.0.
xpendingSummary
    :: (RedisCtx m f)
    => ByteString -- ^ Stream name.
    -> ByteString -- ^ Stream consumer group.
    -> m (f XPendingSummaryResponse)
xpendingSummary stream group = sendRequest $ ["XPENDING", stream, group]

-- | Details about message returned by the 'xpendingDetails'
data XPendingDetailRecord = XPendingDetailRecord
    { messageId :: ByteString
    , consumer :: ByteString
    , millisSinceLastDelivered :: Integer
    , numTimesDelivered :: Integer
    } deriving (Show, Eq)

instance RedisResult XPendingDetailRecord where
    decode (MultiBulk (Just [
        Bulk (Just messageId) ,
        Bulk (Just consumer),
        Integer millisSinceLastDelivered,
        Integer numTimesDelivered])) = Right XPendingDetailRecord{..}
    decode a = Left a

-- | Additional parameters of the xpending call family
data XPendingDetailOpts = XPendingDetailOpts
  {
    xPendingDetailConsumer :: Maybe ByteString, -- ^ Fetch the messages having a specific owner.
    xPendingDetailIdle :: Maybe Integer
    {- ^  Filter pending stream entries by their idle-time, ms

    Since Redis 6.2: Just values will fail
    -}
  }

-- | Default 'XPendingOpts' values.
--
-- Prefer this method over use of the constructor in order to preserve
-- backwards compatibility.
defaultXPendingDetailOpts :: XPendingDetailOpts
defaultXPendingDetailOpts = XPendingDetailOpts {
    xPendingDetailConsumer = Nothing,
    xPendingDetailIdle     = Nothing
}

-- | /O(N)/ N - number of messages returned.
--
-- Get detailed information about pending messages (https://redis.io/commands/xpending).
xpendingDetail
    :: (RedisCtx m f)
    => ByteString -- ^ Stream name.
    -> ByteString -- ^ Consumer group name.
    -> ByteString -- ^ ID of the first interesting message.
    -> ByteString -- ^ ID of the last intersting message.
    -> Integer -- ^ Limits the numbere of messages returned from the call.
    -> XPendingDetailOpts
    -> m (f [XPendingDetailRecord])
xpendingDetail stream group startId endId count opts = sendRequest $
    ["XPENDING", stream, group] ++ idleArg ++ [startId, endId, encode count] ++ consumerArg
    where consumerArg = maybeToList (xPendingDetailConsumer opts)
          idleArg = maybe [] (("IDLE":) . (:[]) . encode) (xPendingDetailIdle opts)

data XClaimOpts = XClaimOpts
    { xclaimIdle :: Maybe Integer
    , xclaimTime :: Maybe Integer
    , xclaimRetryCount :: Maybe Integer
    , xclaimForce :: Bool
    } deriving (Show, Eq)

defaultXClaimOpts :: XClaimOpts
defaultXClaimOpts = XClaimOpts
    { xclaimIdle = Nothing
    , xclaimTime = Nothing
    , xclaimRetryCount = Nothing
    , xclaimForce = False
    }


-- |Format a request for XCLAIM.
xclaimRequest
    :: ByteString -- ^ stream
    -> ByteString -- ^ group
    -> ByteString -- ^ consumer
    -> Integer -- ^ min idle time
    -> XClaimOpts -- ^ optional arguments
    -> [ByteString] -- ^ message IDs
    -> [ByteString]
xclaimRequest stream group consumer minIdleTime XClaimOpts{..} messageIds =
    ["XCLAIM", stream, group, consumer, encode minIdleTime] ++ ( map encode messageIds ) ++ optArgs
    where optArgs = idleArg ++ timeArg ++ retryCountArg ++ forceArg
          idleArg = optArg "IDLE" xclaimIdle
          timeArg = optArg "TIME" xclaimTime
          retryCountArg = optArg "RETRYCOUNT" xclaimRetryCount
          forceArg = if xclaimForce then ["FORCE"] else []
          optArg name maybeArg = maybe [] (\x -> [name, encode x]) maybeArg

xclaim
    :: (RedisCtx m f)
    => ByteString -- ^ stream
    -> ByteString -- ^ group
    -> ByteString -- ^ consumer
    -> Integer -- ^ min idle time
    -> XClaimOpts -- ^ optional arguments
    -> [ByteString] -- ^ message IDs
    -> m (f [StreamsRecord])
xclaim stream group consumer minIdleTime opts messageIds = sendRequest $
    xclaimRequest stream group consumer minIdleTime opts messageIds

xclaimJustIds
    :: (RedisCtx m f)
    => ByteString -- ^ stream
    -> ByteString -- ^ group
    -> ByteString -- ^ consumer
    -> Integer -- ^ min idle time
    -> XClaimOpts -- ^ optional arguments
    -> [ByteString] -- ^ message IDs
    -> m (f [ByteString])
xclaimJustIds stream group consumer minIdleTime opts messageIds = sendRequest $
    (xclaimRequest stream group consumer minIdleTime opts messageIds) ++ ["JUSTID"]

-- | Data structure that is returned as a result of  'xinfoConsumers'
data XInfoConsumersResponse = XInfoConsumersResponse
    { xinfoConsumerName :: ByteString -- ^ The name of the consumer.
    , xinfoConsumerNumPendingMessages :: Integer -- ^ The number of entries in the PEL (pending elemeent list): pending messages for the consumer, which are messages that were delivered but are yet to be acknowledged
    , xinfoConsumerIdleTime :: Integer -- ^ The number of milliseconds that have passed since the consumer's last attempted interaction (Examples: 'xreadGroup', 'xclam', 'xautoclaim')
    , xinfoConsumerInactive :: Maybe Integer
    {- ^ The number of milliseconds that have passed since the consumer's last successful interaction (Examples: 'xreadGroup' that actually read some entries into the PEL, 'xclaim'/'xautoclaim' that actually claimed some entries)

    @since Redis 7.0: always @Nothing@ for previous versions.
    -}
    } deriving (Show, Eq)

instance RedisResult XInfoConsumersResponse where
    decode = decodeRedis6 <> decodeRedis7
      where decodeRedis6 (MultiBulk (Just [
                Bulk (Just "name"),
                Bulk (Just xinfoConsumerName),
                Bulk (Just "pending"),
                Integer xinfoConsumerNumPendingMessages,
                Bulk (Just "idle"),
                Integer xinfoConsumerIdleTime])) = Right XInfoConsumersResponse{xinfoConsumerInactive = Nothing, ..}
            decodeRedis6 a = Left a

            decodeRedis7 (MultiBulk (Just [
                Bulk (Just "name"),
                Bulk (Just xinfoConsumerName),
                Bulk (Just "pending"),
                Integer xinfoConsumerNumPendingMessages,
                Bulk (Just "idle"),
                Integer xinfoConsumerIdleTime,
                Bulk (Just "inactive"),
                Integer xinfoConsumerInactive])) = Right XInfoConsumersResponse{xinfoConsumerInactive = Just xinfoConsumerInactive, ..}
            decodeRedis7 a = Left a

-- | /O(1)/
-- Returns information about the list of the consumers beloging to the consumer group.
--
-- Available since Redis 5.0.0
--
-- Wrapper over @XINFO CONSUMERS \<stream name\> \<group name\>@
xinfoConsumers
    :: (RedisCtx m f)
    => ByteString -- ^ Stream name.
    -> ByteString -- ^ Group name.
    -> m (f [XInfoConsumersResponse])
xinfoConsumers stream group = sendRequest $ ["XINFO", "CONSUMERS", stream, group]

-- | Result of the 'xinfoGroups' call.
data XInfoGroupsResponse = XInfoGroupsResponse
    { xinfoGroupsGroupName :: ByteString -- ^ Name of the consumer group.
    , xinfoGroupsNumConsumers :: Integer -- ^ The number of consumers in the group.
    , xinfoGroupsNumPendingMessages :: Integer -- ^ The length of the group's pending entries list (PEL), which are messages that were delivered but are yet to be acknowledged.
    , xinfoGroupsLastDeliveredMessageId :: ByteString -- ^ The ID of the last entry delivered to the group's consumers.
    , xinfoGroupsEntriesRead :: Maybe Integer
    {- ^ The logical "read counter" of the last entry delivered to group's consumers.

    Since Redis 7.0: always @Nothing@ on the previous versions.
    -}
    , xinfoGroupsLag :: Maybe Integer
    {- ^ the number of entries in the stream that are still waiting to be delivered to the group's consumers, or a Nothing when that number can't be determined.

    Since Redis 7.0: always @Nothing@ on the previous versions.
    -}
    } deriving (Show, Eq)

instance RedisResult XInfoGroupsResponse where
    decode = decodeRedis6 <> decodeRedis7
      where decodeRedis6 (MultiBulk (Just [
              Bulk (Just "name"),      Bulk (Just xinfoGroupsGroupName),
              Bulk (Just "consumers"), Integer xinfoGroupsNumConsumers,
              Bulk (Just "pending"),   Integer xinfoGroupsNumPendingMessages,
              Bulk (Just "last-delivered-id"),
              Bulk (Just xinfoGroupsLastDeliveredMessageId)])) =
                Right XInfoGroupsResponse{
                      xinfoGroupsEntriesRead = Nothing
                    , xinfoGroupsLag         = Nothing
                    , ..}
            decodeRedis6 a = Left a

            decodeRedis7 (MultiBulk (Just [
              Bulk (Just "name"),              Bulk (Just xinfoGroupsGroupName),
              Bulk (Just "consumers"),         Integer xinfoGroupsNumConsumers,
              Bulk (Just "pending"),           Integer xinfoGroupsNumPendingMessages,
              Bulk (Just "last-delivered-id"), Bulk (Just xinfoGroupsLastDeliveredMessageId),
              Bulk (Just "entries-read"),      Integer xinfoGroupsEntriesRead,
              Bulk (Just "lag"),               Integer xinfoGroupsLag])) =
                Right XInfoGroupsResponse{
                      xinfoGroupsEntriesRead = Just xinfoGroupsEntriesRead
                    , xinfoGroupsLag         = Just xinfoGroupsLag
                    , ..}
            decodeRedis7 a = Left a

-- | /O(1)/ Returns information about the groups.
--
-- Available since: Redis 5.0.0
--
-- Wrapper around @XINFO GROUPS \<stream name\>@ call.
xinfoGroups
    :: (RedisCtx m f)
    => ByteString -- ^ Stream name.
    -> m (f [XInfoGroupsResponse])
xinfoGroups stream = sendRequest ["XINFO", "GROUPS", stream]

data XInfoStreamResponse
    = XInfoStreamResponse
    { xinfoStreamLength :: Integer -- ^ The number of entries in the stream.
    , xinfoStreamRadixTreeKeys :: Integer -- ^ The number of keys in the underlying radix data structure.
    , xinfoStreamRadixTreeNodes :: Integer -- ^ The number of nodes in the underlying radix data structure.
    , xinfoMaxDeletedEntryId :: Maybe ByteString
    {- ^ The maximal entry ID that was deleted from the stream

    Since Redis 7.0: always returns @Nothing@ on the previous versions.
    -}
    , xinfoEntriesAdded :: Maybe Integer
    {- ^ The count of all entries added to the stream during its lifetime

    Since Redis 7.0: always returns @Nothing@ on the previous versions.
    -}
    , xinfoRecordedFirstEntryId :: Maybe ByteString
    {- ^ ID of first recorded entry.

    Since Redis 7.0: always returns @Nothing@ on the previous versions.
    -}
    , xinfoStreamNumGroups :: Integer -- ^ The number of consumer groups defined for the stream.
    , xinfoStreamLastEntryId :: ByteString -- ^ ID of the last entry in the stream.
    , xinfoStreamFirstEntry :: StreamsRecord -- ^ ID and field-value tuples of the first entry in the stream.
    , xinfoStreamLastEntry :: StreamsRecord -- ^ ID and field-value tuples of the last entry in the stream.
    }
    | XInfoStreamEmptyResponse
    { xinfoStreamLength :: Integer -- ^ The number of entries in the stream.
    , xinfoStreamRadixTreeKeys :: Integer -- ^ The number of keys in the underlying radix data structure.
    , xinfoStreamRadixTreeNodes :: Integer -- ^ The number of nodes in the underlying radix data structure.
    , xinfoMaxDeletedEntryId :: Maybe ByteString
    {- ^ The maximal entry ID that was deleted from the stream.

    Since Redis 7.0: always returns @Nothing@ on the previous versions.
    -}
    , xinfoEntriesAdded :: Maybe Integer
    {- ^ The count of all entries added to the stream during its lifetime

    Since Redis 7.0: always returns @Nothing@ on the previous versions.
    -}
    , xinfoRecordedFirstEntryId :: Maybe ByteString
    {- ^ ID of first recorded entry.

    Since Redis 7.0: always returns @Nothing@ on the previous versions.
    -}
    , xinfoStreamNumGroups :: Integer -- ^ The number of consumer groups defined for the stream.
    , xinfoStreamLastEntryId :: ByteString -- ^ The ID of the last entry in the stream.
    }
    deriving (Show, Eq)

instance RedisResult XInfoStreamResponse where
    decode = decodeRedis5 <> decodeRedis6 <> decodeRedis7
        where
            decodeRedis5 (MultiBulk (Just [
                 Bulk (Just "length"),            Integer xinfoStreamLength,
                 Bulk (Just "radix-tree-keys"),   Integer xinfoStreamRadixTreeKeys,
                 Bulk (Just "radix-tree-nodes"),  Integer xinfoStreamRadixTreeNodes,
                 Bulk (Just "groups"),            Integer xinfoStreamNumGroups,
                 Bulk (Just "last-generated-id"), Bulk (Just xinfoStreamLastEntryId),
                 Bulk (Just "first-entry"),       Bulk Nothing ,
                 Bulk (Just "last-entry"),        Bulk Nothing ])) = do
                    return XInfoStreamEmptyResponse{
                          xinfoMaxDeletedEntryId    = Nothing
                        , xinfoEntriesAdded         = Nothing
                        , xinfoRecordedFirstEntryId = Nothing
                        , ..}
            decodeRedis5 (MultiBulk (Just [
                Bulk (Just "length"),            Integer xinfoStreamLength,
                Bulk (Just "radix-tree-keys"),   Integer xinfoStreamRadixTreeKeys,
                Bulk (Just "radix-tree-nodes"),  Integer xinfoStreamRadixTreeNodes,
                Bulk (Just "groups"),            Integer xinfoStreamNumGroups,
                Bulk (Just "last-generated-id"), Bulk (Just xinfoStreamLastEntryId),
                Bulk (Just "first-entry"),       rawFirstEntry ,
                Bulk (Just "last-entry"),        rawLastEntry ])) = do
                    xinfoStreamFirstEntry <- decode rawFirstEntry
                    xinfoStreamLastEntry  <- decode rawLastEntry
                    return XInfoStreamResponse{
                          xinfoMaxDeletedEntryId    = Nothing
                        , xinfoEntriesAdded         = Nothing
                        , xinfoRecordedFirstEntryId = Nothing
                        , ..}
            decodeRedis5 a = Left a

            decodeRedis6 (MultiBulk (Just [
                Bulk (Just "length"),            Integer xinfoStreamLength,
                Bulk (Just "radix-tree-keys"),   Integer xinfoStreamRadixTreeKeys,
                Bulk (Just "radix-tree-nodes"),  Integer xinfoStreamRadixTreeNodes,
                Bulk (Just "last-generated-id"), Bulk (Just xinfoStreamLastEntryId),
                Bulk (Just "groups"),            Integer xinfoStreamNumGroups,
                Bulk (Just "first-entry"),       Bulk Nothing ,
                Bulk (Just "last-entry"),        Bulk Nothing ])) = do
                    return XInfoStreamEmptyResponse{
                          xinfoMaxDeletedEntryId    = Nothing
                        , xinfoEntriesAdded         = Nothing
                        , xinfoRecordedFirstEntryId = Nothing
                        , ..}
            decodeRedis6 (MultiBulk (Just [
                Bulk (Just "length"),            Integer xinfoStreamLength,
                Bulk (Just "radix-tree-keys"),   Integer xinfoStreamRadixTreeKeys,
                Bulk (Just "radix-tree-nodes"),  Integer xinfoStreamRadixTreeNodes,
                Bulk (Just "last-generated-id"), Bulk (Just xinfoStreamLastEntryId),
                Bulk (Just "groups"),            Integer xinfoStreamNumGroups,
                Bulk (Just "first-entry"),       rawFirstEntry ,
                Bulk (Just "last-entry"),        rawLastEntry ])) = do
                    xinfoStreamFirstEntry <- decode rawFirstEntry
                    xinfoStreamLastEntry  <- decode rawLastEntry
                    return XInfoStreamResponse{
                          xinfoMaxDeletedEntryId    = Nothing
                        , xinfoEntriesAdded         = Nothing
                        , xinfoRecordedFirstEntryId = Nothing
                        , ..}
            decodeRedis6 a = Left a

            decodeRedis7 (MultiBulk (Just [
                Bulk (Just "length"),                  Integer xinfoStreamLength,
                Bulk (Just "radix-tree-keys"),         Integer xinfoStreamRadixTreeKeys,
                Bulk (Just "radix-tree-nodes"),        Integer xinfoStreamRadixTreeNodes,
                Bulk (Just "last-generated-id"),       Bulk (Just xinfoStreamLastEntryId),
                Bulk (Just "max-deleted-entry-id"),    Bulk (Just xinfoMaxDeletedEntryId),
                Bulk (Just "entries-added"),           Integer xinfoEntriesAdded,
                Bulk (Just "recorded-first-entry-id"), Bulk (Just xinfoRecordedFirstEntryId),
                Bulk (Just "groups"),                  Integer xinfoStreamNumGroups,
                Bulk (Just "first-entry"),             Bulk Nothing ,
                Bulk (Just "last-entry"),              Bulk Nothing ])) = do
                    return XInfoStreamEmptyResponse{
                          xinfoMaxDeletedEntryId    = Just xinfoMaxDeletedEntryId
                        , xinfoEntriesAdded         = Just xinfoEntriesAdded
                        , xinfoRecordedFirstEntryId = Just xinfoRecordedFirstEntryId
                        , ..}

            decodeRedis7 (MultiBulk (Just [
                Bulk (Just "length"),                  Integer xinfoStreamLength,
                Bulk (Just "radix-tree-keys"),         Integer xinfoStreamRadixTreeKeys,
                Bulk (Just "radix-tree-nodes"),        Integer xinfoStreamRadixTreeNodes,
                Bulk (Just "last-generated-id"),       Bulk (Just xinfoStreamLastEntryId),
                Bulk (Just "max-deleted-entry-id"),    Bulk (Just xinfoMaxDeletedEntryId),
                Bulk (Just "entries-added"),           Integer xinfoEntriesAdded,
                Bulk (Just "recorded-first-entry-id"), Bulk (Just xinfoRecordedFirstEntryId),
                Bulk (Just "groups"),                  Integer xinfoStreamNumGroups,
                Bulk (Just "first-entry"),          rawFirstEntry ,
                Bulk (Just "last-entry"),           rawLastEntry ])) = do
                    xinfoStreamFirstEntry <- decode rawFirstEntry
                    xinfoStreamLastEntry  <- decode rawLastEntry
                    return XInfoStreamResponse{
                          xinfoMaxDeletedEntryId    = Just xinfoMaxDeletedEntryId
                        , xinfoEntriesAdded         = Just xinfoEntriesAdded
                        , xinfoRecordedFirstEntryId = Just xinfoRecordedFirstEntryId
                        , ..}
            decodeRedis7 a = Left a

-- | Get info about a stream. The Redis command @XINFO@ is split into 'xinfoConsumers', 'xinfoGroups', and 'xinfoStream'.
-- Since Redis 5.0.0
xinfoStream
    :: (RedisCtx m f)
    => ByteString -- ^ stream
    -> m (f XInfoStreamResponse)
xinfoStream stream = sendRequest ["XINFO", "STREAM", stream]

-- | Delete messages from a stream.
-- Since Redis 5.0.0
xdel
    :: (RedisCtx m f)
    => ByteString -- ^ stream
    -> NonEmpty ByteString -- ^ message IDs
    -> m (f Integer)
xdel stream (messageId:|messageIds) = sendRequest ("XDEL":stream:messageId: messageIds)

-- |Set the upper bound for number of messages in a stream. Since Redis 5.0.0
xtrim
    :: (RedisCtx m f)
    => ByteString -- ^ stream
    -> TrimOpts
    -> m (f Integer)
xtrim stream opts = sendRequest ("XTRIM":stream:internalTrimArgToList opts)

-- |Constructor for `inf` Redis argument values
inf :: RealFloat a => a
inf = 1 / 0

-- | Additional parameters for the auth command.
data AuthOpts = AuthOpts
  { authOptsUsername
    :: Maybe ByteString
    {- ^ Username.

      Since Redis 6.0: fails on earlier
     -}
  }
  deriving Show

-- | Default options for AuthOpts
--
-- >>> defaultAuthOpts
-- AuthOpts {authOptsUsername = Nothing}
defaultAuthOpts :: AuthOpts
defaultAuthOpts = AuthOpts
  { authOptsUsername = Nothing
  }

-- | /O(N)/ where N is the number of passwords defined for the user.
--
-- Authenticates client to the server.
auth
    :: RedisCtx m f
    => ByteString -- ^ Password.
    -> m (f Status)
auth password = authOpts password defaultAuthOpts

-- | /O(N)/ where N is the number of passwords defined for the user.
--
-- Authenticates client to the server.
--
-- This method allows passing additional options.
authOpts
    :: RedisCtx m f
    => ByteString -- ^ Password.
    -> AuthOpts -- ^ Additional options.
    -> m (f Status)
authOpts password AuthOpts{..} = sendRequest $
  ["AUTH"] <> maybe [] (:[]) authOptsUsername <> [password]

-- |Change the selected database for the current connection (<http://redis.io/commands/select>). Since Redis 1.0.0
select
    :: RedisCtx m f
    => Integer -- ^ index
    -> m (f Status)
select ix = sendRequest ["SELECT", encode ix]

-- |Ping the server (<http://redis.io/commands/ping>). Since Redis 1.0.0
ping
    :: (RedisCtx m f)
    => m (f Status)
ping  = sendRequest (["PING"] )

-- https://redis.io/commands/cluster-info/
data ClusterInfoResponse = ClusterInfoResponse
  { clusterInfoResponseState :: ClusterInfoResponseState,
    clusterInfoResponseSlotsAssigned :: Integer,
    clusterInfoResponseSlotsOK :: Integer,
    clusterInfoResponseSlotsPfail :: Integer,
    clusterInfoResponseSlotsFail :: Integer,
    clusterInfoResponseKnownNodes :: Integer,
    clusterInfoResponseSize :: Integer,
    clusterInfoResponseCurrentEpoch :: Integer,
    clusterInfoResponseMyEpoch :: Integer,
    clusterInfoResponseStatsMessagesSent :: Integer,
    clusterInfoResponseStatsMessagesReceived :: Integer,
    clusterInfoResponseTotalLinksBufferLimitExceeded :: Integer,
    clusterInfoResponseStatsMessagesPingSent :: Maybe Integer,
    clusterInfoResponseStatsMessagesPingReceived :: Maybe Integer,
    clusterInfoResponseStatsMessagesPongSent :: Maybe Integer,
    clusterInfoResponseStatsMessagesPongReceived :: Maybe Integer,
    clusterInfoResponseStatsMessagesMeetSent :: Maybe Integer,
    clusterInfoResponseStatsMessagesMeetReceived :: Maybe Integer,
    clusterInfoResponseStatsMessagesFailSent :: Maybe Integer,
    clusterInfoResponseStatsMessagesFailReceived :: Maybe Integer,
    clusterInfoResponseStatsMessagesPublishSent :: Maybe Integer,
    clusterInfoResponseStatsMessagesPublishReceived :: Maybe Integer,
    clusterInfoResponseStatsMessagesAuthReqSent :: Maybe Integer,
    clusterInfoResponseStatsMessagesAuthReqReceived :: Maybe Integer,
    clusterInfoResponseStatsMessagesAuthAckSent :: Maybe Integer,
    clusterInfoResponseStatsMessagesAuthAckReceived :: Maybe Integer,
    clusterInfoResponseStatsMessagesUpdateSent :: Maybe Integer,
    clusterInfoResponseStatsMessagesUpdateReceived :: Maybe Integer,
    clusterInfoResponseStatsMessagesMfstartSent :: Maybe Integer,
    clusterInfoResponseStatsMessagesMfstartReceived :: Maybe Integer,
    clusterInfoResponseStatsMessagesModuleSent :: Maybe Integer,
    clusterInfoResponseStatsMessagesModuleReceived :: Maybe Integer,
    clusterInfoResponseStatsMessagesPublishshardSent :: Maybe Integer,
    clusterInfoResponseStatsMessagesPublishshardReceived :: Maybe Integer
  }
  deriving (Show, Eq)

data ClusterInfoResponseState
  = OK
  | Down
  deriving (Show, Eq)

defClusterInfoResponse :: ClusterInfoResponse
defClusterInfoResponse =
  ClusterInfoResponse
    { clusterInfoResponseState = Down,
      clusterInfoResponseSlotsAssigned = 0,
      clusterInfoResponseSlotsOK = 0,
      clusterInfoResponseSlotsPfail = 0,
      clusterInfoResponseSlotsFail = 0,
      clusterInfoResponseKnownNodes = 0,
      clusterInfoResponseSize = 0,
      clusterInfoResponseCurrentEpoch = 0,
      clusterInfoResponseMyEpoch = 0,
      clusterInfoResponseStatsMessagesSent = 0,
      clusterInfoResponseStatsMessagesReceived = 0,
      clusterInfoResponseTotalLinksBufferLimitExceeded = 0,
      clusterInfoResponseStatsMessagesPingSent = Nothing,
      clusterInfoResponseStatsMessagesPingReceived = Nothing,
      clusterInfoResponseStatsMessagesPongSent = Nothing,
      clusterInfoResponseStatsMessagesPongReceived = Nothing,
      clusterInfoResponseStatsMessagesMeetSent = Nothing,
      clusterInfoResponseStatsMessagesMeetReceived = Nothing,
      clusterInfoResponseStatsMessagesFailSent = Nothing,
      clusterInfoResponseStatsMessagesFailReceived = Nothing,
      clusterInfoResponseStatsMessagesPublishSent = Nothing,
      clusterInfoResponseStatsMessagesPublishReceived = Nothing,
      clusterInfoResponseStatsMessagesAuthReqSent = Nothing,
      clusterInfoResponseStatsMessagesAuthReqReceived = Nothing,
      clusterInfoResponseStatsMessagesAuthAckSent = Nothing,
      clusterInfoResponseStatsMessagesAuthAckReceived = Nothing,
      clusterInfoResponseStatsMessagesUpdateSent = Nothing,
      clusterInfoResponseStatsMessagesUpdateReceived = Nothing,
      clusterInfoResponseStatsMessagesMfstartSent = Nothing,
      clusterInfoResponseStatsMessagesMfstartReceived = Nothing,
      clusterInfoResponseStatsMessagesModuleSent = Nothing,
      clusterInfoResponseStatsMessagesModuleReceived = Nothing,
      clusterInfoResponseStatsMessagesPublishshardSent = Nothing,
      clusterInfoResponseStatsMessagesPublishshardReceived = Nothing
    }

parseClusterInfoResponse :: [[ByteString]] -> ClusterInfoResponse -> Maybe ClusterInfoResponse
parseClusterInfoResponse fields resp = case fields of
  [] -> pure resp
  (["cluster_state", state] : fs) -> parseState state >>= \s -> parseClusterInfoResponse fs $ resp {clusterInfoResponseState = s}
  (["cluster_slots_assigned", value] : fs) -> parseInteger value >>= \v -> parseClusterInfoResponse fs $ resp {clusterInfoResponseSlotsAssigned = v}
  (["cluster_slots_ok", value] : fs) -> parseInteger value >>= \v -> parseClusterInfoResponse fs $ resp {clusterInfoResponseSlotsOK = v}
  (["cluster_slots_pfail", value] : fs) -> parseInteger value >>= \v -> parseClusterInfoResponse fs $ resp {clusterInfoResponseSlotsPfail = v}
  (["cluster_slots_fail", value] : fs) -> parseInteger value >>= \v -> parseClusterInfoResponse fs $ resp {clusterInfoResponseSlotsFail = v}
  (["cluster_known_nodes", value] : fs) -> parseInteger value >>= \v -> parseClusterInfoResponse fs $ resp {clusterInfoResponseKnownNodes = v}
  (["cluster_size", value] : fs) -> parseInteger value >>= \v -> parseClusterInfoResponse fs $ resp {clusterInfoResponseSize = v}
  (["cluster_current_epoch", value] : fs) -> parseInteger value >>= \v -> parseClusterInfoResponse fs $ resp {clusterInfoResponseCurrentEpoch = v}
  (["cluster_my_epoch", value] : fs) -> parseInteger value >>= \v -> parseClusterInfoResponse fs $ resp {clusterInfoResponseMyEpoch = v}
  (["cluster_stats_messages_sent", value] : fs) -> parseInteger value >>= \v -> parseClusterInfoResponse fs $ resp {clusterInfoResponseStatsMessagesSent = v}
  (["cluster_stats_messages_received", value] : fs) -> parseInteger value >>= \v -> parseClusterInfoResponse fs $ resp {clusterInfoResponseStatsMessagesReceived = v}
  (["total_cluster_links_buffer_limit_exceeded", value] : fs) -> parseClusterInfoResponse fs $ resp {clusterInfoResponseTotalLinksBufferLimitExceeded = fromMaybe 0 $ parseInteger value} -- this value should be mandatory according to the spec, but isn't necessarily set in Redis 6
  (["cluster_stats_messages_ping_sent", value] : fs) -> parseClusterInfoResponse fs $ resp {clusterInfoResponseStatsMessagesPingSent = parseInteger value}
  (["cluster_stats_messages_ping_received", value] : fs) -> parseClusterInfoResponse fs $ resp {clusterInfoResponseStatsMessagesPingReceived = parseInteger value}
  (["cluster_stats_messages_pong_sent", value] : fs) -> parseClusterInfoResponse fs $ resp {clusterInfoResponseStatsMessagesPongSent = parseInteger value}
  (["cluster_stats_messages_pong_received", value] : fs) -> parseClusterInfoResponse fs $ resp {clusterInfoResponseStatsMessagesPongReceived = parseInteger value}
  (["cluster_stats_messages_meet_sent", value] : fs) -> parseClusterInfoResponse fs $ resp {clusterInfoResponseStatsMessagesMeetSent = parseInteger value}
  (["cluster_stats_messages_meet_received", value] : fs) -> parseClusterInfoResponse fs $ resp {clusterInfoResponseStatsMessagesMeetReceived = parseInteger value}
  (["cluster_stats_messages_fail_sent", value] : fs) -> parseClusterInfoResponse fs $ resp {clusterInfoResponseStatsMessagesFailSent = parseInteger value}
  (["cluster_stats_messages_fail_received", value] : fs) -> parseClusterInfoResponse fs $ resp {clusterInfoResponseStatsMessagesFailReceived = parseInteger value}
  (["cluster_stats_messages_publish_sent", value] : fs) -> parseClusterInfoResponse fs $ resp {clusterInfoResponseStatsMessagesPublishSent = parseInteger value}
  (["cluster_stats_messages_publish_received", value] : fs) -> parseClusterInfoResponse fs $ resp {clusterInfoResponseStatsMessagesPublishReceived = parseInteger value}
  (["cluster_stats_messages_auth_req_sent", value] : fs) -> parseClusterInfoResponse fs $ resp {clusterInfoResponseStatsMessagesAuthReqSent = parseInteger value}
  (["cluster_stats_messages_auth_req_received", value] : fs) -> parseClusterInfoResponse fs $ resp {clusterInfoResponseStatsMessagesAuthReqReceived = parseInteger value}
  (["cluster_stats_messages_auth_ack_sent", value] : fs) -> parseClusterInfoResponse fs $ resp {clusterInfoResponseStatsMessagesAuthAckSent = parseInteger value}
  (["cluster_stats_messages_auth_ack_received", value] : fs) -> parseClusterInfoResponse fs $ resp {clusterInfoResponseStatsMessagesAuthAckReceived = parseInteger value}
  (["cluster_stats_messages_update_sent", value] : fs) -> parseClusterInfoResponse fs $ resp {clusterInfoResponseStatsMessagesUpdateSent = parseInteger value}
  (["cluster_stats_messages_update_received", value] : fs) -> parseClusterInfoResponse fs $ resp {clusterInfoResponseStatsMessagesUpdateReceived = parseInteger value}
  (["cluster_stats_messages_mfstart_sent", value] : fs) -> parseClusterInfoResponse fs $ resp {clusterInfoResponseStatsMessagesMfstartSent = parseInteger value}
  (["cluster_stats_messages_mfstart_received", value] : fs) -> parseClusterInfoResponse fs $ resp {clusterInfoResponseStatsMessagesMfstartReceived = parseInteger value}
  (["cluster_stats_messages_module_sent", value] : fs) -> parseClusterInfoResponse fs $ resp {clusterInfoResponseStatsMessagesModuleSent = parseInteger value}
  (["cluster_stats_messages_module_received", value] : fs) -> parseClusterInfoResponse fs $ resp {clusterInfoResponseStatsMessagesModuleReceived = parseInteger value}
  (["cluster_stats_messages_publishshard_sent", value] : fs) -> parseClusterInfoResponse fs $ resp {clusterInfoResponseStatsMessagesPublishshardSent = parseInteger value}
  (["cluster_stats_messages_publishshard_received", value] : fs) -> parseClusterInfoResponse fs $ resp {clusterInfoResponseStatsMessagesPublishshardReceived = parseInteger value}
  (_ : fs) -> parseClusterInfoResponse fs resp
  where
    parseState bs = case bs of
      "ok" -> Just OK
      "fail" -> Just Down
      _ -> Nothing
    parseInteger = fmap fst . Char8.readInteger

instance RedisResult ClusterInfoResponse where
  decode r@(Bulk (Just bulkData)) =
    maybe (Left r) Right
      . flip parseClusterInfoResponse defClusterInfoResponse
      . map (Char8.split ':' . Char8.takeWhile (/= '\r'))
      $ Char8.lines bulkData
  decode r = Left r

clusterInfo :: RedisCtx m f => m (f ClusterInfoResponse)
clusterInfo = sendRequest ["CLUSTER", "INFO"]

data ClusterNodesResponse = ClusterNodesResponse
    { clusterNodesResponseEntries :: [ClusterNodesResponseEntry]
    } deriving (Show, Eq)

data ClusterNodesResponseEntry = ClusterNodesResponseEntry { clusterNodesResponseNodeId :: ByteString
    , clusterNodesResponseNodeIp :: ByteString
    , clusterNodesResponseNodePort :: Integer
    , clusterNodesResponseNodeFlags :: [ByteString]
    , clusterNodesResponseMasterId :: Maybe ByteString
    , clusterNodesResponsePingSent :: Integer
    , clusterNodesResponsePongReceived :: Integer
    , clusterNodesResponseConfigEpoch :: Integer
    , clusterNodesResponseLinkState :: ByteString
    , clusterNodesResponseSlots :: [ClusterNodesResponseSlotSpec]
    } deriving (Show, Eq)

data ClusterNodesResponseSlotSpec
    = ClusterNodesResponseSingleSlot Integer
    | ClusterNodesResponseSlotRange Integer Integer
    | ClusterNodesResponseSlotImporting Integer ByteString
    | ClusterNodesResponseSlotMigrating Integer ByteString deriving (Show, Eq)


instance RedisResult ClusterNodesResponse where
    decode r@(Bulk (Just bulkData)) = maybe (Left r) Right $ do
        infos <- mapM parseNodeInfo $ Char8.lines bulkData
        return $ ClusterNodesResponse infos where
            parseNodeInfo :: ByteString -> Maybe ClusterNodesResponseEntry
            parseNodeInfo line = case Char8.words line of
              (nodeId : hostNamePort : flags : masterNodeId : pingSent : pongRecv : epoch : linkState : slots) ->
                case Char8.split ':' hostNamePort of
                  [hostName, port] -> ClusterNodesResponseEntry <$> pure nodeId
                                               <*> pure hostName
                                               <*> readInteger port
                                               <*> pure (Char8.split ',' flags)
                                               <*> pure (readMasterNodeId masterNodeId)
                                               <*> readInteger pingSent
                                               <*> readInteger pongRecv
                                               <*> readInteger epoch
                                               <*> pure linkState
                                               <*> (pure . catMaybes $ map readNodeSlot slots)
                  _ -> Nothing
              _ -> Nothing
            readInteger :: ByteString -> Maybe Integer
            readInteger = fmap fst . Char8.readInteger

            readMasterNodeId :: ByteString -> Maybe ByteString
            readMasterNodeId "-"    = Nothing
            readMasterNodeId nodeId = Just nodeId

            readNodeSlot :: ByteString -> Maybe ClusterNodesResponseSlotSpec
            readNodeSlot slotSpec = case '[' `Char8.elem` slotSpec of
                True -> readSlotImportMigrate slotSpec
                False -> case '-' `Char8.elem` slotSpec of
                    True -> readSlotRange slotSpec
                    False -> ClusterNodesResponseSingleSlot <$> readInteger slotSpec
            readSlotImportMigrate :: ByteString -> Maybe ClusterNodesResponseSlotSpec
            readSlotImportMigrate slotSpec = case BS.breakSubstring "->-" slotSpec of
                (_, "") -> case BS.breakSubstring "-<-" slotSpec of
                    (_, "") -> Nothing
                    (leftPart, rightPart) -> ClusterNodesResponseSlotImporting
                        <$> (readInteger $ Char8.drop 1 leftPart)
                        <*> (pure $ BS.take (BS.length rightPart - 1) rightPart)
                (leftPart, rightPart) -> ClusterNodesResponseSlotMigrating
                    <$> (readInteger $ Char8.drop 1 leftPart)
                    <*> (pure $ BS.take (BS.length rightPart - 1) rightPart)
            readSlotRange :: ByteString -> Maybe ClusterNodesResponseSlotSpec
            readSlotRange slotSpec = case BS.breakSubstring "-" slotSpec of
                (_, "") -> Nothing
                (leftPart, rightPart) -> ClusterNodesResponseSlotRange
                    <$> readInteger leftPart
                    <*> (readInteger $ BS.drop 1 rightPart)

    decode r = Left r

clusterNodes
    :: (RedisCtx m f)
    => m (f ClusterNodesResponse)
clusterNodes = sendRequest $ ["CLUSTER", "NODES"]

data ClusterSlotsResponse = ClusterSlotsResponse { clusterSlotsResponseEntries :: [ClusterSlotsResponseEntry] } deriving (Show)

data ClusterSlotsNode = ClusterSlotsNode
    { clusterSlotsNodeIP :: ByteString
    , clusterSlotsNodePort :: Int
    , clusterSlotsNodeID :: ByteString
    } deriving (Show)

data ClusterSlotsResponseEntry = ClusterSlotsResponseEntry
    { clusterSlotsResponseEntryStartSlot :: Int
    , clusterSlotsResponseEntryEndSlot :: Int
    , clusterSlotsResponseEntryMaster :: ClusterSlotsNode
    , clusterSlotsResponseEntryReplicas :: [ClusterSlotsNode]
    } deriving (Show)

instance RedisResult ClusterSlotsResponse where
    decode (MultiBulk (Just bulkData)) = do
        clusterSlotsResponseEntries <- mapM decode bulkData
        return ClusterSlotsResponse{..}
    decode a = Left a

instance RedisResult ClusterSlotsResponseEntry where
    decode (MultiBulk (Just
        ((Integer startSlot):(Integer endSlot):masterData:replicas))) = do
            clusterSlotsResponseEntryMaster <- decode masterData
            clusterSlotsResponseEntryReplicas <- mapM decode replicas
            let clusterSlotsResponseEntryStartSlot = fromInteger startSlot
            let clusterSlotsResponseEntryEndSlot = fromInteger endSlot
            return ClusterSlotsResponseEntry{..}
    decode a = Left a

instance RedisResult ClusterSlotsNode where
    decode (MultiBulk (Just ((Bulk (Just clusterSlotsNodeIP)):(Integer port):(Bulk (Just clusterSlotsNodeID)):_))) = Right ClusterSlotsNode{..}
        where clusterSlotsNodePort = fromInteger port
    decode a = Left a


clusterSlots
    :: (RedisCtx m f)
    => m (f ClusterSlotsResponse)
clusterSlots = sendRequest $ ["CLUSTER", "SLOTS"]

clusterSetSlotImporting
    :: (RedisCtx m f)
    => Integer
    -> ByteString
    -> m (f Status)
clusterSetSlotImporting slot sourceNodeId = sendRequest $ ["CLUSTER", "SETSLOT", (encode slot), "IMPORTING", sourceNodeId]

clusterSetSlotMigrating
    :: (RedisCtx m f)
    => Integer
    -> ByteString
    -> m (f Status)
clusterSetSlotMigrating slot destinationNodeId = sendRequest $ ["CLUSTER", "SETSLOT", (encode slot), "MIGRATING", destinationNodeId]

clusterSetSlotStable
    :: (RedisCtx m f)
    => Integer
    -> m (f Status)
clusterSetSlotStable slot = sendRequest $ ["CLUSTER", "SETSLOT", "STABLE", (encode slot)]

clusterSetSlotNode
    :: (RedisCtx m f)
    => Integer
    -> ByteString
    -> m (f Status)
clusterSetSlotNode slot node = sendRequest ["CLUSTER", "SETSLOT", (encode slot), "NODE", node]

clusterGetKeysInSlot
    :: (RedisCtx m f)
    => Integer
    -> Integer
    -> m (f [ByteString])
clusterGetKeysInSlot slot count = sendRequest ["CLUSTER", "GETKEYSINSLOT", (encode slot), (encode count)]

command :: (RedisCtx m f) => m (f [CMD.CommandInfo])
command = sendRequest ["COMMAND"]


data ExpireOpts
  = ExpireOptsTime Condition
  | ExpireOptsValue SizeCondition

instance RedisArg ExpireOpts where
  encode (ExpireOptsTime c)  = encode c
  encode (ExpireOptsValue c) = encode c

-- |Set the expiration for a key as a UNIX timestamp specified in milliseconds (<http://redis.io/commands/pexpireat>).
-- Since Redis 7.0
pexpireatOpts
    :: (RedisCtx m f)
    => ByteString -- ^ key
    -> Integer -- ^ millisecondsTimestamp
    -> ExpireOpts
    -> m (f Bool)
pexpireatOpts key millisecondsTimestamp opts =
  sendRequest ["PEXPIREAT", key, encode millisecondsTimestamp, encode opts]

expireOpts
    :: (RedisCtx m f)
    => ByteString -- ^ key
    -> Integer -- ^ seconds
    -> ExpireOpts
    -> m (f Bool)
expireOpts key seconds opts = sendRequest ["EXPIRE", key, encode seconds, encode opts]

-- | Set the expiration for a key as a UNIX timestamp (<http://redis.io/commands/expireat>).
-- Since Redis 1.2.0
expireatOpts
    :: (RedisCtx m f)
    => ByteString -- ^ key
    -> Integer -- ^ timestamp
    -> ExpireOpts
    -> m (f Bool)
expireatOpts key timestamp opts = sendRequest ["EXPIREAT", key, encode timestamp, encode opts]

data FlushOpts
  = FlushOptsSync
  | FlushOptsAsync

instance RedisArg FlushOpts where
  encode FlushOptsSync = "SYNC"
  encode FlushOptsAsync = "ASYNC"

-- |Remove all keys from the current database (<http://redis.io/commands/flushdb>).
-- Since Redis 6.2
flushdbOpts
    :: (RedisCtx m f)
    => FlushOpts
    -> m (f Status)
flushdbOpts opts = sendRequest ["FLUSHDB", encode opts]

-- |Remove all keys from the current database (<http://redis.io/commands/flushdb>).
-- Since Redis 6.2
flushallOpts
    :: (RedisCtx m f)
    => FlushOpts
    -> m (f Status)
flushallOpts opts = sendRequest ["FLUSHALL", encode opts]

data BitposType = Byte | Bit

instance RedisArg BitposType where
  encode Byte = "BYTE"
  encode Bit = "BIT"

data BitposOpts
  = BitposOptsStart Integer
  | BitposOptsStartEnd Integer Integer (Maybe BitposType)

bitposOpts
    :: (RedisCtx m f)
    => ByteString
    -> Integer
    -> BitposOpts
    -> m (f Integer)
bitposOpts key_ bit opts = sendRequest ("BITPOS": key_:encode bit: rest) where
  rest  = case opts of
    BitposOptsStart s -> [encode s]
    BitposOptsStartEnd start end bits ->
      [encode start, encode end] ++ [ encode bits_ | Just bits_ <- pure bits]