import errno
import sys
import time
from contextlib import contextmanager
from typing import Sequence

import zmq
from zmq.auth.thread import ThreadAuthenticator

from clarabot_nano.utility.network import get_free_port_for_dynamic_service


class FailedByHWM(Exception):
    pass


def polling_send_multipart(socket: zmq.Socket, parts: Sequence[bytes], poll: zmq.Poller = None, timeout: float = 10.0):
    """
    The goal of this construct is to:
        - Prevent infinite blocking if HWM persists. (for sockets like DEALER)
        - Prevent dropping messages if in HWM state. (for sockets like ROUTER)

    The poll must only select for POLLOUT on the socket if provided.
    """
    if poll is None:
        poll = zmq.Poller()
        poll.register(socket, zmq.POLLOUT)
    t = time.perf_counter() + timeout
    while True:
        if socket in dict(poll.poll(1000)):
            try:
                socket.send_multipart(parts, flags=zmq.NOBLOCK)
                break
            except zmq.ZMQError as e:
                if e.errno == zmq.EAGAIN:
                    # buffer state changed since poll event... or
                    # https://github.com/zeromq/libzmq/issues/2623
                    pass
                else:
                    raise
        if time.perf_counter() >= t:
            raise FailedByHWM()


def bind_zmq_socket_to_free_port_for_dynamic_service(socket, address, timeout=10.0):
    """
    This is similar to zmq.Socket.bind_to_random_port() except:
        - we timeout and not try-count
        - ports are pre-tried and only a tiny race condition window exists where this part may fail by other an
          process acquiring the socket
    """
    t = time.perf_counter() + timeout
    while time.perf_counter() < t:
        try:
            port = get_free_port_for_dynamic_service()
            socket.bind("%s:%s" % (address, port))
        except zmq.ZMQError as exception:
            en = exception.errno
            if en == zmq.EADDRINUSE:
                continue
            elif sys.platform.startswith("win") and en == errno.EACCES:
                continue
            else:
                raise
        else:
            return port
    raise Exception("Failed to bind to dynamic port")


@contextmanager
def zmq_authenticator(ctx=None):
    """
    An authenticator has to be available for ROUTERs to accept incoming connections.
    """
    if ctx is None:
        ctx = zmq.Context.instance()
    # Start an authenticator for this context.
    auth = ThreadAuthenticator(ctx)
    auth.start()
    auth.allow("127.0.0.1")  # plain should only be used on localhost anyways, so no option to change this
    try:
        yield auth
    finally:
        auth.stop()
