# -*- coding: utf-8 -*-
# manager.py
# Copyright (C) 2015 LEAP
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""
VPN Tunnel.
"""

import os
import tempfile

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

from ._config import _TempVPNConfig, _TempProviderConfig
from .constants import IS_WIN
from .process import VPNProcess


# TODO ----------------- refactor --------------------
# [ ] 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]",),
# TODO ----------------- refactor --------------------


class ConfiguredTunnel(object):

    """
    A ConfiguredTunne holds the configuration for a VPN connection, and allows
    to control that connection.

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

    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.
    """

    log = Logger()

    def __init__(self, provider, remotes, cert_path, key_path, ca_path,
                 extra_flags):
        """
        :param remotes: a list of gateways tuple (ip, port) looking like this:
            ((ip1, portA), (ip2, portB), ...)
        :type remotes: tuple of tuple(str, int)
        """
        self._remotes = remotes
        ports = []

        self._vpnconfig = _TempVPNConfig(extra_flags, cert_path, ports)
        self._providerconfig = _TempProviderConfig(provider, ca_path)

        host, port = _get_management_location()
        self._host = host
        self._port = port
        self._vpnproc = None

    def start(self):
        return self._start_vpn()

    def stop(self):
        return self._stop_vpn(restart=False)

    #  status

    @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

    # VPN Control

    def _start_vpn(self):
        self.log.debug('VPN: start')
        args = [self._vpnconfig, self._providerconfig, self._host,
                self._port]
        kwargs = {'openvpn_verb': 4, 'remotes': self._remotes,
                  'restartfun': self._restart_vpn}
        vpnproc = VPNProcess(*args, **kwargs)
        self._vpnproc = vpnproc

        self._start_pre_up(vpnproc)
        cmd = self.__start_get_cmd(vpnproc)
        # XXX this should be a deferred
        running = self.__start_spawn_proc(vpnproc, cmd)
        if running:
            vpnproc.pid = running.pid
            return True
        else:
            return False

    def __start_pre_up(self, proc):
        try:
            proc.preUp()
        except Exception as exc:
            self.log.error('Error on vpn pre-up {0!r}'.format(exc))
            raise

    def __start_get_cmd(self, proc):
        try:
            cmd = proc.getCommand()
        except Exception as exc:
            self.log.error(
                'Error while getting vpn command... {0!r}'.format(exc))
            raise
        return cmd

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

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

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

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

        :param restart: whether this stop is part of a hard restart.
        :type restart: bool
        """
        # TODO how to return False if this fails
        # XXX maybe return a deferred

        if self._vpnproc is None:
            self.log.debug('Tried to stop VPN but no process found')
            return

        self._vpnproc.restarting = restart
        self.__stop_pre_down(self._vpnproc)
        self._vpnproc.terminate_or_kill()

    def __stop_pre_down(self, proc):
        try:
            proc.preDown()
        except Exception as e:
            self.log.error('Error on vpn pre-down {0!r}'.format(e))
            raise


# utils


def _get_management_location():
    """
    Return a tuple with the host (socket) and port to be used for VPN.

    :return: (host, port)
    :rtype: tuple (str, str)
    """
    if IS_WIN:
        host = "localhost"
        port = "9876"
    else:
        host = os.path.join(
            tempfile.mkdtemp(prefix="leap-tmp"), 'openvpn.socket')
        port = "unix"
    return host, port