import logging
import os
import platform
import re
import tempfile

from leap import __branding as BRANDING
from leap import certs
from leap.util.misc import null_check
from leap.util.fileutil import (which, mkdir_p, check_and_fix_urw_only)

from leap.base import config as baseconfig
from leap.baseapp.permcheck import (is_pkexec_in_system,
                                    is_auth_agent_running)
from leap.eip import exceptions as eip_exceptions
from leap.eip import specs as eipspecs

logger = logging.getLogger(name=__name__)
provider_ca_file = BRANDING.get('provider_ca_file', None)

_platform = platform.system()


class EIPConfig(baseconfig.JSONLeapConfig):
    spec = eipspecs.eipconfig_spec

    def _get_slug(self):
        eipjsonpath = baseconfig.get_config_file(
            'eip.json')
        return eipjsonpath

    def _set_slug(self, *args, **kwargs):
        raise AttributeError("you cannot set slug")

    slug = property(_get_slug, _set_slug)


class EIPServiceConfig(baseconfig.JSONLeapConfig):
    spec = eipspecs.eipservice_config_spec

    def _get_slug(self):
        domain = getattr(self, 'domain', None)
        if domain:
            path = baseconfig.get_provider_path(domain)
        else:
            path = baseconfig.get_default_provider_path()
        return baseconfig.get_config_file(
            'eip-service.json', folder=path)

    def _set_slug(self):
        raise AttributeError("you cannot set slug")

    slug = property(_get_slug, _set_slug)


def get_socket_path():
    socket_path = os.path.join(
        tempfile.mkdtemp(prefix="leap-tmp"),
        'openvpn.socket')
    #logger.debug('socket path: %s', socket_path)
    return socket_path


def get_eip_gateway(eipconfig=None, eipserviceconfig=None):
    """
    return the first host in eip service config
    that matches the name defined in the eip.json config
    file.
    """
    # XXX eventually we should move to a more clever
    # gateway selection. maybe we could return
    # all gateways that match our cluster.

    null_check(eipconfig, "eipconfig")
    null_check(eipserviceconfig, "eipserviceconfig")
    PLACEHOLDER = "testprovider.example.org"

    conf = eipconfig.config
    eipsconf = eipserviceconfig.config

    primary_gateway = conf.get('primary_gateway', None)
    if not primary_gateway:
        return PLACEHOLDER

    gateways = eipsconf.get('gateways', None)
    if not gateways:
        logger.error('missing gateways in eip service config')
        return PLACEHOLDER

    if len(gateways) > 0:
        for gw in gateways:
            clustername = gw.get('cluster', None)
            if not clustername:
                logger.error('no cluster name')
                return

            if clustername == primary_gateway:
                # XXX at some moment, we must
                # make this a more generic function,
                # and return ports, protocols...
                ipaddress = gw.get('ip_address', None)
                if not ipaddress:
                    logger.error('no ip_address')
                    return
                return ipaddress
    logger.error('could not find primary gateway in provider'
                 'gateway list')


def get_cipher_options(eipserviceconfig=None):
    """
    gathers optional cipher options from eip-service config.
    :param eipserviceconfig: EIPServiceConfig instance
    """
    null_check(eipserviceconfig, 'eipserviceconfig')
    eipsconf = eipserviceconfig.get_config()

    ALLOWED_KEYS = ("auth", "cipher", "tls-cipher")
    CIPHERS_REGEX = re.compile("[A-Z0-9\-]+")
    opts = []
    if 'openvpn_configuration' in eipsconf:
        config = eipserviceconfig.config.get(
            "openvpn_configuration", {})
        for key, value in config.items():
            if key in ALLOWED_KEYS and value is not None:
                sanitized_val = CIPHERS_REGEX.findall(value)
                if len(sanitized_val) != 0:
                    _val = sanitized_val[0]
                    opts.append('--%s' % key)
                    opts.append('%s' % _val)
    return opts

LINUX_UP_DOWN_SCRIPT = "/etc/leap/resolv-update"
OPENVPN_DOWN_ROOT = "/usr/lib/openvpn/openvpn-down-root.so"


def has_updown_scripts():
    """
    checks the existence of the up/down scripts
    """
    # XXX should check permissions too
    is_file = os.path.isfile(LINUX_UP_DOWN_SCRIPT)
    if not is_file:
        logger.warning(
            "Could not find up/down scripts at %s! "
            "Risk of DNS Leaks!!!")
    return is_file


def build_ovpn_options(daemon=False, socket_path=None, **kwargs):
    """
    build a list of options
    to be passed in the
    openvpn invocation
    @rtype: list
    @rparam: options
    """
    # XXX review which of the
    # options we don't need.

    # TODO pass also the config file,
    # since we will need to take some
    # things from there if present.

    provider = kwargs.pop('provider', None)
    eipconfig = EIPConfig(domain=provider)
    eipconfig.load()
    eipserviceconfig = EIPServiceConfig(domain=provider)
    eipserviceconfig.load()

    # get user/group name
    # also from config.
    user = baseconfig.get_username()
    group = baseconfig.get_groupname()

    opts = []

    opts.append('--client')

    opts.append('--dev')
    # XXX same in win?
    opts.append('tun')
    opts.append('--persist-tun')
    opts.append('--persist-key')

    verbosity = kwargs.get('ovpn_verbosity', None)
    if verbosity and 1 <= verbosity <= 6:
        opts.append('--verb')
        opts.append("%s" % verbosity)

    # remote ##############################
    # (server, port, protocol)

    opts.append('--remote')

    gw = get_eip_gateway(eipconfig=eipconfig,
                         eipserviceconfig=eipserviceconfig)
    logger.debug('setting eip gateway to %s', gw)
    opts.append(str(gw))

    # get port/protocol from eipservice too
    opts.append('1194')
    #opts.append('80')
    opts.append('udp')

    opts.append('--tls-client')
    opts.append('--remote-cert-tls')
    opts.append('server')

    # get ciphers #######################

    ciphers = get_cipher_options(
        eipserviceconfig=eipserviceconfig)
    for cipheropt in ciphers:
        opts.append(str(cipheropt))

    # set user and group
    opts.append('--user')
    opts.append('%s' % user)
    opts.append('--group')
    opts.append('%s' % group)

    opts.append('--management-client-user')
    opts.append('%s' % user)
    opts.append('--management-signal')

    # set default options for management
    # interface. unix sockets or telnet interface for win.
    # XXX take them from the config object.

    if _platform == "Windows":
        opts.append('--management')
        opts.append('localhost')
        # XXX which is a good choice?
        opts.append('7777')

    if _platform in ("Linux", "Darwin"):
        opts.append('--management')

        if socket_path is None:
            socket_path = get_socket_path()
        opts.append(socket_path)
        opts.append('unix')

        opts.append('--script-security')
        opts.append('2')

    if _platform == "Linux":
        if has_updown_scripts():
            opts.append("--up")
            opts.append(LINUX_UP_DOWN_SCRIPT)
            opts.append("--down")
            opts.append(LINUX_UP_DOWN_SCRIPT)
            opts.append("--plugin")
            opts.append(OPENVPN_DOWN_ROOT)
            opts.append("'script_type=down %s'" % LINUX_UP_DOWN_SCRIPT)

    # certs
    client_cert_path = eipspecs.client_cert_path(provider)
    ca_cert_path = eipspecs.provider_ca_path(provider)

    # XXX FIX paths for MAC
    opts.append('--cert')
    opts.append(client_cert_path)
    opts.append('--key')
    opts.append(client_cert_path)
    opts.append('--ca')
    opts.append(ca_cert_path)

    # we cannot run in daemon mode
    # with the current subp setting.
    # see: https://leap.se/code/issues/383
    #if daemon is True:
        #opts.append('--daemon')

    logger.debug('vpn options: %s', ' '.join(opts))
    return opts


def build_ovpn_command(debug=False, do_pkexec_check=True, vpnbin=None,
                       socket_path=None, **kwargs):
    """
    build a string with the
    complete openvpn invocation

    @rtype [string, [list of strings]]
    @rparam: a list containing the command string
        and a list of options.
    """
    command = []
    use_pkexec = True
    ovpn = None

    # XXX get use_pkexec from config instead.

    if _platform == "Linux" and use_pkexec and do_pkexec_check:

        # check for both pkexec
        # AND a suitable authentication
        # agent running.
        logger.info('use_pkexec set to True')

        if not is_pkexec_in_system():
            logger.error('no pkexec in system')
            raise eip_exceptions.EIPNoPkexecAvailable

        if not is_auth_agent_running():
            logger.warning(
                "no polkit auth agent found. "
                "pkexec will use its own text "
                "based authentication agent. "
                "that's probably a bad idea")
            raise eip_exceptions.EIPNoPolkitAuthAgentAvailable

        command.append('pkexec')

    if vpnbin is None:
        if _platform == "Darwin":
            # XXX Should hardcode our installed path
            # /Applications/LEAPClient.app/Contents/Resources/openvpn.leap
            openvpn_bin = "openvpn.leap"
        else:
            openvpn_bin = "openvpn"
        #XXX hardcode for darwin
        ovpn = which(openvpn_bin)
    else:
        ovpn = vpnbin
    if ovpn:
        vpn_command = ovpn
    else:
        vpn_command = "openvpn"
    command.append(vpn_command)
    daemon_mode = not debug

    for opt in build_ovpn_options(daemon=daemon_mode, socket_path=socket_path,
                                  **kwargs):
        command.append(opt)

    # XXX check len and raise proper error

    if _platform == "Darwin":
        OSX_ASADMIN = 'do shell script "%s" with administrator privileges'
        # XXX fix workaround for Nones
        _command = [x if x else " " for x in command]
        # XXX debugging!
        # XXX get openvpn log path from debug flags
        _command.append('--log')
        _command.append('/tmp/leap_openvpn.log')
        return ["osascript", ["-e", OSX_ASADMIN % ' '.join(_command)]]
    else:
        return [command[0], command[1:]]


def check_vpn_keys(provider=None):
    """
    performs an existance and permission check
    over the openvpn keys file.
    Currently we're expecting a single file
    per provider, containing the CA cert,
    the provider key, and our client certificate
    """
    assert provider is not None
    provider_ca = eipspecs.provider_ca_path(provider)
    client_cert = eipspecs.client_cert_path(provider)

    logger.debug('provider ca = %s', provider_ca)
    logger.debug('client cert = %s', client_cert)

    # if no keys, raise error.
    # it's catched by the ui and signal user.

    if not os.path.isfile(provider_ca):
        # not there. let's try to copy.
        folder, filename = os.path.split(provider_ca)
        if not os.path.isdir(folder):
            mkdir_p(folder)
        if provider_ca_file:
            cacert = certs.where(provider_ca_file)
        with open(provider_ca, 'w') as pca:
            with open(cacert, 'r') as cac:
                pca.write(cac.read())

    if not os.path.isfile(provider_ca):
        logger.error('key file %s not found. aborting.',
                     provider_ca)
        raise eip_exceptions.EIPInitNoKeyFileError

    if not os.path.isfile(client_cert):
        logger.error('key file %s not found. aborting.',
                     client_cert)
        raise eip_exceptions.EIPInitNoKeyFileError

    for keyfile in (provider_ca, client_cert):
        # bad perms? try to fix them
        try:
            check_and_fix_urw_only(keyfile)
        except OSError:
            raise eip_exceptions.EIPInitBadKeyFilePermError