# Copyright (C) 2015 Nippon Telegraph and Telephone Corporation.
#
# 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.


from lib.base import (
    BGPContainer,
    CmdBuffer,
    try_several_times,
    wait_for_completion,
    yellow,
)


class ExaBGPContainer(BGPContainer):

    SHARED_VOLUME = '/shared_volume'
    PID_FILE = '/var/run/exabgp.pid'

    def __init__(self, name, asn, router_id, ctn_image_name='osrg/exabgp:4.0.5'):
        super(ExaBGPContainer, self).__init__(name, asn, router_id, ctn_image_name)
        self.shared_volumes.append((self.config_dir, self.SHARED_VOLUME))

    def _pre_start_exabgp(self):
        # Create named pipes for "exabgpcli"
        named_pipes = '/run/exabgp.in /run/exabgp.out'
        self.local('mkfifo {0}'.format(named_pipes), capture=True)
        self.local('chmod 777 {0}'.format(named_pipes), capture=True)

    def _start_exabgp(self):
        cmd = CmdBuffer(' ')
        cmd << 'env exabgp.log.destination={0}/exabgpd.log'.format(self.SHARED_VOLUME)
        cmd << 'exabgp.daemon.user=root'
        cmd << 'exabgp.daemon.pid={0}'.format(self.PID_FILE)
        cmd << 'exabgp.tcp.bind="0.0.0.0" exabgp.tcp.port=179'
        cmd << 'exabgp {0}/exabgpd.conf'.format(self.SHARED_VOLUME)
        self.local(str(cmd), detach=True)

    def _wait_for_boot(self):
        def _f():
            ret = self.local('exabgpcli version > /dev/null 2>&1; echo $?', capture=True)
            return ret == '0'

        return wait_for_completion(_f)

    def run(self):
        super(ExaBGPContainer, self).run()
        self._pre_start_exabgp()
        # To start ExaBGP, it is required to configure neighbor settings, so
        # here does not start ExaBGP yet.
        # self._start_exabgp()
        return self.WAIT_FOR_BOOT

    def create_config(self):
        # Manpage of exabgp.conf(5):
        # https://github.com/Exa-Networks/exabgp/blob/master/doc/man/exabgp.conf.5
        cmd = CmdBuffer('\n')
        for peer, info in self.peers.items():
            cmd << 'neighbor {0} {{'.format(info['neigh_addr'].split('/')[0])
            cmd << '    router-id {0};'.format(self.router_id)
            local_addr = info['local_addr'].split('/')[0].split('%')[0]
            cmd << '    local-address {0};'.format(local_addr)
            cmd << '    local-as {0};'.format(self.asn)
            cmd << '    peer-as {0};'.format(peer.asn)

            caps = []
            if info['as2']:
                caps.append('        asn4 disable;')
            if info["addpath"] > 0:
                caps.append('        add-path send/receive;')
            if caps:
                cmd << '    capability {'
                for cap in caps:
                    cmd << cap
                cmd << '    }'

            if info['passwd']:
                cmd << '    md5-password "{0}";'.format(info['passwd'])

            if info['passive']:
                cmd << '    passive;'
            cmd << '}'

        with open('{0}/exabgpd.conf'.format(self.config_dir), 'w') as f:
            print(yellow('[{0}\'s new exabgpd.conf]'.format(self.name)))
            print(yellow(str(cmd)))
            f.write(str(cmd))

    def _is_running(self):
        ret = self.local("test -f {0}; echo $?".format(self.PID_FILE), capture=True)
        return ret == '0'

    def reload_config(self):
        if not self.peers:
            return

        def _reload():
            if self._is_running():
                self.local('/usr/bin/pkill --pidfile {0} && rm -f {0}'.format(self.PID_FILE), capture=True)
            else:
                self._start_exabgp()
                self._wait_for_boot()

            if not self._is_running():
                raise RuntimeError('Could not start ExaBGP')

        try_several_times(_reload)

    def _construct_ip_unicast(self, path):
        cmd = CmdBuffer(' ')
        cmd << str(path['prefix'])
        if path['next-hop']:
            cmd << 'next-hop {0}'.format(path['next-hop'])
        else:
            cmd << 'next-hop self'
        return str(cmd)

    def _construct_flowspec(self, path):
        cmd = CmdBuffer(' ')
        cmd << '{ match {'
        for match in path['matchs']:
            cmd << '{0};'.format(match)
        cmd << '} then {'
        for then in path['thens']:
            cmd << '{0};'.format(then)
        cmd << '} }'
        return str(cmd)

    def _construct_path_attributes(self, path):
        cmd = CmdBuffer(' ')
        if path['as-path']:
            cmd << 'as-path [{0}]'.format(' '.join(str(i) for i in path['as-path']))
        if path['med']:
            cmd << 'med {0}'.format(path['med'])
        if path['local-pref']:
            cmd << 'local-preference {0}'.format(path['local-pref'])
        if path['community']:
            cmd << 'community [{0}]'.format(' '.join(c for c in path['community']))
        if path['extended-community']:
            cmd << 'extended-community [{0}]'.format(path['extended-community'])
        if path['attr']:
            cmd << 'attribute [ {0} ]'.format(path['attr'])
        return str(cmd)

    def _construct_path(self, path, rf='ipv4', is_withdraw=False):
        cmd = CmdBuffer(' ')

        if rf in ['ipv4', 'ipv6']:
            cmd << 'route'
            cmd << self._construct_ip_unicast(path)
        elif rf in ['ipv4-flowspec', 'ipv6-flowspec']:
            cmd << 'flow route'
            cmd << self._construct_flowspec(path)
        else:
            raise ValueError('unsupported address family: %s' % rf)

        if path['identifier']:
            cmd << 'path-information {0}'.format(path['identifier'])

        if not is_withdraw:
            # Withdrawal should not require path attributes
            cmd << self._construct_path_attributes(path)

        return str(cmd)

    def add_route(self, route, rf='ipv4', attribute=None, aspath=None,
                  community=None, med=None, extendedcommunity=None,
                  nexthop=None, matchs=None, thens=None,
                  local_pref=None, identifier=None, reload_config=False):
        if not self._is_running():
            raise RuntimeError('ExaBGP is not yet running')

        self.routes.setdefault(route, [])
        path = {
            'prefix': route,
            'rf': rf,
            'attr': attribute,
            'next-hop': nexthop,
            'as-path': aspath,
            'community': community,
            'med': med,
            'local-pref': local_pref,
            'extended-community': extendedcommunity,
            'identifier': identifier,
            'matchs': matchs,
            'thens': thens,
        }

        cmd = CmdBuffer(' ')
        cmd << "exabgpcli 'announce"
        cmd << self._construct_path(path, rf=rf)
        cmd << "'"
        self.local(str(cmd), capture=True)

        self.routes[route].append(path)

    def del_route(self, route, identifier=None, reload_config=False):
        if not self._is_running():
            raise RuntimeError('ExaBGP is not yet running')

        path = None
        new_paths = []
        for p in self.routes.get(route, []):
            if p['identifier'] != identifier:
                new_paths.append(p)
            else:
                path = p
        if not path:
            return

        rf = path['rf']
        cmd = CmdBuffer(' ')
        cmd << "exabgpcli 'withdraw"
        cmd << self._construct_path(path, rf=rf, is_withdraw=True)
        cmd << "'"
        self.local(str(cmd), capture=True)

        self.routes[route] = new_paths

    def _get_adj_rib(self, peer, rf, in_out='in'):
        # IPv4 Unicast:
        # neighbor 172.17.0.2 ipv4 unicast 192.168.100.0/24 path-information 0.0.0.20 next-hop self
        # IPv6 FlowSpec:
        # neighbor 172.17.0.2 ipv6 flow flow destination-ipv6 2002:1::/64/0 source-ipv6 2002:2::/64/0 next-header =udp flow-label >100
        rf_map = {
            'ipv4': ['ipv4', 'unicast'],
            'ipv6': ['ipv6', 'unicast'],
            'ipv4-flowspec': ['ipv4', 'flow'],
            'ipv6-flowspec': ['ipv6', 'flow'],
        }
        assert rf in rf_map
        assert in_out in ('in', 'out')
        peer_addr = self.peer_name(peer)
        lines = self.local('exabgpcli show adj-rib {0}'.format(in_out), capture=True).split('\n')
        # rib = {
        #     <nlri>: [
        #         {
        #             'nlri': <nlri>,
        #             'next-hop': <next-hop>,
        #             ...
        #         },
        #         ...
        #     ],
        # }
        rib = {}
        for line in lines:
            if not line:
                continue
            values = line.split()
            if peer_addr != values[1]:
                continue
            elif rf is not None and rf_map[rf] != values[2:4]:
                continue
            if rf in ('ipv4', 'ipv6'):
                nlri = values[4]
                rib.setdefault(nlri, [])
                path = {k: v for k, v in zip(*[iter(values[5:])] * 2)}
                path['nlri'] = nlri
                rib[nlri].append(path)
            elif rf in ('ipv4-flowspec', 'ipv6-flowspec'):
                # XXX: Missing path attributes?
                nlri = ' '.join(values[5:])
                rib.setdefault(nlri, [])
                path = {'nlri': nlri}
                rib[nlri].append(path)
        return rib

    def get_adj_rib_in(self, peer, rf='ipv4'):
        return self._get_adj_rib(peer, rf, 'in')

    def get_adj_rib_out(self, peer, rf='ipv4'):
        return self._get_adj_rib(peer, rf, 'out')


class RawExaBGPContainer(ExaBGPContainer):
    def __init__(self, name, config, ctn_image_name='osrg/exabgp',
                 exabgp_path=''):
        asn = None
        router_id = None
        for line in config.split('\n'):
            line = line.strip()
            if line.startswith('local-as'):
                asn = int(line[len('local-as'):].strip('; '))
            if line.startswith('router-id'):
                router_id = line[len('router-id'):].strip('; ')
        if not asn:
            raise Exception('asn not in exabgp config')
        if not router_id:
            raise Exception('router-id not in exabgp config')
        self.config = config

        super(RawExaBGPContainer, self).__init__(name, asn, router_id,
                                                 ctn_image_name, exabgp_path)

    def create_config(self):
        with open('{0}/exabgpd.conf'.format(self.config_dir), 'w') as f:
            print(yellow('[{0}\'s new exabgpd.conf]'.format(self.name)))
            print(yellow(self.config))
            f.write(self.config)
