from collections import namedtuple

from aioredis.util import wait_ok, wait_convert, wait_make_dict, _NOTSET
from aioredis.log import logger


class ServerCommandsMixin:
    """Server commands mixin.

    For commands details see: http://redis.io/commands/#server
    """

    SHUTDOWN_SAVE = 'SHUTDOWN_SAVE'
    SHUTDOWN_NOSAVE = 'SHUTDOWN_NOSAVE'

    def bgrewriteaof(self):
        """Asynchronously rewrite the append-only file."""
        fut = self.execute(b'BGREWRITEAOF')
        return wait_ok(fut)

    def bgsave(self):
        """Asynchronously save the dataset to disk."""
        fut = self.execute(b'BGSAVE')
        return wait_ok(fut)

    def client_kill(self):
        """Kill the connection of a client.

        .. warning:: Not Implemented
        """
        raise NotImplementedError

    def client_list(self):
        """Get the list of client connections.

        Returns list of ClientInfo named tuples.
        """
        fut = self.execute(b'CLIENT', b'LIST', encoding='utf-8')
        return wait_convert(fut, to_tuples)

    def client_getname(self, encoding=_NOTSET):
        """Get the current connection name."""
        return self.execute(b'CLIENT', b'GETNAME', encoding=encoding)

    def client_pause(self, timeout):
        """Stop processing commands from clients for *timeout* milliseconds.

        :raises TypeError: if timeout is not int
        :raises ValueError: if timeout is less than 0
        """
        if not isinstance(timeout, int):
            raise TypeError("timeout argument must be int")
        if timeout < 0:
            raise ValueError("timeout must be greater equal 0")
        fut = self.execute(b'CLIENT', b'PAUSE', timeout)
        return wait_ok(fut)

    def client_reply(self):
        raise NotImplementedError()

    def client_setname(self, name):
        """Set the current connection name."""
        fut = self.execute(b'CLIENT', b'SETNAME', name)
        return wait_ok(fut)

    def command(self):
        """Get array of Redis commands."""
        # TODO: convert result
        return self.execute(b'COMMAND', encoding='utf-8')

    def command_count(self):
        """Get total number of Redis commands."""
        return self.execute(b'COMMAND', b'COUNT')

    def command_getkeys(self, command, *args, encoding='utf-8'):
        """Extract keys given a full Redis command."""
        return self.execute(b'COMMAND', b'GETKEYS', command, *args,
                            encoding=encoding)

    def command_info(self, command, *commands):
        """Get array of specific Redis command details."""
        return self.execute(b'COMMAND', b'INFO', command, *commands,
                            encoding='utf-8')

    def config_get(self, parameter='*'):
        """Get the value of a configuration parameter(s).

        If called without argument will return all parameters.

        :raises TypeError: if parameter is not string
        """
        if not isinstance(parameter, str):
            raise TypeError("parameter must be str")
        fut = self.execute(b'CONFIG', b'GET', parameter, encoding='utf-8')
        return wait_make_dict(fut)

    def config_rewrite(self):
        """Rewrite the configuration file with the in memory configuration."""
        fut = self.execute(b'CONFIG', b'REWRITE')
        return wait_ok(fut)

    def config_set(self, parameter, value):
        """Set a configuration parameter to the given value."""
        if not isinstance(parameter, str):
            raise TypeError("parameter must be str")
        fut = self.execute(b'CONFIG', b'SET', parameter, value)
        return wait_ok(fut)

    def config_resetstat(self):
        """Reset the stats returned by INFO."""
        fut = self.execute(b'CONFIG', b'RESETSTAT')
        return wait_ok(fut)

    def dbsize(self):
        """Return the number of keys in the selected database."""
        return self.execute(b'DBSIZE')

    def debug_sleep(self, timeout):
        """Suspend connection for timeout seconds."""
        fut = self.execute(b'DEBUG', b'SLEEP', timeout)
        return wait_ok(fut)

    def debug_object(self, key):
        """Get debugging information about a key."""
        return self.execute(b'DEBUG', b'OBJECT', key)

    def debug_segfault(self, key):
        """Make the server crash."""
        # won't test, this probably works
        return self.execute(b'DEBUG', 'SEGFAULT')  # pragma: no cover

    def flushall(self, async_op=False):
        """
        Remove all keys from all databases.

        :param async_op: lets the entire dataset to be freed asynchronously. \
        Defaults to False
        """
        if async_op:
            fut = self.execute(b'FLUSHALL', b'ASYNC')
        else:
            fut = self.execute(b'FLUSHALL')
        return wait_ok(fut)

    def flushdb(self, async_op=False):
        """
        Remove all keys from the current database.

        :param async_op: lets a single database to be freed asynchronously. \
        Defaults to False
        """
        if async_op:
            fut = self.execute(b'FLUSHDB', b'ASYNC')
        else:
            fut = self.execute(b'FLUSHDB')
        return wait_ok(fut)

    def info(self, section='default'):
        """Get information and statistics about the server.

        If called without argument will return default set of sections.
        For available sections, see http://redis.io/commands/INFO

        :raises ValueError: if section is invalid

        """
        if not section:
            raise ValueError("invalid section")
        fut = self.execute(b'INFO', section, encoding='utf-8')
        return wait_convert(fut, parse_info)

    def lastsave(self):
        """Get the UNIX time stamp of the last successful save to disk."""
        return self.execute(b'LASTSAVE')

    def monitor(self):
        """Listen for all requests received by the server in real time.

        .. warning::
           Will not be implemented for now.
        """
        # NOTE: will not implement for now;
        raise NotImplementedError

    def role(self):
        """Return the role of the server instance.

        Returns named tuples describing role of the instance.
        For fields information see http://redis.io/commands/role#output-format
        """
        fut = self.execute(b'ROLE', encoding='utf-8')
        return wait_convert(fut, parse_role)

    def save(self):
        """Synchronously save the dataset to disk."""
        return self.execute(b'SAVE')

    def shutdown(self, save=None):
        """Synchronously save the dataset to disk and then
        shut down the server.
        """
        if save is self.SHUTDOWN_SAVE:
            return self.execute(b'SHUTDOWN', b'SAVE')
        elif save is self.SHUTDOWN_NOSAVE:
            return self.execute(b'SHUTDOWN', b'NOSAVE')
        else:
            return self.execute(b'SHUTDOWN')

    def slaveof(self, host=_NOTSET, port=None):
        """Make the server a slave of another instance,
        or promote it as master.

        Calling ``slaveof(None)`` will send ``SLAVEOF NO ONE``.

        .. versionchanged:: v0.2.6
           ``slaveof()`` form deprecated
           in favour of explicit ``slaveof(None)``.
        """
        if host is _NOTSET:
            logger.warning("slaveof() form is deprecated!"
                           " Use slaveof(None) to turn redis into a MASTER.")
            host = None
            # TODO: drop in 0.3.0
        if host is None and port is None:
            return self.execute(b'SLAVEOF', b'NO', b'ONE')
        return self.execute(b'SLAVEOF', host, port)

    def slowlog_get(self, length=None):
        """Returns the Redis slow queries log."""
        if length is not None:
            if not isinstance(length, int):
                raise TypeError("length must be int or None")
            return self.execute(b'SLOWLOG', b'GET', length)
        else:
            return self.execute(b'SLOWLOG', b'GET')

    def slowlog_len(self):
        """Returns length of Redis slow queries log."""
        return self.execute(b'SLOWLOG', b'LEN')

    def slowlog_reset(self):
        """Resets Redis slow queries log."""
        fut = self.execute(b'SLOWLOG', b'RESET')
        return wait_ok(fut)

    def sync(self):
        """Redis-server internal command used for replication."""
        return self.execute(b'SYNC')

    def time(self):
        """Return current server time."""
        fut = self.execute(b'TIME')
        return wait_convert(fut, to_time)


def _split(s):
    k, v = s.split('=')
    return k.replace('-', '_'), v


def to_time(obj):
    return int(obj[0]) + int(obj[1]) * 1e-6


def to_tuples(value):
    line, *lines = value.splitlines(False)
    line = list(map(_split, line.split(' ')))
    ClientInfo = namedtuple('ClientInfo', ' '.join(k for k, v in line))
    # TODO: parse flags and other known fields
    result = [ClientInfo(**dict(line))]
    for line in lines:
        result.append(ClientInfo(**dict(map(_split, line.split(' ')))))
    return result


def parse_info(info):
    res = {}
    for block in info.split('\r\n\r\n'):
        section, *block = block.strip().splitlines()
        section = section[2:].lower()
        res[section] = tmp = {}
        for line in block:
            key, value = line.split(':', 1)
            if ',' in line and '=' in line:
                value = dict(map(lambda i: i.split('='), value.split(',')))
            tmp[key] = value
    return res


# XXX: may change in future
#      (may be hard to maintain for new/old redis versions)
MasterInfo = namedtuple('MasterInfo', 'role replication_offset slaves')
MasterSlaveInfo = namedtuple('MasterSlaveInfo', 'ip port ack_offset')

SlaveInfo = namedtuple('SlaveInfo',
                       'role master_ip master_port state received')

SentinelInfo = namedtuple('SentinelInfo', 'role masters')


def parse_role(role):
    type_ = role[0]
    if type_ == 'master':
        slaves = [MasterSlaveInfo(s[0], int(s[1]), int(s[2]))
                  for s in role[2]]
        return MasterInfo(role[0], int(role[1]), slaves)
    elif type_ == 'slave':
        return SlaveInfo(role[0], role[1], int(role[2]), role[3], int(role[4]))
    elif type_ == 'sentinel':
        return SentinelInfo(*role)
    return role
