import os

from twisted.internet import reactor, defer
from twisted.logger import Logger

from .process import VPNProcess
from .constants import IS_LINUX


# TODO
# TODO merge these classes with service.
# [ ] register change state listener
# emit_async(catalog.VPN_STATUS_CHANGED)
# [ ] catch ping-restart
# 'NETWORK_UNREACHABLE': (
#    'Network is unreachable (code=101)',),
# 'PROCESS_RESTART_TLS': (
#    "SIGTERM[soft,tls-error]",),


class VPNControl(object):

    """
    This is the high-level object that the service knows about.
    It exposes the start and terminate methods.

    On start, it spawns a VPNProcess instance that will use a vpnlauncher
    suited for the running platform and connect to the management interface
    opened by the openvpn process, executing commands over that interface on
    demand.
    """

    TERMINATE_MAXTRIES = 10
    TERMINATE_WAIT = 1  # secs
    RESTART_WAIT = 2  # secs

    log = Logger()

    def __init__(self, remotes, vpnconfig,
                 providerconfig, socket_host, socket_port):
        self._vpnproc = None
        self._user_stopped = False

        self._remotes = remotes
        self._vpnconfig = vpnconfig
        self._providerconfig = providerconfig
        self._host = socket_host
        self._port = socket_port

    def start(self):
        self.log.debug('VPN: start')

        self._user_stopped = False

        args = [self._vpnconfig, self._providerconfig, self._host,
                self._port]
        kwargs = {'openvpn_verb': 4, 'remotes': self._remotes,
                  'restartfun': self.restart}

        vpnproc = VPNProcess(*args, **kwargs)

        # TODO -- restore
        # if get_openvpn_process():
        #    self.log.info(
        #        'Another vpn process is running. Will try to stop it.')
        #    vpnproc.stop_if_already_running()

        try:
            vpnproc.preUp()
        except Exception as e:
            self.log.error('Error on vpn pre-up {0!r}'.format(e))
            raise
        try:
            cmd = vpnproc.getCommand()
        except Exception as e:
            self.log.error(
                'Error while getting vpn command... {0!r}'.format(e))
            raise

        env = os.environ

        try:
            runningproc = reactor.spawnProcess(vpnproc, cmd[0], cmd, env)
        except Exception as e:
            self.log.error(
                'Error while spawning vpn process... {0!r}'.format(e))
            return False

        # TODO get pid from management instead
        vpnproc.pid = runningproc.pid
        self._vpnproc = vpnproc

        return True

    @defer.inlineCallbacks
    def restart(self):
        yield self.stop(shutdown=False, restart=True)
        reactor.callLater(
            self.RESTART_WAIT, self.start)

    def stop(self, shutdown=False, restart=False):
        """
        Stops the openvpn subprocess.

        Attempts to send a SIGTERM first, and after a timeout
        it sends a SIGKILL.

        :param shutdown: whether this is the final shutdown
        :type shutdown: bool
        :param restart: whether this stop is part of a hard restart.
        :type restart: bool
        """
        # We assume that the only valid stops are initiated
        # by an user action, not hard restarts
        self._user_stopped = not restart
        if self._vpnproc is not None:
            self._vpnproc.restarting = restart

        try:
            if self._vpnproc is not None:
                self._vpnproc.preDown()
        except Exception as e:
            self.log.error('Error on vpn pre-down {0!r}'.format(e))
            raise

        d = defer.succeed(True)
        if IS_LINUX:
            # TODO factor this out to a linux-only launcher mechanism.
            # First we try to be polite and send a SIGTERM...
            if self._vpnproc is not None:
                self._sentterm = True
                self._vpnproc.terminate()

                # we trigger a countdown to be unpolite
                # if strictly needed.
                d = defer.Deferred()
                reactor.callLater(
                    self.TERMINATE_WAIT, self._kill_if_left_alive, d)
        return d

    # TODO -- remove indirection
    @property
    def status(self):
        if not self._vpnproc:
            return {'status': 'off', 'error': None}
        return self._vpnproc.status

    @property
    def traffic_status(self):
        return self._vpnproc.traffic_status

    def _killit(self):
        if self._vpnproc is None:
            self.log.debug("There's no vpn process running to kill.")
        else:
            self._vpnproc.aborted = True
            self._vpnproc.kill()

    def _kill_if_left_alive(self, deferred, tries=0):
        """
        Check if the process is still alive, and send a
        SIGKILL after a timeout period.

        :param tries: counter of tries, used in recursion
        :type tries: int
        """
        if tries < self.TERMINATE_MAXTRIES:
            if self._vpnproc.transport.pid is None:
                deferred.callback(True)
                return
            else:
                self.log.debug('Process did not die, waiting...')

            tries += 1
            reactor.callLater(self.TERMINATE_WAIT,
                              self._kill_if_left_alive, deferred, tries)
            return

        # after running out of patience, we try a killProcess
        self.log.debug('Process did not die. Sending a SIGKILL.')
        try:
            self._killit()
        except OSError:
            self.log.error('Could not kill process!')
        deferred.callback(True)