import os
import subprocess

from twisted.internet.task import LoopingCall
from twisted.internet import reactor
from twisted.logger import Logger

from .process import VPNProcess
from .constants import IS_MAC

logger = Logger()

# NOTE: We need to set a bigger poll time in OSX because it seems
# openvpn malfunctions when you ask it a lot of things in a short
# amount of time.
POLL_TIME = 2.5 if IS_MAC else 1.0


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

    OPENVPN_VERB = "openvpn_verb"

    def __init__(self, remotes, vpnconfig,
                 providerconfig, socket_host, socket_port):
        self._vpnproc = None
        self._pollers = []

        self._openvpn_verb = 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):
        logger.debug('VPN: start')

        self._user_stopped = False
        self._stop_pollers()

        vpnproc = VPNProcess(
            self._vpnconfig, self._providerconfig, self._host,
            self._port, openvpn_verb=7, remotes=self._remotes,
            restartfun=self.restart)

        if vpnproc.get_openvpn_process():
            logger.info("Another vpn process is running. Will try to stop it.")
            vpnproc.stop_if_already_running()

        # FIXME it would be good to document where the
        # errors here are catched, since we currently handle them
        # at the frontend layer. This *should* move to be handled entirely
        # in the backend.

        try:
            cmd = vpnproc.getCommand()
        except Exception as e:
            logger.error("Error while getting vpn command... {0!r}".format(e))
            raise

        env = os.environ
        for key, val in vpnproc.vpn_env.items():
            env[key] = val

        reactor.spawnProcess(vpnproc, cmd[0], cmd, env)
        self._vpnproc = vpnproc

        # add pollers for status and state
        # this could be extended to a collection of
        # generic watchers

        poll_list = [LoopingCall(vpnproc.pollStatus),
                     LoopingCall(vpnproc.pollState)]
        self._pollers.extend(poll_list)
        self._start_pollers()
        return True

    def restart(self):
        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
        """
        self._stop_pollers()

        # First we try to be polite and send a SIGTERM...
        if self._vpnproc is not None:
            # We assume that the only valid stops are initiated
            # by an user action, not hard restarts
            self._user_stopped = not restart
            self._vpnproc.restarting = restart

            self._sentterm = True
            self._vpnproc.terminate_openvpn(shutdown=shutdown)

            # ...but we also trigger a countdown to be unpolite
            # if strictly needed.
            reactor.callLater(
                self.TERMINATE_WAIT, self._kill_if_left_alive)
        else:
            logger.debug("VPN is not running.")

        return True

    # FIXME -- is this used from somewhere???
    def bitmask_root_vpn_down(self):
        """
        Bring openvpn down using the privileged wrapper.
        """
        if IS_MAC:
            # We don't support Mac so far
            return True
        BM_ROOT = force_eval(linux.LinuxVPNLauncher.BITMASK_ROOT)

        # FIXME -- port to processProtocol
        exitCode = subprocess.call(["pkexec",
                                    BM_ROOT, "openvpn", "stop"])
        return True if exitCode is 0 else False

    @property
    def status(self):
        if not self._vpnproc:
            return 'OFFLINE'
        return self._vpnproc.status

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

    def _killit(self):
        """
        Sends a kill signal to the process.
        """
        self._stop_pollers()
        if self._vpnproc is None:
            logger.debug("There's no vpn process running to kill.")
        else:
            self._vpnproc.aborted = True
            self._vpnproc.killProcess()

    def _kill_if_left_alive(self, 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
        """
        while tries < self.TERMINATE_MAXTRIES:
            if self._vpnproc.transport.pid is None:
                logger.debug("Process has been happily terminated.")
                return
            else:
                logger.debug("Process did not die, waiting...")

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

        # after running out of patience, we try a killProcess
        logger.debug("Process did not died. Sending a SIGKILL.")
        try:
            self._killit()
        except OSError:
            logger.error("Could not kill process!")

    def _start_pollers(self):
        """
        Iterate through the registered observers
        and start the looping call for them.
        """
        for poller in self._pollers:
            poller.start(POLL_TIME)

    def _stop_pollers(self):
        """
        Iterate through the registered observers
        and stop the looping calls if they are running.
        """
        for poller in self._pollers:
            if poller.running:
                poller.stop()
        self._pollers = []