import asyncio
import json
import logging
from random import random
from typing import List, Union, Dict, Callable, Awaitable
from enum import Enum

from shortid import ShortId
import websockets as ws


class SubscribeCategory(str, Enum):
    account = 'subscribeToAccounts'
    accounts = 'subscribeToAccounts'
    market = 'subscribeToMarkets'
    markets = 'subscribeToMarkets'
    chain = 'subscribeToChains'
    chains = 'subscribeToChains'


class ReconnectingWebsocket:

    STREAM_URL: str = 'wss://datastream.idex.market'
    MAX_RECONNECTS: int = 5
    MAX_RECONNECT_SECONDS: int = 60
    MIN_RECONNECT_WAIT = 0.1
    TIMEOUT: int = 10
    PROTOCOL_VERSION: str = '1.0.0'

    def __init__(self, loop, coro, api_key):
        self._loop = loop
        self._log = logging.getLogger(__name__)
        self._coro = coro
        self._reconnect_attempts: int = 0
        self._conn = None
        self._socket: ws.client.WebSocketClientProtocol = None
        self._sid: str = None
        self._handshaken: bool = False
        self._api_key = api_key

        self._connect()

    def set_sid(self, sid: str):
        self._sid = sid

    def _connect(self):
        self._conn = asyncio.ensure_future(self._run())

    async def _run(self):

        keep_waiting: bool = True

        async with ws.connect(self.STREAM_URL, ssl=True) as socket:
            self._socket = socket

            await self.handshake()
            try:
                while keep_waiting:
                    try:
                        evt = await asyncio.wait_for(self._socket.recv(), timeout=self.TIMEOUT)
                    except asyncio.TimeoutError:
                        self._log.debug("no message in {} seconds".format(self.TIMEOUT))
                        await self._socket.ping()
                    except asyncio.CancelledError:
                        self._log.debug("cancelled error")
                        await self._socket.ping()
                    else:
                        try:
                            evt_obj = json.loads(evt)
                        except ValueError:
                            pass
                        else:
                            await self._coro(evt_obj)

            except ws.ConnectionClosed as e:
                keep_waiting = False
                await self._reconnect()
            except Exception as e:
                self._log.debug('ws exception:{}'.format(e))
                keep_waiting = False
            #    await self._reconnect()

    async def _reconnect(self):
        await self.cancel()
        self._reconnect_attempts += 1
        if self._reconnect_attempts < self.MAX_RECONNECTS:

            self._log.debug(f"websocket reconnecting {self.MAX_RECONNECTS - self._reconnect_attempts} attempts left")
            reconnect_wait = self._get_reconnect_wait(self._reconnect_attempts)
            await asyncio.sleep(reconnect_wait)
            self._handshaken = False
            self._connect()
        else:
            # maybe raise an exception
            self._log.error(f"websocket could not reconnect after {self._reconnect_attempts} attempts")
            pass

    def _get_reconnect_wait(self, attempts: int) -> int:
        expo = 2 ** attempts
        return round(random() * min(self.MAX_RECONNECT_SECONDS, expo - 1) * 1000 + 1)

    async def send_message(self, category: SubscribeCategory, msg):
        wait_count = 0
        if not self._socket or not self._sid:
            self._log.debug("waiting for socket to init and handshake")
            wait_count += 1
            if wait_count < 5:
                await asyncio.sleep(1)

        # build the message
        rid = ShortId()
        socket_msg = json.dumps({
            "rid": f"rid:{rid.generate()}",
            "sid": self._sid,
            "request": category,
            "payload": json.dumps(msg)
        })
        await self._socket.send(socket_msg)

    async def handshake(self):
        if self._handshaken:
            return

        self._handshaken = True

        handshake = json.dumps({
            'request': 'handshake',
            'payload': json.dumps({
                # 'locale': 'en-au',
                # 'type': 'client',
                'version': self.PROTOCOL_VERSION,
                'key': self._api_key
            })
        })
        await self._socket.send(handshake)

    async def cancel(self):
        self._conn.cancel()


class IdexSocketManager:

    def __init__(self):
        """Initialise the IdexSocketManager

        """
        self._callback: Callable[[int], Awaitable[str]]
        self._conn = None
        self._loop = None
        self._log = logging.getLogger(__name__)

    @classmethod
    async def create(cls, loop, callback: Callable[[int], Awaitable[str]], api_key):
        self = IdexSocketManager()
        self._loop = loop
        self._callback = callback
        self._conn = ReconnectingWebsocket(loop, self._recv, api_key=api_key)
        return self

    async def _recv(self, msg: Dict):
        # self._log.debug(f"mes recvd:{msg}")
        # get topic
        if 'result' in msg:
            if msg['result'] == 'success':
                self._conn.set_sid(msg['sid'])

        elif 'event' in msg:

            await self._callback(msg)

    async def subscribe(self, category: SubscribeCategory, topic: Union[str, List[str]], events: [List[str]]):
        """Subscribe to a market or markets

        https://github.com/AuroraDAO/datastream-client-js/blob/master/docs/index.md

        :param category: required
        :param topic: required
        :param events: required
        :returns: None

        Message Formats

        Sample response

        .. code-block:: python

            {
                'chain': 'eth',
                'event': 'market_cancels',
                'payload': '{               # a JSON encoded string
                    "market":"ETH_IDXM",
                    "cancels": [
                        {
                            "id":461889486,
                            "market":
                            "ETH_IDXM",
                            "orderHash":"0xb0ddfd9e919493aaec790da1c089c846396fca5ac4592a340cbd032f65d1bde6",
                            "createdAt":"2019-02-11T09:27:57.000Z"
                        }
                    ]
                }',
                'sid': 'csi:76XcGEza40XPB',
                'eid': 'evt:GTaYL4sEcp5fY',
                'seq': 98
            }


        """

        req_msg = {
            'action': 'subscribe',
            'topics': topic,
            'events': events
        }

        await self._conn.send_message(category, req_msg)

    async def unsubscribe(self, category: SubscribeCategory, topic: Union[str, List[str]], events: [List[str]]):
        """Unsubscribe from a market

        https://github.com/AuroraDAO/datastream-client-js/blob/master/docs/index.md

        :param category: required
        :param topic: required
        :param events: required

        :returns: None

        """

        req_msg = {
            'action': 'unsubscribe',
            'topics': topic,
            'events': events
        }

        await self._conn.send_message(category, req_msg)