"""Donald Asyncio Tasks."""

import asyncio as aio
import atexit
import datetime
import multiprocessing as mp
from queue import Empty

from crontab import CronTab

from . import logger
from .queue import Queue
from .utils import AsyncMixin, AttrDict, FileLock, repr_func, create_task
from .worker import ProcessWorker


class Donald(AsyncMixin):

    """I'am on Donald."""

    defaults = dict(

        # Run tasks imediatelly in the same process/thread
        fake_mode=False,

        # Number of workers
        num_workers=mp.cpu_count() - 1,

        # Maximum concurent tasks per worker
        max_tasks_per_worker=100,

        # Ensure that the Donald starts only once
        filelock=None,

        # logging level
        loglevel='INFO',

        # Start handlers
        on_start=[],

        # Stop handlers
        on_stop=[],

        # AMQP params
        queue=dict(
            exchange='donald',
            queue='donald',
        ),

    )

    def __init__(self, **params):
        """Initialize donald parameters."""
        self.params = AttrDict(self.defaults)
        self.params.update(params)

        logger.setLevel(self.params['loglevel'].upper())
        logger.propagate = False

        ctx = mp.get_context('spawn')
        self.rx = ctx.Queue()
        self.tx = ctx.Queue()

        self.workers = None
        self.listener = None
        self.lock = FileLock(self.params.filelock)
        self.schedules = []
        self.waiting = {}
        self._loop = None
        self._stopping = False
        self._started = False
        self._exc_handlers = []

        self.on_start = self.params.on_start.append
        self.on_stop = self.params.on_stop.append

        self.queue = Queue(self, **self.params.queue)

    def __str__(self):
        """Representate as a string."""
        return 'Donald [%s]' % self.params['num_workers']

    def init_loop(self, loop):
        """Bind to given loop."""
        if not self._started:
            self._loop = loop or aio.get_event_loop()
            self.queue.init_loop(loop)

    async def start(self, loop=None):
        """Start workers.

        :returns: A coroutine
        """
        logger.warning('Start Donald')
        self.init_loop(loop)

        if self.params.fake_mode:
            return True

        if self.params.filelock:
            try:
                self.lock.acquire()
            except self.lock.Error:
                logger.warning('Donald is locked. Exit.')
                return

        self.workers = tuple(
            ProcessWorker(self.rx, self.tx, **self.params)
            for _ in range(max(self.params.num_workers, 1)))

        # Start listener
        self.listener = aio.create_task(self.listen())

        # Start workers
        for wrk in self.workers:
            wrk.start()

        while not all(wrk.started.is_set() for wrk in self.workers):
            continue

        # Stop on exit
        atexit.register(
            lambda: not self.loop.is_closed() and self.loop.run_until_complete(self.stop))

        # Mark self started
        self._started = True

        return True

    async def stop(self, *args):
        """Stop workers. Disconnect from queue. Cancel schedules.

        The method could be called syncroniously too.

        :returns: A future
        """
        if self.is_closed() or not self._started:
            return

        logger.warning('Stop Donald')

        self._stopping = True

        # Stop queue
        await self.queue.stop()

        if self.params.filelock:
            self.lock.release()

        # Stop schedules
        for task in self.schedules:
            task.cancel()

        # Stop runner if exists
        self.listener and self.listener.cancel()

        # Stop workers
        for wrk in self.workers:
            wrk.stopped.set()
            wrk.join(1)
            wrk.terminate()

        self.rx.close()
        self.tx.close()

        self._started = False

        return True

    async def listen(self):
        """Wait for a result and process."""
        while not self._stopping:
            if not self.waiting:
                await aio.sleep(0.01)
                continue

            try:
                ident, res = self.tx.get(block=False)
                fut = self.waiting.pop(ident, None)
                if fut:
                    if isinstance(res, Exception):
                        self.handle_exc(res)
                        fut.set_exception(res)
                    else:
                        fut.set_result(res)

            except Empty:
                pass

            await aio.sleep(0)

    def submit(self, func, *args, **kwargs):
        """Submit a task to workers.

        :returns: asyncio.Future
        """
        if not callable(func):
            raise ValueError('Invalid call: %r' % func)

        logger.debug('Submit: %s', repr_func(func, args, kwargs))
        if self.params.fake_mode:
            return create_task(func, args, kwargs)

        if not self._started:
            raise RuntimeError('Donald is not started yet')

        fut = self.loop.create_future()
        self.waiting[id(fut)] = fut
        self.rx.put((id(fut), func, args, kwargs))
        return fut

    def schedule(self, interval, func, *args, **kwargs):
        """Run given func/coro periodically."""
        timer = lambda: float(interval) # noqa

        if isinstance(interval, datetime.timedelta):
            timer = interval.total_seconds

        elif isinstance(interval, CronTab):
            timer = lambda: interval.next(default_utc=True)  # noqa

        async def scheduler():
            while True:
                sleep = max(timer(), 0.01)
                logger.info('Next %s in %0.2f s', repr_func(func, args, kwargs), sleep)
                await aio.sleep(sleep)
                self.submit(func, *args, **kwargs)

        logger.info('Schedule %r', repr_func(func, args, kwargs))
        schedule = aio.create_task(scheduler())
        self.schedules.append(schedule)
        return schedule

    def handle_exc(self, exc):
        """Handle exception for periodic tasks."""
        for handler in self._exc_handlers:
            handler(exc)
