"""BGP implementation for FAUCET."""

# Copyright (C) 2013 Nippon Telegraph and Telephone Corporation.
# Copyright (C) 2015 Brad Cowie, Christopher Lorier and Joe Stringer.
# Copyright (C) 2015 Research and Education Advanced Network New Zealand Ltd.
# Copyright (C) 2015--2017 The Contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import json
import ipaddress
from ryu.services.protocols.bgp.bgpspeaker import BGPSpeaker
from ryu.services.protocols.bgp.api.base import CoreNotStarted

from faucet.valve_util import btos


class FaucetBgp(object):

    def __init__(self, logger, send_flow_msgs):
        self._dp_bgp_speakers = {}
        self._metrics = None
        self._valves = None
        self.logger = logger
        self._send_flow_msgs = send_flow_msgs

    def _bgp_route_handler(self, path_change, vlan):
        """Handle a BGP change event.

        Args:
            path_change (ryu.services.protocols.bgp.bgpspeaker.EventPrefix): path change
            vlan (vlan): Valve VLAN this path change was received for.
        """
        prefix = ipaddress.ip_network(btos(path_change.prefix))
        nexthop = ipaddress.ip_address(btos(path_change.nexthop))
        withdraw = path_change.is_withdraw
        flowmods = []
        if not self._valves or vlan.dp_id not in self._valves:
            return
        valve = self._valves[vlan.dp_id]
        if vlan.is_faucet_vip(nexthop):
            self.logger.error(
                'BGP nexthop %s for prefix %s cannot be us',
                nexthop, prefix)
            return
        if vlan.ip_in_vip_subnet(nexthop) is None:
            self.logger.error(
                'BGP nexthop %s for prefix %s is not a connected network',
                nexthop, prefix)
            return

        if withdraw:
            self.logger.info(
                'BGP withdraw %s nexthop %s', prefix, nexthop)
            flowmods = valve.del_route(vlan, prefix)
        else:
            self.logger.info(
                'BGP add %s nexthop %s', prefix, nexthop)
            flowmods = valve.add_route(vlan, nexthop, prefix)
        if flowmods:
            self._send_flow_msgs(vlan.dp_id, flowmods)

    def _create_bgp_speaker_for_vlan(self, vlan):
        """Set up BGP speaker for an individual VLAN if required.

        Args:
            vlan (vlan): VLAN associated with this speaker.
        Returns:
            ryu.services.protocols.bgp.bgpspeaker.BGPSpeaker: BGP speaker.
        """
        handler = lambda x: self._bgp_route_handler(x, vlan)
        bgp_speaker = BGPSpeaker(
            as_number=vlan.bgp_as,
            router_id=vlan.bgp_routerid,
            bgp_server_port=vlan.bgp_port,
            bgp_server_hosts=vlan.bgp_server_addresses,
            best_path_change_handler=handler)
        for faucet_vip in vlan.faucet_vips:
            bgp_speaker.prefix_add(
                prefix=str(faucet_vip), next_hop=str(faucet_vip.ip))
        for ipv in vlan.ipvs():
            routes = vlan.routes_by_ipv(ipv)
            for ip_dst, ip_gw in list(routes.items()):
                bgp_speaker.prefix_add(
                    prefix=str(ip_dst), next_hop=str(ip_gw))
        for bgp_neighbor_address in vlan.bgp_neighbor_addresses:
            bgp_speaker.neighbor_add(
                address=bgp_neighbor_address,
                remote_as=vlan.bgp_neighbor_as,
                local_address=vlan.bgp_local_address,
                enable_ipv4=True,
                enable_ipv6=True)
        return bgp_speaker

    def reset(self, valves, metrics):
        """Set up a BGP speaker for every VLAN that requires it."""
        self._valves = valves
        self._metrics = metrics
        # TODO: port status changes should cause us to withdraw a route.
        for dp_id, valve in list(self._valves.items()):
            if dp_id not in self._dp_bgp_speakers:
                self._dp_bgp_speakers[dp_id] = {}
            bgp_speakers = self._dp_bgp_speakers[dp_id]
            for bgp_speaker in list(bgp_speakers.values()):
                bgp_speaker.shutdown()
            for vlan in list(valve.dp.vlans.values()):
                if vlan.bgp_as:
                    bgp_speakers[vlan] = self._create_bgp_speaker_for_vlan(vlan)

    def update_metrics(self):
        """Update BGP metrics."""
        for dp_id, bgp_speakers in list(self._dp_bgp_speakers.items()):
            for vlan, bgp_speaker in list(bgp_speakers.items()):
                if bgp_speaker is not None:
                    try:
                        neighbor_states = list(json.loads(
                            bgp_speaker.neighbor_state_get()).items())
                    except CoreNotStarted:
                        continue
                    for neighbor, neighbor_state in neighbor_states:
                        base_labels = self._valves[dp_id].base_prom_labels
                        # pylint: disable=no-member
                        self._metrics.bgp_neighbor_uptime_seconds.labels(
                            **dict(base_labels, vlan=vlan.vid, neighbor=neighbor)).set(
                                neighbor_state['info']['uptime'])
                        for ipv in vlan.ipvs():
                            # pylint: disable=no-member
                            self._metrics.bgp_neighbor_routes.labels(
                                **dict(base_labels, vlan=vlan.vid, neighbor=neighbor, ipv=ipv)).set(
                                    len(vlan.routes_by_ipv(ipv)))
