import asyncio
import pickle
from urllib.parse import urlparse

try:
    import aioredis
except ImportError:
    aioredis = None

from .asyncio_pubsub_manager import AsyncPubSubManager


def _parse_redis_url(url):
    p = urlparse(url)
    if p.scheme not in {'redis', 'rediss'}:
        raise ValueError('Invalid redis url')
    ssl = p.scheme == 'rediss'
    host = p.hostname or 'localhost'
    port = p.port or 6379
    password = p.password
    if p.path:
        db = int(p.path[1:])
    else:
        db = 0
    return host, port, password, db, ssl


class AsyncRedisManager(AsyncPubSubManager):  # pragma: no cover
    """Redis based client manager for asyncio servers.

    This class implements a Redis backend for event sharing across multiple
    processes. Only kept here as one more example of how to build a custom
    backend, since the kombu backend is perfectly adequate to support a Redis
    message queue.

    To use a Redis backend, initialize the :class:`Server` instance as
    follows::

        server = socketio.Server(client_manager=socketio.AsyncRedisManager(
            'redis://hostname:port/0'))

    :param url: The connection URL for the Redis server. For a default Redis
                store running on the same host, use ``redis://``.  To use an
                SSL connection, use ``rediss://``.
    :param channel: The channel name on which the server sends and receives
                    notifications. Must be the same in all the servers.
    :param write_only: If set to ``True``, only initialize to emit events. The
                       default of ``False`` initializes the class for emitting
                       and receiving.
    """
    name = 'aioredis'

    def __init__(self, url='redis://localhost:6379/0', channel='socketio',
                 write_only=False, logger=None):
        if aioredis is None:
            raise RuntimeError('Redis package is not installed '
                               '(Run "pip install aioredis" in your '
                               'virtualenv).')
        (
            self.host, self.port, self.password, self.db, self.ssl
        ) = _parse_redis_url(url)
        self.pub = None
        self.sub = None
        super().__init__(channel=channel, write_only=write_only, logger=logger)

    async def _publish(self, data):
        retry = True
        while True:
            try:
                if self.pub is None:
                    self.pub = await aioredis.create_redis(
                        (self.host, self.port), db=self.db,
                        password=self.password, ssl=self.ssl
                    )
                return await self.pub.publish(self.channel,
                                              pickle.dumps(data))
            except (aioredis.RedisError, OSError):
                if retry:
                    self._get_logger().error('Cannot publish to redis... '
                                             'retrying')
                    self.pub = None
                    retry = False
                else:
                    self._get_logger().error('Cannot publish to redis... '
                                             'giving up')
                    break

    async def _listen(self):
        retry_sleep = 1
        while True:
            try:
                if self.sub is None:
                    self.sub = await aioredis.create_redis(
                        (self.host, self.port), db=self.db,
                        password=self.password, ssl=self.ssl
                    )
                self.ch = (await self.sub.subscribe(self.channel))[0]
                retry_sleep = 1
                return await self.ch.get()
            except (aioredis.RedisError, OSError):
                self._get_logger().error('Cannot receive from redis... '
                                         'retrying in '
                                         '{} secs'.format(retry_sleep))
                self.sub = None
                await asyncio.sleep(retry_sleep)
                retry_sleep *= 2
                if retry_sleep > 60:
                    retry_sleep = 60
