"""This file defines Northbound application controller classes."""

import abc
import copy
from ConfigParser import DEFAULTSECT

import networkx as nx

from fibbingnode.southbound.interface import FakeNodeProxy, ShapeshifterProxy
from fibbingnode.algorithms.ospf_simple import OSPFSimple
from fibbingnode.misc.sjmp import SJMPClient, ProxyCloner
from fibbingnode.misc.igp_graph import IGPGraph
from fibbingnode import CFG
from fibbingnode import log


def sanitize_edge_data(d):
    """Because json.decode() does not set all types back ..."""
    try:
        d['metric'] = int(d['metric'])
    except KeyError:
        pass
    return d


class SouthboundListener(ShapeshifterProxy):
    """This basic controller maintains a structure describing the IGP topology
    and listens for changes."""

    def __init__(self, *args, **kwargs):
        super(SouthboundListener, self).__init__(*args, **kwargs)
        self.igp_graph = IGPGraph()
        self.dirty = False
        self.json_proxy = SJMPClient(hostname=CFG.get(DEFAULTSECT,
                                                      'json_hostname'),
                                     port=CFG.getint(DEFAULTSECT, 'json_port'),
                                     target=self)
        self.quagga_manager = ProxyCloner(FakeNodeProxy, self.json_proxy)

    def run(self):
        """Connect the the southbound controller. This call will not return
        unless the connection is halted."""
        log.info('Connecting to server ...')
        self.json_proxy.communicate()

    def stop(self):
        """Stop the connection to the southbound controller"""
        self.json_proxy.stop()

    def bootstrap_graph(self, graph, node_properties):
        self.igp_graph.clear()
        self.igp_graph.add_edges_from(graph)
        for _, _, d in self.igp_graph.edges_iter(data=True):
            sanitize_edge_data(d)
        self.update_node_properties(**node_properties)
        log.debug('Bootstrapped graph with edges: %s and properties: %s',
                  self.igp_graph.edges(data=True), node_properties)
        self.received_initial_graph()
        self.graph_changed()

    def received_initial_graph(self):
        """Called when the initial graph has been bootstrapped, before
        calling graph_changed"""
        pass

    def add_edge(self, source, destination, properties={'metric': 1}):
        properties = sanitize_edge_data(properties)
        # metric is added twice to support backward-compat.
        self.igp_graph.add_edge(source, destination, properties)
        log.debug('Added edge: %s-%s@%s', source, destination, properties)
        # Only trigger an update if the link is bidirectional
        self.dirty = self.igp_graph.has_edge(destination, source)

    def commit(self):
        log.debug('End of graph update')
        if self.dirty:
            self.dirty = False
            self.graph_changed()

    @abc.abstractmethod
    def graph_changed(self):
        """Called when the IGP graph has changed."""

    def remove_edge(self, source, destination):
        # TODO: pay attention to re-add the symmetric edge if only one way
        # crashed
        try:
            self.igp_graph.remove_edge(source, destination)
            log.debug('Removed edge %s-%s', source, destination)
            self.igp_graph.remove_edge(destination, source)
            log.debug('Removed edge %s-%s', destination, source)
        except nx.NetworkXError:
            # This means that we had already removed both side of the edge
            # earlier or that the adjacency was not fully established before
            # going down
            pass
        else:
            self.dirty = True

    def update_node_properties(self, **properties):
        log.debug('Updating node propeties: %s', properties)
        for node, data in properties.iteritems():
            self.igp_graph.node[node].update(data)
        self.dirty = self.dirty or properties


class SouthboundController(SouthboundListener):
    """A simple northbound controller that monitors for changes in the IGP
    graph, and keeps track of advertized LSAs to remove them on exit"""
    def __init__(self, *args, **kwargs):
        super(SouthboundController, self).__init__(*args, **kwargs)
        self.advertized_lsa = set()

    def stop(self):
        self.remove_lsa(*self.advertized_lsa)
        super(SouthboundController, self).stop()

    @abc.abstractmethod
    def refresh_augmented_topo(self):
        """The IGP graph has changed, return the _set_ of LSAs that need to be
        advertized in the network (possibly just the previous one)"""

    def graph_changed(self):
        self.refresh_lsas()

    def advertize_lsa(self, *lsas):
        """Instructs the southbound controller to announce LSAs"""
        lsas = list(lsas)
        if lsas:
            self.quagga_manager.add(lsas)
            self.advertized_lsa.update(lsas)
        else:
            log.warning('Tried to advertize an empty list of LSA')

    def remove_lsa(self, *lsas):
        """Instructs the southbound controller to remove LSAs"""
        lsas = list(lsas)
        if lsas:
            self.quagga_manager.remove(lsas)
            self.advertized_lsa.difference_update(lsas)
        else:
            log.warning('Tried to remove an empty list of LSA')

    def _get_diff_lsas(self):
        new_lsas = self.refresh_augmented_topo()
        log.debug('New LSA set: %s', new_lsas)
        to_add = new_lsas.difference(self.advertized_lsa)
        to_rem = self.advertized_lsa.difference(new_lsas)
        log.debug('Removing LSA set: %s', to_rem)
        self.advertized_lsa = new_lsas
        return to_add, to_rem

    def refresh_lsas(self):
        """Refresh the set of LSAs that needs to be sent in the IGP,
        and instructs the southbound controller to update it if changed"""
        (to_add, to_rem) = self._get_diff_lsas()
        if not to_add and not to_rem:
            log.debug('Nothing to do for the current topology')
            return
        if to_rem:
            self.remove_lsa(*to_rem)
        if to_add:
            self.advertize_lsa(*to_add)


class StaticPathManager(SouthboundController):
    """Dumb controller that will simply enforce static lsas"""
    def __init__(self, *args, **kwargs):
        super(StaticPathManager, self).__init__(*args, **kwargs)
        self.demands = set()

    def refresh_augmented_topo(self):
        return set(self.demands)

    def add_lie(self, *lies):
        """Add lies (LSA) to send in the network"""
        self.demands.update(lies)
        self.refresh_lsas()

    def remove_lie(self, *lies):
        """Remove lies (LSA) to send in the network"""
        self.demands.difference_update(lies)
        self.refresh_lsas()


class SouthboundManager(SouthboundController):
    """A Northbound controller that will use a solver to implement path
    requirements expressed as forwarding DAGs"""
    def __init__(self,
                 fwd_dags=None,
                 optimizer=None,
                 additional_routes=None,
                 *args, **kwargs):
        self.additional_routes = additional_routes
        self.current_lsas = set([])
        self.optimizer = optimizer if optimizer else OSPFSimple()
        self.fwd_dags = fwd_dags if fwd_dags else {}
        self.has_initial_topo = False
        super(SouthboundManager, self).__init__(*args, **kwargs)

    def refresh_augmented_topo(self):
        log.info('Solving topologies')
        if not self.json_proxy.alive() or not self.has_initial_topo:
            log.debug('Skipping as we do not yet have a topology')
            return self.advertized_lsa
        try:
            self.optimizer.solve(self.igp_graph.copy(),
                                 {p: dag.copy()
                                  for p, dag in self.fwd_dags.iteritems()})
        except Exception as e:
            log.exception(e)
            return self.advertized_lsa
        else:
            return set(self.optimizer.get_fake_lsas())

    def simple_path_requirement(self, prefix, path):
        """Add a path requirement for the given prefix.

        :param path: The ordered list of routerid composing the path.
                     E.g. for path = [A, B, C], the following edges will be
                     used as requirements: [](A, B), (B, C), (C, D)]"""
        self.fwd_dags[prefix] = IGPGraph(
                [(s, d) for s, d in zip(path[:-1], path[1:])])
        self.refresh_lsas()

    def add_dag_requirement(self, prefix, dag):
        self.fwd_dags[prefix] = dag.copy()
        self.refresh_lsas()

    def add_dag_requirements_from(self, fw_dags):
        """
        Adds a bunch of fw dag requirements
        :param fw_dags: dictionary prefix -> dag
        """
        self.fwd_dags.update(copy.deepcopy(fw_dags))
        self.refresh_lsas()

    def remove_dag_requirement(self, prefix):
        if prefix in self.fwd_dags.keys():
            self.fwd_dags.pop(prefix)
            self.refresh_lsas()

    def remove_all_dag_requirements(self):
        self.fwd_dags.clear()
        self.refresh_lsas()

    def received_initial_graph(self):
        log.debug('Sending initial lsa''s')
        self.has_initial_topo = True
        if self.additional_routes:
            self.advertize_lsa(*self.additional_routes)