
from __future__ import absolute_import
from __future__ import division

import libvirt

from vdsm.common import conv
from vdsm.common import xmlutils
from vdsm.network import api as net_api
from vdsm.virt import displaynetwork
from vdsm.virt import libvirtnetwork
from vdsm.virt import utils
from vdsm.config import config
from vdsm.virt import vmxml

from . import hwclass
from .core import Base
from .core import update_device_params
from .core import update_device_params_from_meta
from .core import find_device_type
from .core import get_metadata_values
from .core import dev_class_from_dev_elem
import six


LIBVIRT_PORT_AUTOSELECT = '-1'


_LEGACY_MAP = {
    'keyboardLayout': 'keyMap',
    'displayNetwork': 'displayNetwork',
    'spiceSecureChannels': 'spiceSecureChannels',
    'copyPasteEnable': 'copyPasteEnable',
    'fileTransferEnable': 'fileTransferEnable',
}


class Graphics(Base):

    SPICE_CHANNEL_NAMES = (
        'main', 'display', 'inputs', 'cursor', 'playback',
        'record', 'smartcard', 'usbredir')

    __slots__ = ('port', 'tlsPort',)

    @classmethod
    def get_identifying_attrs(cls, dev_elem):
        return {
            'devtype': dev_class_from_dev_elem(dev_elem),
            'name': find_device_type(dev_elem),
        }

    def get_metadata(self, dev_class):
        attrs = {
            'devtype': dev_class,
            'name': self.device,
        }
        return (
            attrs,
            get_metadata_values(self),
        )

    def __init__(self, log, **kwargs):
        super(Graphics, self).__init__(log, **kwargs)
        self.port = LIBVIRT_PORT_AUTOSELECT
        self.tlsPort = LIBVIRT_PORT_AUTOSELECT

    def setup(self):
        display_network = self.specParams.get('displayNetwork')
        if display_network:
            displaynetwork.create_network(display_network, self.vmid)
            display_ip = _getNetworkIp(display_network)
        else:
            display_ip = '0'
        self.specParams['displayIp'] = display_ip

    def teardown(self):
        display_network = self.specParams.get('displayNetwork')
        if display_network:
            displaynetwork.delete_network(display_network, self.vmid)

    def getSpiceVmcChannelsXML(self):
        vmc = vmxml.Element('channel', type='spicevmc')
        vmc.appendChildWithArgs('target', type='virtio',
                                name='com.redhat.spice.0')
        return vmc

    def _getSpiceChannels(self):
        for name in self.specParams['spiceSecureChannels'].split(','):
            if name in Graphics.SPICE_CHANNEL_NAMES:
                yield name
            elif (name[0] == 's' and name[1:] in
                  Graphics.SPICE_CHANNEL_NAMES):
                yield name[1:]
            else:
                self.log.error('unsupported spice channel name "%s"', name)

    @classmethod
    def from_xml_tree(cls, log, dev, meta):
        params = {
            'device': find_device_type(dev),
            'type': dev.tag,
        }
        update_device_params(params, dev, attrs=('port', 'tlsPort'))
        params['specParams'] = _make_spec_params(dev, meta)
        update_device_params_from_meta(params, meta)
        params['vmid'] = meta['vmid']
        return cls(log, **params)

    def getXML(self):
        graphicsAttrs = {
            'type': self.device,
            'port': self.port,
            'autoport': 'yes',
        }
        if config.getboolean('vars', 'ssl'):
            graphicsAttrs['defaultMode'] = 'secure'

        if self.device == 'spice':
            graphicsAttrs['tlsPort'] = self.tlsPort

        self._setPasswd(graphicsAttrs)

        if 'keyMap' in self.specParams:
            graphicsAttrs['keymap'] = self.specParams['keyMap']

        graphics = vmxml.Element('graphics', **graphicsAttrs)

        if not conv.tobool(self.specParams.get('copyPasteEnable', True)):
            clipboard = vmxml.Element('clipboard', copypaste='no')
            graphics.appendChild(clipboard)

        if not conv.tobool(
                self.specParams.get('fileTransferEnable', True)):
            filetransfer = vmxml.Element('filetransfer', enable='no')
            graphics.appendChild(filetransfer)

        if (self.device == 'spice' and
           'spiceSecureChannels' in self.specParams):
            for chan in self._getSpiceChannels():
                graphics.appendChildWithArgs('channel', name=chan,
                                             mode='secure')

        display_network = self.specParams.get('displayNetwork')
        if display_network:
            graphics.appendChildWithArgs(
                'listen', type='network',
                network=libvirtnetwork.netname_o2l(display_network))
        else:
            graphics.setAttrs(listen='0')

        return graphics

    def _setPasswd(self, attrs):
        attrs['passwd'] = '*****'
        attrs['passwdValidTo'] = '1970-01-01T00:00:01'

    def setupPassword(self, devXML):
        self._setPasswd(devXML.attrib)

    @classmethod
    def update_device_info(cls, vm, device_conf):
        for gxml in vm.domain.get_device_elements('graphics'):
            port = vmxml.attr(gxml, 'port')
            tlsPort = vmxml.attr(gxml, 'tlsPort')
            graphicsType = vmxml.attr(gxml, 'type')

            for d in device_conf:
                if d.device == graphicsType:
                    if port:
                        d.port = port
                    if tlsPort:
                        d.tlsPort = tlsPort
                    break

            for dev in vm.conf['devices']:
                if (dev.get('type') == hwclass.GRAPHICS and
                        dev.get('device') == graphicsType):
                    if port:
                        dev['port'] = port
                    if tlsPort:
                        dev['tlsPort'] = tlsPort
                    break

    def get_extra_xmls(self):
        if self.device == 'spice':
            yield self.getSpiceVmcChannelsXML()


def isSupportedDisplayType(vmParams):
    display = vmParams.get('display')
    if display is not None:
        if display not in ('vnc', 'qxl', 'qxlnc'):
            return False

    for dev in vmParams.get('devices', ()):
        if dev['type'] == hwclass.GRAPHICS:
            if dev['device'] not in ('spice', 'vnc'):
                return False

    return True


def makeSpecParams(conf):
    return dict((newName, conf[oldName])
                for oldName, newName in six.iteritems(_LEGACY_MAP)
                if oldName in conf)


def _getNetworkIp(network):
    try:
        nets = libvirtnetwork.networks()
        device = (nets[network].get('iface', network)
                  if network in nets else network)
        _, _, ipv4addrs, ipv6addrs = net_api.ip_addrs_info(device)
        if ipv4addrs:
            ip = ipv4addrs[0].split('/')[0]
        elif ipv6addrs:
            ip = ipv6addrs[0].split('/')[0]
        else:
            ip = '0'
    except (libvirt.libvirtError, KeyError, IndexError):
        ip = '0'
    finally:
        if ip == '':
            ip = '0'
    return ip


def _make_spec_params(dev, meta):
    spec_params = {
        'copyPasteEnable': _is_feature_flag_enabled(
            dev, 'clipboard', 'copypaste'
        ),
        'fileTransferEnable': _is_feature_flag_enabled(
            dev, 'filetransfer', 'enable'
        ),
    }
    key_map = dev.attrib.get('keymap')
    if key_map:
        spec_params['keyMap'] = key_map
    display_network = meta.get('display_network')
    if display_network:
        spec_params['displayNetwork'] = display_network
    listen = vmxml.find_first(dev, 'listen')
    if listen.attrib.get('type') == 'network':
        xml_display_network = listen.attrib.get('network')
        if xml_display_network:
            spec_params['displayNetwork'] = libvirtnetwork.netname_l2o(
                xml_display_network
            )
    elif listen.attrib.get('type') == 'address':
        spec_params['displayIp'] = listen.attrib.get('address', '0')
    return spec_params


def _is_feature_flag_enabled(dev, node, attr):
    value = vmxml.find_attr(dev, node, attr)
    if value is not None and value.lower() == 'no':
        return False
    else:
        return True


def is_vnc_secure(vmParams, log):
    if 'xml' not in vmParams:
        return True
    parsed = xmlutils.fromstring(vmParams['xml'])
    graphics = vmxml.find_all(parsed, 'graphics')
    for g in graphics:
        if vmxml.attr(g, 'type') == 'vnc':
            no_password_auth = vmxml.attr(g, 'passwdValidTo') == ''
            if no_password_auth and not utils.sasl_enabled():
                log.warn("VNC not secure: passwdValidTo empty or missing"
                         " and SASL not configured")
                return False
    return True
