from __future__ import annotations

import logging
import threading
import time
from queue import Queue
from typing import Union, Tuple

import psutil

from clarabot_nano.utility.subprocess.spawn import platform_fix_spawn_child_kwargs
from clarabot_nano.utility.thread import AbstractThread

logger = logging.getLogger(__name__)


def _spawn_reaper_thread(spawn: AbstractSpawn, proc: psutil.Popen):
    return_code = proc.wait()
    logger.debug("Reaping spawn %s with return code %s", proc.pid, return_code)
    spawn.dispose()
    spawn.clear_healthy()


class AbstractSpawn(object):
    DEFAULT_HEALTH_TIMEOUT = 12.0

    def __init__(self, health_timeout: float = DEFAULT_HEALTH_TIMEOUT, start_timeout: float = None):
        if not start_timeout:
            start_timeout = health_timeout
        self.health_timeout = health_timeout
        self.start_health_wait = time.perf_counter() + start_timeout
        self.last_healthy = 0
        self._disposed = False

    def is_disposed(self):
        return self._disposed

    def start(self):
        """
        Start spawn.
        """
        raise NotImplementedError()

    def dispose(self):
        """
        Stop spawn and do cleanup.
        """
        self._disposed = True

    def wait(self, timeout=5.0) -> bool:
        """
        Call this after cleanup is started to wait for finish.
        """
        raise NotImplementedError()

    def kill(self):
        """
        Final measure to stop the spawn.
        """
        raise NotImplementedError()

    def get_pid_prefix(self) -> str:
        """
        Process pid file prefix. This is used to categorize the spawned subprocesses by uniqueness.
        If the spawn-index is also added they are slot-bound and the cleanup will properly only stop the process
        of that slot/index.
        Also used for logging.
        """
        raise NotImplementedError()

    def set_healthy(self):
        if not self._disposed:
            self.last_healthy = time.perf_counter()

    def clear_healthy(self):
        self.last_healthy = -self.health_timeout

    def is_healthy(self):
        if self.last_healthy > 0:
            return self.last_healthy > time.perf_counter() - self.health_timeout
        return time.perf_counter() < self.start_health_wait


class AbstractSpawnWithThread(AbstractSpawn, AbstractThread):
    CONTROL_THREAD_COMMAND_START = "start"
    CONTROL_THREAD_COMMAND_STOP = "stop"

    def __init__(self, health_timeout: float = AbstractSpawn.DEFAULT_HEALTH_TIMEOUT, start_timeout: float = None):
        AbstractSpawn.__init__(self, health_timeout=health_timeout, start_timeout=start_timeout)
        AbstractThread.__init__(self)
        self._thread_queue = Queue()

    def start(self):
        AbstractThread.start(self)
        self._thread_queue.put(self._get_stop_command())  # force restart if any is running still
        self._thread_queue.put(self._get_start_command())

    def dispose(self):
        AbstractSpawn.dispose(self)
        self._thread_queue.put(self._get_stop_command())
        self._thread_queue.put(None)  # schedules termination of thread
        self.stop(0)  # only marks unhealthy (self.__alive is not used by thread as exit-signal)

    def is_healthy(self):
        return self.is_alive() and AbstractSpawn.is_healthy(self)

    def wait(self, timeout=5.0) -> bool:
        self.stop(timeout)  # after the thread is done, spawn is done
        return self.is_finished()

    def kill(self):
        """
        This will be used by the main thread to indicate final shutdown is commencing. And everything must stop ASAP.
        """
        raise NotImplementedError()

    def _get_stop_command(self) -> Union[str, Tuple[str, ...]]:
        """
        To pass in custom arguments to the handler override this and return a tuple with the appended arguments.
        """
        return self.CONTROL_THREAD_COMMAND_STOP

    def _get_start_command(self) -> Union[str, Tuple[str, ...]]:
        """
        To pass in custom arguments to the handler override this and return a tuple with the appended arguments.
        """
        return self.CONTROL_THREAD_COMMAND_START

    # below all runs in the control thread
    def _thread(self):
        while True:
            command_args = self._thread_queue.get()
            if not command_args:
                break
            self._thread_command(command_args)

    def _thread_command(self, command_args):
        if isinstance(command_args, tuple):
            command = command_args[0]
            args = command_args[1:]
        else:
            command = command_args
            args = ()
        if command == self.CONTROL_THREAD_COMMAND_STOP:
            self._stop_spawn(*args)
        elif command == self.CONTROL_THREAD_COMMAND_START:
            p = self._start_spawn(*args)
            if p:
                self._post_start_spawn(p)
        else:
            raise Exception(f"Unknown spawn command {command_args}")

    def _stop_spawn(self, *args):
        """
        Custom parameters may be passed in, but accessing the attributes on self is also possible. Fields that do
        not change after set in __init__ are safe to be used from this thread as well.
        """
        raise NotImplementedError()

    def _start_spawn(self, *args) -> psutil.Popen:
        """
        Custom parameters may be passed in, but accessing the attributes on self is also possible. Fields that do
        not change after set in __init__ are safe to be used from this thread as well.
        """
        raise NotImplementedError()

    def _post_start_spawn(self, p: psutil.Popen):
        """
        Setup anything that depends on the actual Popen object.
        """

    def _start_reap_popen(self, args, gui_type=False, /, **kwargs) -> psutil.Popen:
        p = psutil.Popen(args, **platform_fix_spawn_child_kwargs(kwargs, gui_type))
        # We need to explicitly receive the exit code of the processes on POSIX for them to be cleaned up.
        # This also starts disposing the spawn when it exists.
        threading.Thread(
            target=_spawn_reaper_thread,
            args=(self, p),
            daemon=True,
            name=f"spawn_reaper-{p.pid}",
        ).start()
        return p
