
from __future__ import absolute_import
from __future__ import division

from collections import defaultdict
import logging
import threading

import six
import xml.etree.cElementTree as etree
from xml.sax.saxutils import escape

from libvirt import libvirtError, VIR_ERR_NO_NETWORK

from vdsm.common import libvirtconnection

LIBVIRT_NET_PREFIX = 'vdsm-'

_libvirt_net_lock = threading.Lock()


def createNetworkDef(network, bridged=True, iface=None):
    netName = LIBVIRT_NET_PREFIX + network

    def EtreeElement(tagName, text=None, **attrs):
        elem = etree.Element(tagName)
        if text:
            elem.text = escape(text)
        if attrs:
            for attr, value in six.iteritems(attrs):
                elem.set(attr, escape(str(value)))
        return elem

    root = etree.Element('network')
    nameElem = EtreeElement('name', netName)
    forwardElem = EtreeElement(
        'forward', mode='bridge' if bridged else 'passthrough'
    )
    root.append(nameElem)
    root.append(forwardElem)
    if bridged:
        root.append(EtreeElement('bridge', name=network))
    else:
        forwardElem.append(EtreeElement('interface', dev=iface))
    return etree.tostring(root)


def create_network(netname, iface, user_reference=None):
    with _libvirt_net_lock:
        if not is_libvirt_network(netname):
            bridged = netname == iface
            iface = None if bridged else iface
            _createNetwork(createNetworkDef(netname, bridged, iface))

        NetworksUsersCache.add(netname, user_reference)


def delete_network(netname, user_reference=None):
    with _libvirt_net_lock:
        NetworksUsersCache.remove(netname, user_reference)
        if not NetworksUsersCache.has_users(netname):
            removeNetwork(netname)


def _createNetwork(netXml):
    conn = libvirtconnection.get()
    net = conn.networkDefineXML(netXml)
    net.create()
    net.setAutostart(1)


def removeNetwork(network):
    netName = LIBVIRT_NET_PREFIX + network
    conn = libvirtconnection.get()

    net = _netlookup_by_name(conn, netName)
    if net:
        if net.isActive():
            net.destroy()
        if net.isPersistent():
            net.undefine()


def networks():
    nets = {}
    conn = libvirtconnection.get()
    allNets = ((net, net.name()) for net in conn.listAllNetworks(0))
    for net, netname in allNets:
        if netname.startswith(LIBVIRT_NET_PREFIX):
            netname = netname[len(LIBVIRT_NET_PREFIX):]
            nets[netname] = {}
            xml = etree.fromstring(net.XMLDesc(0))
            interface = xml.find('.//interface')
            if interface is not None:
                nets[netname]['iface'] = interface.get('dev')
                nets[netname]['bridged'] = False
            else:
                nets[netname]['bridge'] = xml.find('.//bridge').get('name')
                nets[netname]['bridged'] = True
    return nets


def is_libvirt_network(netname):
    conn = libvirtconnection.get()
    libvirt_nets = conn.listAllNetworks()
    netname = LIBVIRT_NET_PREFIX + netname
    return any(n.name() == netname for n in libvirt_nets)


def netname_o2l(ovirt_name):
    return LIBVIRT_NET_PREFIX + ovirt_name


def netname_l2o(libvirt_name):
    return libvirt_name[len(LIBVIRT_NET_PREFIX):]


def _netlookup_by_name(conn, netname):
    try:
        return conn.networkLookupByName(netname)
    except libvirtError as e:
        if e.get_error_code() == VIR_ERR_NO_NETWORK:
            return None
        raise


class NetworksUsersCache(object):
    _nets_users = defaultdict(set)

    @staticmethod
    def add(net, user_ref):
        if (net in NetworksUsersCache._nets_users and
                user_ref in NetworksUsersCache._nets_users[net]):
            logging.warning('Attempting to add an existing net user: %s/%s',
                            net, user_ref)

        NetworksUsersCache._nets_users[net].add(user_ref)

    @staticmethod
    def remove(net, user_ref):
        if net not in NetworksUsersCache._nets_users:
            logging.warning('Attempting to remove a non existing network: '
                            '%s/%s', net, user_ref)

        net_users = NetworksUsersCache._nets_users[net]
        try:
            net_users.remove(user_ref)
        except KeyError:
            logging.warning('Attempting to remove a non existing net user: '
                            '%s/%s', net, user_ref)
        if len(net_users) == 0:
            del NetworksUsersCache._nets_users[net]

    @staticmethod
    def has_users(net):
        if net not in NetworksUsersCache._nets_users:
            return False
        return len(NetworksUsersCache._nets_users[net]) > 0
