"""
Helpers/utils for working with tornado asynchronous stuff
"""

import asyncio
import contextlib
import logging
import sys
import threading
import warnings

import tornado.concurrent
import tornado.ioloop

log = logging.getLogger(__name__)


def aioloop(io_loop, warn=False):
    """
    Ensure the ioloop is an asyncio loop not a tornado ioloop.

    pyzmq's ``zmq.asyncio`` sockets require an ``asyncio.AbstractEventLoop``;
    passing them a tornado ``IOLoop`` wrapper causes a subtle mismatch that
    on Windows manifests as a ``libzmq`` ``errno_assert(rc >= 0)`` abort
    inside ``zmq_poll`` (``Resource temporarily unavailable``).
    """
    if isinstance(io_loop, asyncio.AbstractEventLoop):
        return io_loop
    if isinstance(io_loop, tornado.ioloop.IOLoop):
        if warn:
            import traceback

            log.warning("Passed tornado loop %s", "".join(traceback.format_stack()))
        return io_loop.asyncio_loop
    raise RuntimeError("Loop must be AbstractEventLoop (preferred) or IOLoop")


@contextlib.contextmanager
def current_ioloop(io_loop):
    """
    A context manager that will set the current ioloop to io_loop for the context
    """
    try:
        orig_loop = tornado.ioloop.IOLoop.current()
    except RuntimeError:
        orig_loop = None
    asyncio.set_event_loop(io_loop.asyncio_loop)
    try:
        yield
    finally:
        if orig_loop:
            asyncio.set_event_loop(orig_loop.asyncio_loop)
        else:
            asyncio.set_event_loop(None)


class SyncWrapper:
    """
    A wrapper to make Async classes synchronous

    This is uses as a simple wrapper, for example:

    asynchronous = AsyncClass()
    # this method would regularly return a future
    future = asynchronous.async_method()

    sync = SyncWrapper(async_factory_method, (arg1, arg2), {'kwarg1': 'val'})
    # the sync wrapper will automatically wait on the future
    ret = sync.async_method()
    """

    def __init__(
        self,
        cls,
        args=None,
        kwargs=None,
        async_methods=None,
        close_methods=None,
        loop_kwarg=None,
    ):
        self.asyncio_loop = asyncio.new_event_loop()
        self.io_loop = tornado.ioloop.IOLoop(
            asyncio_loop=self.asyncio_loop, make_current=False
        )
        if args is None:
            args = []
        if kwargs is None:
            kwargs = {}
        if async_methods is None:
            async_methods = []
        if close_methods is None:
            close_methods = []
        self.loop_kwarg = loop_kwarg
        self.cls = cls
        if loop_kwarg:
            kwargs[self.loop_kwarg] = self.io_loop
        with current_ioloop(self.io_loop):
            self.obj = cls(*args, **kwargs)
        self._async_methods = list(
            set(async_methods + getattr(self.obj, "async_methods", []))
        )
        self._close_methods = list(
            set(close_methods + getattr(self.obj, "close_methods", []))
        )

    def _populate_async_methods(self):
        """
        We need the '_coroutines' attribute on classes until we can depricate
        tornado<4.5. After that 'is_coroutine_fuction' will always be
        available.
        """
        if hasattr(self.obj, "_coroutines"):
            self._async_methods += self.obj._coroutines

    def __repr__(self):
        return f"<SyncWrapper(cls={self.cls})"

    def close(self):
        for method in self._close_methods:
            if method in self._async_methods:
                method = self._wrap(method)
            else:
                try:
                    method = getattr(self.obj, method)
                except AttributeError:
                    log.error("No sync method %s on object %r", method, self.obj)
                    continue
            try:
                method()
            except AttributeError:
                log.error("No async method %s on object %r", method, self.obj)
            except Exception:  # pylint: disable=broad-except
                log.exception("Exception encountered while running stop method")
        io_loop = self.io_loop
        io_loop.stop()
        try:
            io_loop.close(all_fds=True)
        except KeyError:
            pass
        self.asyncio_loop.close()

    def __getattr__(self, key):
        if key in self._async_methods:
            return self._wrap(key)
        return getattr(self.obj, key)

    def _wrap(self, key):
        def wrap(*args, **kwargs):
            results = []
            thread = threading.Thread(
                target=self._target,
                args=(key, args, kwargs, results, self.asyncio_loop),
            )
            thread.start()
            thread.join()
            if results[0]:
                return results[1]
            else:
                exc_info = results[1]
                raise exc_info[1].with_traceback(exc_info[2])

        return wrap

    def _target(self, key, args, kwargs, results, asyncio_loop):
        asyncio.set_event_loop(asyncio_loop)
        io_loop = tornado.ioloop.IOLoop.current()
        try:
            result = io_loop.run_sync(lambda: getattr(self.obj, key)(*args, **kwargs))
            results.append(True)
            results.append(result)
        except Exception:  # pylint: disable=broad-except
            results.append(False)
            results.append(sys.exc_info())

    def __enter__(self):
        if hasattr(self.obj, "__aenter__"):
            ret = self._wrap("__aenter__")()
            if ret == self.obj:
                return self
            else:
                return ret
        elif hasattr(self.obj, "__enter__"):
            ret = self.obj.__enter__()
            if ret == self.obj:
                return self
            else:
                return ret
        return self

    def __exit__(self, exc_type, exc_val, tb):
        if hasattr(self.obj, "__aexit__"):
            self._wrap("__aexit__")(exc_type, exc_val, tb)
        self.close()

    # pylint: disable=W1701
    def __del__(self):
        # PATCH: mirror ``SaltEvent.__del__`` at ``salt/utils/event.py``
        # -- deliberately do NOT close the wrapped ``obj`` / io_loop /
        # asyncio_loop from ``__del__``.  ``__del__`` fires during GC
        # (may be arbitrarily delayed, may skip on reference cycles)
        # and during interpreter shutdown, when the world is already
        # tearing down and touching a tornado/asyncio loop can raise
        # from a partially-freed C extension.  Instead, emit a
        # ``ResourceWarning`` so callers that missed ``close()`` /
        # context-manager surface loudly in tests / sentry / log
        # aggregators.
        #
        # Motivation: ``SyncWrapper``-owned asyncio loops are the
        # dominant leak surface on the minion under sustained
        # ``saltutil.refresh_pillar`` / re-auth churn -- each abandoned
        # wrapper holds a whole IOLoop, its ZMQ context, and the two
        # socketpairs backing the master REQ channel.  Observed ~451
        # leaked socketpairs (~902 fds) per minion, tripping the
        # 1024-file ulimit critical threshold and the minion's own
        # sock-throttle logic.
        try:
            unclosed = getattr(self, "obj", None) is not None or (
                getattr(self, "asyncio_loop", None) is not None
                and not self.asyncio_loop.is_closed()
            )
        except Exception:  # pylint: disable=broad-except
            return
        if not unclosed:
            return
        try:
            warnings.warn(
                f"unclosed {type(self).__name__} for cls="
                f"{getattr(self, 'cls', None)!r}; call ``close()`` or "
                f"use as a context manager",
                ResourceWarning,
                source=self,
            )
        except Exception:  # pylint: disable=broad-except
            # ``warnings.warn`` can raise during interpreter shutdown
            # when the ``warnings`` module has already been torn down.
            # A finalizer must not propagate exceptions.
            pass

    # pylint: enable=W1701
