"""
OpenVPN Connection
"""
from __future__ import (print_function)
from functools import partial
import logging
import os
import psutil
import shutil
import select
import socket
from time import sleep

logger = logging.getLogger(name=__name__)

from leap.base.connection import Connection
from leap.base.constants import OPENVPN_BIN
from leap.util.coroutines import spawn_and_watch_process
from leap.util.misc import get_openvpn_pids

from leap.eip.udstelnet import UDSTelnet
from leap.eip import config as eip_config
from leap.eip import exceptions as eip_exceptions


class OpenVPNManagement(object):

    # TODO explain a little bit how management interface works
    # and our telnet interface with support for unix sockets.

    """
    for more information, read openvpn management notes.
    zcat `dpkg -L openvpn | grep management`
    """

    def _connect_to_management(self):
        """
        Connect to openvpn management interface
        """
        if hasattr(self, 'tn'):
            self._close_management_socket()
        self.tn = UDSTelnet(self.host, self.port)

        # XXX make password optional
        # specially for win. we should generate
        # the pass on the fly when invoking manager
        # from conductor

        #self.tn.read_until('ENTER PASSWORD:', 2)
        #self.tn.write(self.password + '\n')
        #self.tn.read_until('SUCCESS:', 2)
        if self.tn:
            self._seek_to_eof()
        return True

    def _close_management_socket(self, announce=True):
        """
        Close connection to openvpn management interface
        """
        logger.debug('closing socket')
        if announce:
            self.tn.write("quit\n")
            self.tn.read_all()
        self.tn.get_socket().close()
        del self.tn

    def _seek_to_eof(self):
        """
        Read as much as available. Position seek pointer to end of stream
        """
        try:
            b = self.tn.read_eager()
        except EOFError:
            logger.debug("Could not read from socket. Assuming it died.")
            return
        while b:
            try:
                b = self.tn.read_eager()
            except EOFError:
                logger.debug("Could not read from socket. Assuming it died.")

    def _send_command(self, cmd):
        """
        Send a command to openvpn and return response as list
        """
        if not self.connected():
            try:
                self._connect_to_management()
            except eip_exceptions.MissingSocketError:
                #logger.warning('missing management socket')
                return []
        try:
            if hasattr(self, 'tn'):
                self.tn.write(cmd + "\n")
        except socket.error:
            logger.error('socket error')
            self._close_management_socket(announce=False)
            return []
        try:
            buf = self.tn.read_until(b"END", 2)
            self._seek_to_eof()
            blist = buf.split('\r\n')
            if blist[-1].startswith('END'):
                del blist[-1]
                return blist
            else:
                return []
        except socket.error as exc:
            logger.debug('socket error: %s' % exc.message)
        except select.error as exc:
            logger.debug('select error: %s' % exc.message)

    def _send_short_command(self, cmd):
        """
        parse output from commands that are
        delimited by "success" instead
        """
        if not self.connected():
            self.connect()
        self.tn.write(cmd + "\n")
        # XXX not working?
        buf = self.tn.read_until(b"SUCCESS", 2)
        self._seek_to_eof()
        blist = buf.split('\r\n')
        return blist

    #
    # random maybe useful vpn commands
    #

    def pid(self):
        #XXX broken
        return self._send_short_command("pid")


class OpenVPNConnection(Connection, OpenVPNManagement):
    """
    All related to invocation
    of the openvpn binary.
    It's extended by EIPConnection.
    """

    # XXX Inheriting from Connection was an early design idea
    # but currently that's an empty class.
    # We can get rid of that if we don't use it for sharing
    # state with other leap modules.

    def __init__(self,
                 watcher_cb=None,
                 debug=False,
                 host=None,
                 port="unix",
                 password=None,
                 *args, **kwargs):
        """
        :param watcher_cb: callback to be \
called for each line in watched stdout
        :param signal_map: dictionary of signal names and callables \
to be triggered for each one of them.
        :type watcher_cb: function
        :type signal_map: dict
        """
        #XXX FIXME
        #change watcher_cb to line_observer
        # XXX if not host: raise ImproperlyConfigured

        logger.debug('init openvpn connection')
        self.debug = debug
        self.ovpn_verbosity = kwargs.get('ovpn_verbosity', None)

        self.watcher_cb = watcher_cb
        #self.signal_maps = signal_maps

        self.subp = None
        self.watcher = None

        self.server = None
        self.port = None
        self.proto = None

        self.command = None
        self.args = None

        # XXX get autostart from config
        self.autostart = True

        # management interface init
        self.host = host
        if isinstance(port, str) and port.isdigit():
            port = int(port)
        elif port == "unix":
            port = "unix"
        else:
            port = None
        self.port = port
        self.password = password

    def run_openvpn_checks(self):
        """
        runs check needed before launching
        openvpn subprocess. will raise if errors found.
        """
        logger.debug('running openvpn checks')
        # XXX I think that "check_if_running" should be called
        # from try openvpn connection instead. -- kali.
        # let's prepare tests for that before changing it...
        self._check_if_running_instance()
        self._set_ovpn_command()
        self._check_vpn_keys()

    def try_openvpn_connection(self):
        """
        attempts to connect
        """
        # XXX should make public method
        if self.command is None:
            raise eip_exceptions.EIPNoCommandError
        if self.subp is not None:
            logger.debug('cowardly refusing to launch subprocess again')
            # XXX this is not returning ???!!
            # FIXME -- so it's calling it all the same!!

        self._launch_openvpn()

    def connected(self):
        """
        Returns True if connected
        rtype: bool
        """
        # XXX make a property
        return hasattr(self, 'tn')

    def terminate_openvpn_connection(self, shutdown=False):
        """
        terminates openvpn child subprocess
        """
        if self.subp:
            try:
                self._stop_openvpn()
            except eip_exceptions.ConnectionRefusedError:
                logger.warning(
                    'unable to send sigterm signal to openvpn: '
                    'connection refused.')

            # XXX kali --
            # XXX review-me
            # I think this will block if child process
            # does not return.
            # Maybe we can .poll() for a given
            # interval and exit in any case.

            RETCODE = self.subp.wait()
            if RETCODE:
                logger.error(
                    'cannot terminate subprocess! Retcode %s'
                    '(We might have left openvpn running)' % RETCODE)

        if shutdown:
            self._cleanup_tempfiles()

    def _cleanup_tempfiles(self):
        """
        remove all temporal files
        we might have left behind
        """
        # if self.port is 'unix', we have
        # created a temporal socket path that, under
        # normal circumstances, we should be able to
        # delete

        if self.port == "unix":
            logger.debug('cleaning socket file temp folder')

            tempfolder = os.path.split(self.host)[0]
            if os.path.isdir(tempfolder):
                try:
                    shutil.rmtree(tempfolder)
                except OSError:
                    logger.error('could not delete tmpfolder %s' % tempfolder)

    # checks

    def _check_if_running_instance(self):
        """
        check if openvpn is already running
        """
        openvpn_pids = get_openvpn_pids()
        if openvpn_pids:
            logger.debug('an openvpn instance is already running.')
            logger.debug('attempting to stop openvpn instance.')
            if not self._stop_openvpn():
                raise eip_exceptions.OpenVPNAlreadyRunning
            return
        else:
            logger.debug('no openvpn instance found.')

    def _set_ovpn_command(self):
        try:
            command, args = eip_config.build_ovpn_command(
                provider=self.provider,
                debug=self.debug,
                socket_path=self.host,
                ovpn_verbosity=self.ovpn_verbosity)
        except eip_exceptions.EIPNoPolkitAuthAgentAvailable:
            command = args = None
            raise
        except eip_exceptions.EIPNoPkexecAvailable:
            command = args = None
            raise

        # XXX if not command, signal error.
        self.command = command
        self.args = args

    def _check_vpn_keys(self):
        """
        checks for correct permissions on vpn keys
        """
        try:
            eip_config.check_vpn_keys(provider=self.provider)
        except eip_exceptions.EIPInitBadKeyFilePermError:
            logger.error('Bad VPN Keys permission!')
            # do nothing now
        # and raise the rest ...

    # starting and stopping openvpn subprocess

    def _launch_openvpn(self):
        """
        invocation of openvpn binaries in a subprocess.
        """
        #XXX TODO:
        #deprecate watcher_cb,
        #use _only_ signal_maps instead

        #logger.debug('_launch_openvpn called')
        if self.watcher_cb is not None:
            linewrite_callback = self.watcher_cb
        else:
            #XXX get logger instead
            linewrite_callback = lambda line: logger.debug(
                    'watcher: %s' % line)

        # the partial is not
        # being applied now because we're not observing the process
        # stdout like we did in the early stages. but I leave it
        # here since it will be handy for observing patterns in the
        # thru-the-manager updates (with regex)
        observers = (linewrite_callback,
                     partial(lambda con_status,
                             line: linewrite_callback, self.status))
        subp, watcher = spawn_and_watch_process(
            self.command,
            self.args,
            observers=observers)
        self.subp = subp
        self.watcher = watcher

    def _stop_openvpn(self):
        """
        stop openvpn process
        by sending SIGTERM to the management
        interface
        """
        # XXX method a bit too long, split
        logger.debug("atempting to terminate openvpn process...")
        if self.connected():
            try:
                self._send_command("signal SIGTERM\n")
                sleep(1)
                if not self.subp:  # XXX ???
                    return True
            except socket.error:
                logger.warning('management socket died')
                return

        #shutting openvpn failured
        #try patching in old openvpn host and trying again
        # XXX could be more than one!
        process = self._get_openvpn_process()
        if process:
            logger.debug('process: %s' % process.name)
            cmdline = process.cmdline

            manag_flag = "--management"
            if isinstance(cmdline, list) and manag_flag in cmdline:
                _index = cmdline.index(manag_flag)
                self.host = cmdline[_index + 1]
                self._send_command("signal SIGTERM\n")

            #make sure the process was terminated
            process = self._get_openvpn_process()
            if not process:
                logger.debug("Existing OpenVPN Process Terminated")
                return True
            else:
                logger.error("Unable to terminate existing OpenVPN Process.")
                return False

        return True

    def _get_openvpn_process(self):
        for process in psutil.process_iter():
            if OPENVPN_BIN in process.name:
                return process
        return None

    def get_log(self, lines=1):
        log = self._send_command("log %s" % lines)
        return log