
from __future__ import absolute_import
from __future__ import division
import logging
import errno
import six

from vdsm.network import dns
from vdsm.network.ip.address import ipv6_supported
from vdsm.network.ip import dhclient
from vdsm.network.ipwrapper import getLinks
from vdsm.network.link import dpdk
from vdsm.network.link import iface as link_iface
from vdsm.network.netconfpersistence import RunningConfig

from .addresses import getIpAddrs, getIpInfo, is_ipv6_local_auto
from . import bonding
from . import bridges
from . import nics
from .routes import get_routes, get_gateway, is_default_route
from .qos import report_network_qos


LEGACY_SWITCH = {'switch': 'legacy'}


class NetworkIsMissing(Exception):
    pass


def _get(vdsmnets=None):
    ipaddrs = getIpAddrs()
    routes = get_routes()

    devices_info = _devices_report(ipaddrs, routes)
    nets_info = _networks_report(vdsmnets, routes, ipaddrs, devices_info)

    _update_dhcp_info(nets_info, devices_info)

    networking_report = {'networks': nets_info}
    networking_report.update(devices_info)

    networking_report['nameservers'] = dns.get_host_nameservers()
    networking_report['supportsIPv6'] = ipv6_supported()

    return networking_report


def _update_dhcp_info(nets_info, devices_info):

    net_ifaces = {net_info['iface'] for net_info in six.viewvalues(nets_info)}
    flat_devs_info = {
        devname: devinfo
        for sub_devs in six.viewvalues(devices_info)
        for devname, devinfo in six.viewitems(sub_devs)
    }
    dhcp_info = dhclient.dhcp_info(net_ifaces | frozenset(flat_devs_info))

    for net_info in six.viewvalues(nets_info):
        net_info.update(dhcp_info[net_info['iface']])

    for devname, devinfo in six.viewitems(flat_devs_info):
        devinfo.update(dhcp_info[devname])


def _networks_report(vdsmnets, routes, ipaddrs, devices_info):
    if vdsmnets is None:
        running_nets = RunningConfig().networks
        nets_info = networks_base_info(running_nets, routes, ipaddrs)
    else:
        nets_info = vdsmnets

    for network_info in six.itervalues(nets_info):
        network_info.update(LEGACY_SWITCH)
        _update_net_southbound_info(network_info, devices_info)

    report_network_qos(nets_info, devices_info)

    return nets_info


def _update_net_southbound_info(network_info, devices_info):
    if network_info['bridged']:
        ports = set(network_info['ports'])
        for dev_type in ('bondings', 'nics', 'vlans'):
            sb_set = ports & set(devices_info[dev_type])
            if len(sb_set) == 1:
                network_info['southbound'], = sb_set
                return
        network_info['southbound'] = None
    else:
        network_info['southbound'] = network_info['iface']


def _devices_report(ipaddrs, routes):
    devs_report = {'bondings': {}, 'bridges': {}, 'nics': {}, 'vlans': {}}

    for dev in (link for link in getLinks() if not link.isHidden()):
        if dev.isBRIDGE():
            devinfo = devs_report['bridges'][dev.name] = bridges.info(dev)
        elif dev.isNICLike():
            if dev.isDPDK():
                devinfo = devs_report['nics'][dev.name] = dpdk.info(dev)
            else:
                devinfo = devs_report['nics'][dev.name] = nics.info(dev)
            devinfo.update(bonding.get_bond_slave_agg_info(dev.name))
        elif dev.isBOND():
            devinfo = devs_report['bondings'][dev.name] = bonding.info(dev)
            devinfo.update(bonding.get_bond_agg_info(dev.name))
            devinfo.update(LEGACY_SWITCH)
        elif dev.isVLAN():
            devinfo = devs_report['vlans'][dev.name] = {'iface': dev.device,
                                                        'vlanid': dev.vlanid}
        else:
            continue
        devinfo.update(_devinfo(dev, routes, ipaddrs))

    _permanent_hwaddr_info(devs_report)

    return devs_report


def _permanent_hwaddr_info(devs_report):
    paddr = bonding.permanent_address()
    nics_info = devs_report.get('nics', {})
    for nic, nicinfo in six.viewitems(nics_info):
        if nic in paddr:
            nicinfo['permhwaddr'] = paddr[nic]


def get(vdsmnets=None, compatibility=None):
    if compatibility is None:
        return _get(vdsmnets)
    elif compatibility < 30700:
        return _stringify_mtus(_get(vdsmnets))

    return _get(vdsmnets)


def _stringify_mtus(netinfo_data):
    for devtype in ('bondings', 'bridges', 'networks', 'nics', 'vlans'):
        for dev in six.itervalues(netinfo_data[devtype]):
            dev['mtu'] = str(dev['mtu'])
    return netinfo_data


def networks_base_info(running_nets, routes=None, ipaddrs=None):
    if routes is None:
        routes = get_routes()
    if ipaddrs is None:
        ipaddrs = getIpAddrs()

    info = {}
    for net, attrs in six.viewitems(running_nets):
        if attrs.get('switch') != 'legacy':
            continue
        iface = get_net_iface_from_config(net, attrs)
        try:
            if not link_iface.iface(iface).exists():
                raise NetworkIsMissing('Iface %s was not found' % iface)
            info[net] = _getNetInfo(iface, attrs['bridged'], routes, ipaddrs)
        except NetworkIsMissing:
            logging.warning('Missing network detected [%s]: %s', net, attrs)

    return info


def get_net_iface_from_config(net, netattrs):
    if netattrs['bridged']:
        return net
    iface = netattrs.get('bonding') or netattrs.get('nic')
    if 'vlan' in netattrs:
        iface = '{}.{}'.format(iface, netattrs['vlan'])

    return iface


def libvirt_vdsm_nets(nets):
    routes = get_routes()
    ipaddrs = getIpAddrs()

    d = {}
    for net, netAttr in six.iteritems(nets):
        try:
            devname = netAttr.get('iface', net)
            d[net] = _getNetInfo(devname, netAttr['bridged'], routes, ipaddrs)
        except KeyError:
    return d


def _devinfo(link, routes, ipaddrs):
    gateway = get_gateway(routes, link.name)
    ipv4addr, ipv4netmask, ipv4addrs, ipv6addrs = getIpInfo(
        link.name, ipaddrs, gateway)

    return {'addr': ipv4addr,
            'ipv4addrs': ipv4addrs,
            'ipv6addrs': ipv6addrs,
            'ipv6autoconf': is_ipv6_local_auto(link.name),
            'gateway': gateway,
            'ipv6gateway': get_gateway(routes, link.name, family=6),
            'mtu': link.mtu,
            'netmask': ipv4netmask,
            'ipv4defaultroute': is_default_route(gateway, routes)}


def _getNetInfo(iface, bridged, routes, ipaddrs):
    data = {}
    try:
        if bridged:
            data.update({'ports': bridges.ports(iface),
                         'stp': bridges.stp_state(iface)})
        else:
            data['interface'] = iface

        gateway = get_gateway(routes, iface)
        ipv4addr, ipv4netmask, ipv4addrs, ipv6addrs = getIpInfo(
            iface, ipaddrs, gateway)

        data.update({'iface': iface, 'bridged': bridged,
                     'addr': ipv4addr, 'netmask': ipv4netmask,
                     'ipv4addrs': ipv4addrs,
                     'ipv6addrs': ipv6addrs,
                     'ipv6autoconf': is_ipv6_local_auto(iface),
                     'gateway': gateway,
                     'ipv6gateway': get_gateway(routes, iface, family=6),
                     'ipv4defaultroute': is_default_route(gateway, routes),
                     'mtu': link_iface.iface(iface).mtu()})
    except (IOError, OSError) as e:
        if e.errno == errno.ENOENT or e.errno == errno.ENODEV:
            logging.info('Obtaining info for net %s.', iface, exc_info=True)
            raise NetworkIsMissing('Network %s was not found' % iface)
        else:
            raise
    return data


class NetInfo(object):
    def __init__(self, _netinfo):
        self.networks = _netinfo['networks']
        self.vlans = _netinfo['vlans']
        self.nics = _netinfo['nics']
        self.bondings = _netinfo['bondings']
        self.bridges = _netinfo['bridges']
        self.nameservers = _netinfo['nameservers']

    def del_network(self, network):
        del self.networks[network]

    def del_bonding(self, bonding):
        del self.bondings[bonding]

    def del_vlan(self, vlan):
        del self.vlans[vlan]

    def del_bridge(self, bridge):
        self.bridges.pop(bridge, None)

    def getVlansForIface(self, iface):
        for vlandict in six.itervalues(self.vlans):
            if iface == vlandict['iface']:
                yield vlandict['vlanid']

    def getNetworkForIface(self, iface):
        for network, netdict in six.iteritems(self.networks):
            if ('ports' in netdict and iface in netdict['ports'] or
                    'iface' in netdict and iface == netdict['iface']):
                return network

    def getBridgedNetworkForIface(self, iface):
        for bridge, netdict in six.iteritems(self.networks):
            if netdict['bridged'] and iface in netdict['ports']:
                return bridge

    def getNicsForBonding(self, bond):
        bondAttrs = self.bondings[bond]
        return bondAttrs['slaves']

    def getBondingForNic(self, nic):
        bondings = [b for (b, attrs) in six.iteritems(self.bondings) if
                    nic in attrs['slaves']]
        if bondings:
            assert len(bondings) == 1, \
                "Unexpected configuration: More than one bonding per nic"
            return bondings[0]
        return None

    def getNicsVlanAndBondingForNetwork(self, network):
        vlan = None
        vlanid = None
        bonding = None
        lnics = []

        if self.networks[network]['switch'] == 'legacy':
            if self.networks[network]['bridged']:
                ports = self.networks[network]['ports']
            else:
                ports = []
                interface = self.networks[network]['iface']
                ports.append(interface)

            for port in ports:
                if port in self.vlans:
                    nic = self.vlans[port]['iface']
                    vlanid = self.vlans[port]['vlanid']
                    port = nic
                if port in self.bondings:
                    bonding = port
                    lnics += self.bondings[bonding]['slaves']
                elif port in self.nics:
                    lnics.append(port)
        else:
            sb = self.networks[network]['southbound']
            if sb in self.bondings:
                bonding = sb
                lnics = self.bondings[bonding]['slaves']
            elif sb in self.nics:
                lnics = [sb]
            vlanid = self.networks[network].get('vlanid')
            vlan = ('%s.%s' % (bonding or lnics[0], vlanid)
                    if vlanid is not None else None)

        return lnics, vlan, vlanid, bonding

    def ifaceUsers(self, iface):
        users = set()
        for n, ndict in six.iteritems(self.networks):
            if ndict['bridged'] and iface in ndict['ports']:
                users.add(n)
            elif not ndict['bridged'] and iface == ndict['iface']:
                users.add(n)
        for b, bdict in six.iteritems(self.bondings):
            if iface in bdict['slaves']:
                users.add(b)
        for v, vdict in six.iteritems(self.vlans):
            if iface == vdict['iface']:
                users.add(v)
        return users


class CachingNetInfo(NetInfo):
    def __init__(self, _netinfo=None):
        if _netinfo is None:
            _netinfo = get()
        super(CachingNetInfo, self).__init__(_netinfo)

    def updateDevices(self):
        _netinfo = get(vdsmnets=self.networks)
        self.networks = _netinfo['networks']
        self.vlans = _netinfo['vlans']
        self.nics = _netinfo['nics']
        self.bondings = _netinfo['bondings']
        self.bridges = _netinfo['bridges']
        self.nameservers = _netinfo['nameservers']
