import base64
import os
import time

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

from clarabot_nano.common.zmq.base import (
    AbstractZmqSocketThread,
)
from clarabot_nano.common.zmq_utils import bind_zmq_socket_to_free_port_for_dynamic_service
from clarabot_nano.utility.serialize import msgpack_dumps


# noinspection PyAbstractClass
class AbstractZmqRouterBaseThreadMixin(AbstractZmqSocketThread):
    """
    This ROUTER socket thread is an abstract base to create clients and servers.
    """

    # noinspection PyMissingConstructor
    def __init__(self, security: str):
        # calling super().__init__ is purposefully omitted here; inheritors will call the __init__ of a specific class
        self.port = None
        # create authentication data
        if security == "ec":
            self.__auth_secret_key, self.auth_public_key = Curve25519KeyGenerator.new_key()
        elif security == "basic":
            self.auth_username = base64.b64encode(os.urandom(18)).decode("ascii")
            self.auth_password = base64.b64encode(os.urandom(42)).decode("ascii")
        else:  # pragma: no cover
            raise Exception(f"unknown security setting: {security}")
        self.__security = security

    @property
    def security(self):
        return self.__security

    def get_client_auth_info(self) -> bytes:
        """
        This shall be securely (as possible) given to clients so they can initiate connection.
        """
        if self.__security == "ec":
            return msgpack_dumps({"security": "ec", "server_public_key": self.auth_public_key})
        elif self.__security == "basic":
            return msgpack_dumps({"security": "basic", "username": self.auth_username, "password": self.auth_password})

    def start(self, callback=None, daemon=True, name_prefix=None, timeout=10.0):
        AbstractZmqSocketThread.start(self, callback=callback, daemon=daemon, name_prefix=name_prefix)
        for _ in range(int(4 * timeout)):
            if not self.is_alive():
                raise Exception("Router thread stopped during prepare")
            if self.port is not None:
                break
            time.sleep(0.25)
        else:
            raise Exception("Router socket could not bind during prepare")
        return self.port

    def _thread_create_sockets(self, sockets: list):
        context = zmq.Context.instance()
        sockets.append(self._thread_create_server_socket(context))
        self.port = bind_zmq_socket_to_free_port_for_dynamic_service(sockets[-1], "tcp://127.0.0.1")
        sockets.append(context.socket(zmq.PAIR))
        sockets[-1].bind(self.inproc_name)

    def _thread_create_server_socket(self, context: zmq.Context) -> zmq.Socket:
        s = context.socket(zmq.ROUTER)
        # auth settings must come before bind
        if self.__security == "ec":
            s.curve_secretkey = self.__auth_secret_key
            del self.__auth_secret_key  # free, not needed directly anymore
            s.curve_publickey = self.auth_public_key
            s.curve_server = True
        elif self.__security == "basic":
            s.plain_server = True  # use plain authentication
        return s

    def _thread_socket_loop(self, server_socket, inproc_socket):
        raise NotImplementedError()
