import time
from queue import Empty

import zmq
from clarabot_crypto.base.key.ec import Curve25519KeyGenerator

from clarabot_nano.common.core.message import MSG_TYPE_KEEPALIVE
from clarabot_nano.common.zmq.base import (
    AbstractZmqSocketThread,
    AbstractAioZmqSocketThread,
)
from clarabot_nano.common.zmq.constants import QUEUE_PUSH_NOTIFICATION, KEEPALIVE_INTERVAL, KEEPALIVE_FAILURE
from clarabot_nano.common.zmq_utils import polling_send_multipart


# noinspection PyAbstractClass
class ZmqDealerClientThreadMixin(AbstractZmqSocketThread):
    """
    This DEALER socket thread is designed to be used as a client: send requests and receive responses.
    It provides an interface to perform connection initialization before the main loop.
    Implicit keep-alive heartbeats are sent and expected in return to ensure connection health.
    """
    SEND_HWM_TIMEOUT = 24.0

    # noinspection PyMissingConstructor
    def __init__(
        self,
        port,
        auth_data,
        keepalive_interval=KEEPALIVE_INTERVAL,
        keepalive_failure=KEEPALIVE_FAILURE,
        keepalive_args=(),
    ):
        # calling super().__init__ is purposefully omitted here; inheritors will call the __init__ of a specific class
        self._port = port
        self._auth_data = auth_data
        self._keepalive_interval = keepalive_interval
        self._keepalive_failure = keepalive_failure
        self._keepalive_frames = (MSG_TYPE_KEEPALIVE,) + keepalive_args

    @property
    def keepalive_failure_seconds(self):
        return self._keepalive_failure

    def _thread_create_sockets(self, sockets: list):
        context = zmq.Context.instance()
        sockets.append(self._thread_create_client_socket(context))
        sockets[-1].connect(f"tcp://127.0.0.1:{self._port}")
        sockets.append(context.socket(zmq.PAIR))
        sockets[-1].bind(self.inproc_name)

    def _thread_create_client_socket(self, context: zmq.Context) -> zmq.Socket:
        s = context.socket(zmq.DEALER)
        if self._auth_data is None:
            pass
        elif (security := self._auth_data.get("security")) == "ec":
            s.curve_secretkey, s.curve_publickey = Curve25519KeyGenerator.new_key()
            s.curve_serverkey = self._auth_data["server_public_key"]
        elif security == "basic":
            s.plain_username = self._auth_data["username"]
            s.plain_password = self._auth_data["password"]
        else:  # pragma: no cover
            raise Exception(f"unknown security setting: {security}")
        return s

    def _thread_socket_loop(self, client_socket, inproc_socket):
        """
        A simple socket processing thread that buffers messages every which way.
        """
        # listen for input on all sockets
        pollin = zmq.Poller()
        pollin.register(client_socket, zmq.POLLIN)
        pollin.register(inproc_socket, zmq.POLLIN)
        self._thread_init_client_socket(client_socket)
        pollout = zmq.Poller()
        pollout.register(client_socket, zmq.POLLOUT)
        # take care of hwm on inproc socket
        inproc_pollout = zmq.Poller()
        inproc_pollout.register(inproc_socket, zmq.POLLOUT)
        # main loop
        last_pulse = last_pulse_sent = time.perf_counter() - self._keepalive_interval
        while self._keep_running():
            if last_pulse < time.perf_counter() - self._keepalive_failure:
                raise Exception("keepalive heartbeat lost from server")
            sockets = dict(pollin.poll(1000))
            if client_socket in sockets:
                data = client_socket.recv_multipart()
                if len(data) >= 1:
                    if data[0] == MSG_TYPE_KEEPALIVE:
                        if len(data) == len(self._keepalive_frames):
                            for i in range(1, len(self._keepalive_frames)):
                                if data[i] != self._keepalive_frames[i]:
                                    break
                            else:
                                last_pulse = time.perf_counter()
                    else:
                        self._thread_socket_send_to_logic(data, inproc_socket, inproc_pollout)
            if inproc_socket in sockets:
                data = inproc_socket.recv_multipart()
                if len(data) >= 1:
                    if data[0] == QUEUE_PUSH_NOTIFICATION:
                        while True:
                            try:
                                data = self._inproc_queue.get(block=False)
                            except Empty:
                                break
                            polling_send_multipart(client_socket, data, pollout, self.SEND_HWM_TIMEOUT)
                    else:
                        polling_send_multipart(client_socket, data, pollout, self.SEND_HWM_TIMEOUT)
            if last_pulse_sent < time.perf_counter() - self._keepalive_interval:
                polling_send_multipart(client_socket, self._keepalive_frames, pollout, self.SEND_HWM_TIMEOUT)
                last_pulse_sent = time.perf_counter()

    def _thread_init_client_socket(self, client_socket):
        raise NotImplementedError()


# class BiPairZmqDealerClientThread(ZmqDealerClientThreadMixin, AbstractBiPairZmqSocketThread):
#     def __init__(self, port, inproc_bind_name):
#         ZmqDealerClientThreadMixin.__init__(self, port)
#         AbstractBiPairZmqSocketThread.__init__(self, inproc_bind_name)


class AioZmqDealerClientThread(ZmqDealerClientThreadMixin, AbstractAioZmqSocketThread):
    def __init__(self, loop, port, auth_data, inproc_bind_name, keepalive_args=()):
        ZmqDealerClientThreadMixin.__init__(self, port, auth_data, keepalive_args=keepalive_args)
        AbstractAioZmqSocketThread.__init__(self, loop, inproc_bind_name)

    def _thread_init_client_socket(self, client_socket):
        raise NotImplementedError()
