
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from contextlib import contextmanager

import sys
import logging
import six
import copy

from vdsm.common import hooks

from vdsm.network import connectivity
from vdsm.network import netstats
from vdsm.network import netswitch
from vdsm.network import sourceroute
from vdsm.network import validator
from vdsm.network.ipwrapper import DUMMY_BRIDGE
from vdsm.network.link import iface as link_iface
from vdsm.network.link import sriov
from vdsm.network.lldp import info as lldp_info

from . import canonicalize
from . ip import address as ipaddress
from . errors import RollbackIncomplete
from . import netconfpersistence


DUMMY_BRIDGE


def network_caps():
    return netswitch.configurator.netcaps(compatibility=30600)


def network_stats():
    return netstats.report()


def change_numvfs(pci_path, numvfs, devname):
    logging.info('Changing number of vfs on device %s -> %s.',
                 pci_path, numvfs)
    sriov.update_numvfs(pci_path, numvfs)
    sriov.persist_numvfs(devname, numvfs)

    link_iface.iface(devname).up()


def ip_addrs_info(device):
    return ipaddress.addrs_info(device)


def net2vlan(network_name):
    return netswitch.configurator.net2vlan(network_name)


def network_northbound(network_name):
    return netswitch.configurator.net2northbound(network_name)


def ovs_bridge(network_name):
    return netswitch.configurator.ovs_net2bridge(network_name)


def _build_setup_hook_dict(req_networks, req_bondings, req_options):

    hook_dict = {'request': {'networks': dict(req_networks),
                             'bondings': dict(req_bondings),
                             'options': dict(req_options)}}

    return hook_dict


def _apply_hook(bondings, networks, options):
    results = hooks.before_network_setup(
        _build_setup_hook_dict(networks, bondings, options))
    networks = results['request']['networks']
    bondings = results['request']['bondings']
    options = results['request']['options']
    return bondings, networks, options


@contextmanager
def _rollback():
    try:
        yield
    except RollbackIncomplete as roi:
        diff, excType, value = roi
        tb = sys.exc_info()[2]
        try:
            setupNetworks(diff.networks, diff.bonds,
                          {'inRollback': True, 'connectivityCheck': 0})
        except Exception:
            logging.error('Memory rollback failed.', exc_info=True)
        finally:
            six.reraise(excType, value, tb)


def setupNetworks(networks, bondings, options):
    networks = copy.deepcopy(networks)
    bondings = copy.deepcopy(bondings)
    options = copy.deepcopy(options)

    logging.info('Setting up network according to configuration: '
                 'networks:%r, bondings:%r, options:%r' % (networks,
                                                           bondings, options))
    try:
        canonicalize.canonicalize_networks(networks)
        canonicalize.canonicalize_external_bonds_used_by_nets(networks,
                                                              bondings)
        canonicalize.canonicalize_bondings(bondings)

        net_info = netswitch.configurator.netinfo()

        validator.validate(networks, bondings, net_info)

        running_config = netconfpersistence.RunningConfig()
        if netswitch.configurator.switch_type_change_needed(
                networks, bondings, running_config):
            _change_switch_type(
                networks, bondings, options, running_config, net_info)
        else:
            _setup_networks(networks, bondings, options, net_info)
    except:
        network_config_dict = {
            'request': {'networks': dict(networks),
                        'bondings': dict(bondings),
                        'options': dict(options)}}
        hooks.after_network_setup_fail(network_config_dict)
        raise
    else:
        hooks.after_network_setup(
            _build_setup_hook_dict(networks, bondings, options))


def _setup_networks(networks, bondings, options, net_info):
    bondings, networks, options = _apply_hook(bondings, networks, options)

    in_rollback = options.get('_inRollback', False)
    with _rollback():
        netswitch.configurator.setup(
            networks, bondings, options, net_info, in_rollback)


def _change_switch_type(networks, bondings, options, running_config, net_info):
    logging.debug('Applying switch type change')

    netswitch.configurator.validate_switch_type_change(
        networks, bondings, running_config)

    in_rollback = options.get('_inRollback', False)

    logging.debug('Removing current switch configuration')
    with _rollback():
        _remove_nets_and_bonds(networks, bondings, net_info, in_rollback)

    logging.debug('Setting up requested switch configuration')
    try:
        with _rollback():
            net_info = netswitch.configurator.netinfo()
            netswitch.configurator.setup(
                networks, bondings, options, net_info, in_rollback)
    except:
        logging.exception('Requested switch setup failed, rolling back to '
                          'initial configuration')
        diff = running_config.diffFrom(netconfpersistence.RunningConfig())
        try:
            net_info = netswitch.configurator.netinfo()
            netswitch.configurator.setup(
                diff.networks,
                diff.bonds,
                {'connectivityCheck': False},
                net_info,
                in_rollback=True
            )
        except:
            logging.exception('Failed during rollback')
            raise
        raise


def _remove_nets_and_bonds(nets, bonds, net_info, in_rollback):
    nets_removal = {name: {'remove': True} for name in six.iterkeys(nets)}
    bonds_removal = {name: {'remove': True} for name in six.iterkeys(bonds)}
    netswitch.configurator.setup(
        nets_removal,
        bonds_removal,
        {'connectivityCheck': False},
        net_info,
        in_rollback
    )


def setSafeNetworkConfig():
    netswitch.configurator.persist()


def add_sourceroute(iface, ip, mask, route):
    sourceroute.add(iface, ip, mask, route)


def remove_sourceroute(iface):
    sourceroute.remove(iface)


def add_ovs_vhostuser_port(bridge, port, socket_path):
    netswitch.configurator.ovs_add_vhostuser_port(bridge, port, socket_path)


def remove_ovs_port(bridge, port):
    netswitch.configurator.ovs_remove_port(bridge, port)


def confirm_connectivity():
    connectivity.confirm()


def get_lldp_info(filter):
    if not filter.get('devices', []):
        filter['devices'] = netswitch.configurator.netinfo()['nics'].keys()
    return lldp_info.get_info(filter)
