"""
frr.py: defines routing services provided by FRRouting.
Assumes installation of FRR via https://deb.frrouting.org/
"""
import netaddr

from core import constants
from core.emane.nodes import EmaneNet
from core.nodes.network import PtpNet, WlanNode
from core.nodes.physical import Rj45Node
from core.services.coreservices import CoreService


class FRRZebra(CoreService):
    name = "FRRzebra"
    group = "FRR"
    dirs = ("/usr/local/etc/frr", "/var/run/frr", "/var/log/frr")
    configs = (
        "/usr/local/etc/frr/frr.conf",
        "frrboot.sh",
        "/usr/local/etc/frr/vtysh.conf",
        "/usr/local/etc/frr/daemons",
    )
    startup = ("sh frrboot.sh zebra",)
    shutdown = ("killall zebra",)
    validate = ("pidof zebra",)

    @classmethod
    def generate_config(cls, node, filename):
        """
        Return the frr.conf or frrboot.sh file contents.
        """
        if filename == cls.configs[0]:
            return cls.generateFrrConf(node)
        elif filename == cls.configs[1]:
            return cls.generateFrrBoot(node)
        elif filename == cls.configs[2]:
            return cls.generateVtyshConf(node)
        elif filename == cls.configs[3]:
            return cls.generateFrrDaemons(node)
        else:
            raise ValueError(
                "file name (%s) is not a known configuration: %s", filename, cls.configs
            )

    @classmethod
    def generateVtyshConf(cls, node):
        """
        Returns configuration file text.
        """
        return "service integrated-vtysh-config\n"

    @classmethod
    def generateFrrConf(cls, node):
        """
        Returns configuration file text. Other services that depend on zebra
        will have generatefrrifcconfig() and generatefrrconfig()
        hooks that are invoked here.
        """
        # we could verify here that filename == frr.conf
        cfg = ""
        for ifc in node.netifs():
            cfg += "interface %s\n" % ifc.name
            # include control interfaces in addressing but not routing daemons
            if hasattr(ifc, "control") and ifc.control is True:
                cfg += "  "
                cfg += "\n  ".join(map(cls.addrstr, ifc.addrlist))
                cfg += "\n"
                continue
            cfgv4 = ""
            cfgv6 = ""
            want_ipv4 = False
            want_ipv6 = False
            for s in node.services:
                if cls.name not in s.dependencies:
                    continue
                ifccfg = s.generatefrrifcconfig(node, ifc)
                if s.ipv4_routing:
                    want_ipv4 = True
                if s.ipv6_routing:
                    want_ipv6 = True
                    cfgv6 += ifccfg
                else:
                    cfgv4 += ifccfg

            if want_ipv4:
                ipv4list = filter(
                    lambda x: netaddr.valid_ipv4(x.split("/")[0]), ifc.addrlist
                )
                cfg += "  "
                cfg += "\n  ".join(map(cls.addrstr, ipv4list))
                cfg += "\n"
                cfg += cfgv4
            if want_ipv6:
                ipv6list = filter(
                    lambda x: netaddr.valid_ipv6(x.split("/")[0]), ifc.addrlist
                )
                cfg += "  "
                cfg += "\n  ".join(map(cls.addrstr, ipv6list))
                cfg += "\n"
                cfg += cfgv6
            cfg += "!\n"

        for s in node.services:
            if cls.name not in s.dependencies:
                continue
            cfg += s.generatefrrconfig(node)
        return cfg

    @staticmethod
    def addrstr(x):
        """
        helper for mapping IP addresses to zebra config statements
        """
        addr = x.split("/")[0]
        if netaddr.valid_ipv4(addr):
            return "ip address %s" % x
        elif netaddr.valid_ipv6(addr):
            return "ipv6 address %s" % x
        else:
            raise ValueError("invalid address: %s", x)

    @classmethod
    def generateFrrBoot(cls, node):
        """
        Generate a shell script used to boot the FRR daemons.
        """
        frr_bin_search = node.session.options.get_config(
            "frr_bin_search", default='"/usr/local/bin /usr/bin /usr/lib/frr"'
        )
        frr_sbin_search = node.session.options.get_config(
            "frr_sbin_search", default='"/usr/local/sbin /usr/sbin /usr/lib/frr"'
        )
        cfg = """\
#!/bin/sh
# auto-generated by zebra service (frr.py)
FRR_CONF=%s
FRR_SBIN_SEARCH=%s
FRR_BIN_SEARCH=%s
FRR_STATE_DIR=%s

searchforprog()
{
    prog=$1
    searchpath=$@
    ret=
    for p in $searchpath; do
        if [ -x $p/$prog ]; then
            ret=$p
            break
        fi
    done
    echo $ret
}

confcheck()
{
    CONF_DIR=`dirname $FRR_CONF`
    # if /etc/frr exists, point /etc/frr/frr.conf -> CONF_DIR
    if [ "$CONF_DIR" != "/etc/frr" ] && [ -d /etc/frr ] && [ ! -e /etc/frr/frr.conf ]; then
        ln -s $CONF_DIR/frr.conf /etc/frr/frr.conf
    fi
    # if /etc/frr exists, point /etc/frr/vtysh.conf -> CONF_DIR
    if [ "$CONF_DIR" != "/etc/frr" ] && [ -d /etc/frr ] && [ ! -e /etc/frr/vtysh.conf ]; then
        ln -s $CONF_DIR/vtysh.conf /etc/frr/vtysh.conf
    fi
}

bootdaemon()
{
    FRR_SBIN_DIR=$(searchforprog $1 $FRR_SBIN_SEARCH)
    if [ "z$FRR_SBIN_DIR" = "z" ]; then
        echo "ERROR: FRR's '$1' daemon not found in search path:"
        echo "  $FRR_SBIN_SEARCH"
        return 1
    fi

    flags=""

    if [ "$1" = "pimd" ] && \\
        grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $FRR_CONF; then
        flags="$flags -6"
    fi

    #force FRR to use CORE generated conf file
    flags="$flags -d -f $FRR_CONF"
    $FRR_SBIN_DIR/$1 $flags

    if [ "$?" != "0" ]; then
        echo "ERROR: FRR's '$1' daemon failed to start!:"
        return 1
    fi
}

bootfrr()
{
    FRR_BIN_DIR=$(searchforprog 'vtysh' $FRR_BIN_SEARCH)
    if [ "z$FRR_BIN_DIR" = "z" ]; then
        echo "ERROR: FRR's 'vtysh' program not found in search path:"
        echo "  $FRR_BIN_SEARCH"
        return 1
    fi

    # fix /var/run/frr permissions
    id -u frr 2>/dev/null >/dev/null
    if [ "$?" = "0" ]; then
        chown frr $FRR_STATE_DIR
    fi

    bootdaemon "zebra"
    if grep -q "^ip route " $FRR_CONF; then
        bootdaemon "staticd"
    fi
    for r in rip ripng ospf6 ospf bgp babel; do
        if grep -q "^router \\<${r}\\>" $FRR_CONF; then
            bootdaemon "${r}d"
        fi
    done

    if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $FRR_CONF; then
        bootdaemon "pimd"
    fi

    $FRR_BIN_DIR/vtysh -b
}

if [ "$1" != "zebra" ]; then
    echo "WARNING: '$1': all FRR daemons are launched by the 'zebra' service!"
    exit 1
fi

confcheck
bootfrr
""" % (
            cls.configs[0],
            frr_sbin_search,
            frr_bin_search,
            constants.FRR_STATE_DIR,
        )
        for ifc in node.netifs():
            cfg += f"ip link set dev {ifc.name} down\n"
            cfg += "sleep 1\n"
            cfg += f"ip link set dev {ifc.name} up\n"
        return cfg

    @classmethod
    def generateFrrDaemons(cls, node):
        """
        Returns configuration file text.
        """
        return """\
#
# When activation a daemon at the first time, a config file, even if it is
# empty, has to be present *and* be owned by the user and group "frr", else
# the daemon will not be started by /etc/init.d/frr. The permissions should
# be u=rw,g=r,o=.
# When using "vtysh" such a config file is also needed. It should be owned by
# group "frrvty" and set to ug=rw,o= though. Check /etc/pam.d/frr, too.
#
# The watchfrr and zebra daemons are always started.
#
bgpd=yes
ospfd=yes
ospf6d=yes
ripd=yes
ripngd=yes
isisd=yes
pimd=yes
ldpd=yes
nhrpd=yes
eigrpd=yes
babeld=yes
sharpd=yes
pbrd=yes
bfdd=yes
fabricd=yes

#
# If this option is set the /etc/init.d/frr script automatically loads
# the config via "vtysh -b" when the servers are started.
# Check /etc/pam.d/frr if you intend to use "vtysh"!
#
vtysh_enable=yes
zebra_options="  -A 127.0.0.1 -s 90000000"
bgpd_options="   -A 127.0.0.1"
ospfd_options="  -A 127.0.0.1"
ospf6d_options=" -A ::1"
ripd_options="   -A 127.0.0.1"
ripngd_options=" -A ::1"
isisd_options="  -A 127.0.0.1"
pimd_options="   -A 127.0.0.1"
ldpd_options="   -A 127.0.0.1"
nhrpd_options="  -A 127.0.0.1"
eigrpd_options=" -A 127.0.0.1"
babeld_options=" -A 127.0.0.1"
sharpd_options=" -A 127.0.0.1"
pbrd_options="   -A 127.0.0.1"
staticd_options="-A 127.0.0.1"
bfdd_options="   -A 127.0.0.1"
fabricd_options="-A 127.0.0.1"

# The list of daemons to watch is automatically generated by the init script.
#watchfrr_options=""

# for debugging purposes, you can specify a "wrap" command to start instead
# of starting the daemon directly, e.g. to use valgrind on ospfd:
#   ospfd_wrap="/usr/bin/valgrind"
# or you can use "all_wrap" for all daemons, e.g. to use perf record:
#   all_wrap="/usr/bin/perf record --call-graph -"
# the normal daemon command is added to this at the end.
"""


class FrrService(CoreService):
    """
    Parent class for FRR services. Defines properties and methods
    common to FRR's routing daemons.
    """

    name = None
    group = "FRR"
    dependencies = ("FRRzebra",)
    dirs = ()
    configs = ()
    startup = ()
    shutdown = ()
    meta = "The config file for this service can be found in the Zebra service."

    ipv4_routing = False
    ipv6_routing = False

    @staticmethod
    def routerid(node):
        """
        Helper to return the first IPv4 address of a node as its router ID.
        """
        for ifc in node.netifs():
            if hasattr(ifc, "control") and ifc.control is True:
                continue
            for a in ifc.addrlist:
                a = a.split("/")[0]
                if netaddr.valid_ipv4(a):
                    return a
        # raise ValueError,  "no IPv4 address found for router ID"
        return "0.0.0.0"

    @staticmethod
    def rj45check(ifc):
        """
        Helper to detect whether interface is connected an external RJ45
        link.
        """
        if ifc.net:
            for peerifc in ifc.net.netifs():
                if peerifc == ifc:
                    continue
                if isinstance(peerifc, Rj45Node):
                    return True
        return False

    @classmethod
    def generate_config(cls, node, filename):
        return ""

    @classmethod
    def generatefrrifcconfig(cls, node, ifc):
        return ""

    @classmethod
    def generatefrrconfig(cls, node):
        return ""


class FRROspfv2(FrrService):
    """
    The OSPFv2 service provides IPv4 routing for wired networks. It does
    not build its own configuration file but has hooks for adding to the
    unified frr.conf file.
    """

    name = "FRROSPFv2"
    startup = ()
    shutdown = ("killall ospfd",)
    validate = ("pidof ospfd",)
    ipv4_routing = True

    @staticmethod
    def mtucheck(ifc):
        """
        Helper to detect MTU mismatch and add the appropriate OSPF
        mtu-ignore command. This is needed when e.g. a node is linked via a
        GreTap device.
        """
        if ifc.mtu != 1500:
            # a workaround for PhysicalNode GreTap, which has no knowledge of
            # the other nodes/nets
            return "  ip ospf mtu-ignore\n"
        if not ifc.net:
            return ""
        for i in ifc.net.netifs():
            if i.mtu != ifc.mtu:
                return "  ip ospf mtu-ignore\n"
        return ""

    @staticmethod
    def ptpcheck(ifc):
        """
        Helper to detect whether interface is connected to a notional
        point-to-point link.
        """
        if isinstance(ifc.net, PtpNet):
            return "  ip ospf network point-to-point\n"
        return ""

    @classmethod
    def generatefrrconfig(cls, node):
        cfg = "router ospf\n"
        rtrid = cls.routerid(node)
        cfg += "  router-id %s\n" % rtrid
        # network 10.0.0.0/24 area 0
        for ifc in node.netifs():
            if hasattr(ifc, "control") and ifc.control is True:
                continue
            for a in ifc.addrlist:
                addr = a.split("/")[0]
                if not netaddr.valid_ipv4(addr):
                    continue
                cfg += "  network %s area 0\n" % a
        cfg += "!\n"
        return cfg

    @classmethod
    def generatefrrifcconfig(cls, node, ifc):
        return cls.mtucheck(ifc)


class FRROspfv3(FrrService):
    """
    The OSPFv3 service provides IPv6 routing for wired networks. It does
    not build its own configuration file but has hooks for adding to the
    unified frr.conf file.
    """

    name = "FRROSPFv3"
    startup = ()
    shutdown = ("killall ospf6d",)
    validate = ("pidof ospf6d",)
    ipv4_routing = True
    ipv6_routing = True

    @staticmethod
    def minmtu(ifc):
        """
        Helper to discover the minimum MTU of interfaces linked with the
        given interface.
        """
        mtu = ifc.mtu
        if not ifc.net:
            return mtu
        for i in ifc.net.netifs():
            if i.mtu < mtu:
                mtu = i.mtu
        return mtu

    @classmethod
    def mtucheck(cls, ifc):
        """
        Helper to detect MTU mismatch and add the appropriate OSPFv3
        ifmtu command. This is needed when e.g. a node is linked via a
        GreTap device.
        """
        minmtu = cls.minmtu(ifc)
        if minmtu < ifc.mtu:
            return "  ipv6 ospf6 ifmtu %d\n" % minmtu
        else:
            return ""

    @staticmethod
    def ptpcheck(ifc):
        """
        Helper to detect whether interface is connected to a notional
        point-to-point link.
        """
        if isinstance(ifc.net, PtpNet):
            return "  ipv6 ospf6 network point-to-point\n"
        return ""

    @classmethod
    def generatefrrconfig(cls, node):
        cfg = "router ospf6\n"
        rtrid = cls.routerid(node)
        cfg += "  router-id %s\n" % rtrid
        for ifc in node.netifs():
            if hasattr(ifc, "control") and ifc.control is True:
                continue
            cfg += "  interface %s area 0.0.0.0\n" % ifc.name
        cfg += "!\n"
        return cfg

    @classmethod
    def generatefrrifcconfig(cls, node, ifc):
        return cls.mtucheck(ifc)
        # cfg = cls.mtucheck(ifc)
        # external RJ45 connections will use default OSPF timers
        # if cls.rj45check(ifc):
        #    return cfg
        # cfg += cls.ptpcheck(ifc)

        # return cfg + """\


# ipv6 ospf6 hello-interval 2
#  ipv6 ospf6 dead-interval 6
#  ipv6 ospf6 retransmit-interval 5
# """


class FRRBgp(FrrService):
    """
    The BGP service provides interdomain routing.
    Peers must be manually configured, with a full mesh for those
    having the same AS number.
    """

    name = "FRRBGP"
    startup = ()
    shutdown = ("killall bgpd",)
    validate = ("pidof bgpd",)
    custom_needed = True
    ipv4_routing = True
    ipv6_routing = True

    @classmethod
    def generatefrrconfig(cls, node):
        cfg = "!\n! BGP configuration\n!\n"
        cfg += "! You should configure the AS number below,\n"
        cfg += "! along with this router's peers.\n!\n"
        cfg += "router bgp %s\n" % node.id
        rtrid = cls.routerid(node)
        cfg += "  bgp router-id %s\n" % rtrid
        cfg += "  redistribute connected\n"
        cfg += "! neighbor 1.2.3.4 remote-as 555\n!\n"
        return cfg


class FRRRip(FrrService):
    """
    The RIP service provides IPv4 routing for wired networks.
    """

    name = "FRRRIP"
    startup = ()
    shutdown = ("killall ripd",)
    validate = ("pidof ripd",)
    ipv4_routing = True

    @classmethod
    def generatefrrconfig(cls, node):
        cfg = """\
router rip
  redistribute static
  redistribute connected
  redistribute ospf
  network 0.0.0.0/0
!
"""
        return cfg


class FRRRipng(FrrService):
    """
    The RIP NG service provides IPv6 routing for wired networks.
    """

    name = "FRRRIPNG"
    startup = ()
    shutdown = ("killall ripngd",)
    validate = ("pidof ripngd",)
    ipv6_routing = True

    @classmethod
    def generatefrrconfig(cls, node):
        cfg = """\
router ripng
  redistribute static
  redistribute connected
  redistribute ospf6
  network ::/0
!
"""
        return cfg


class FRRBabel(FrrService):
    """
    The Babel service provides a loop-avoiding distance-vector routing
    protocol for IPv6 and IPv4 with fast convergence properties.
    """

    name = "FRRBabel"
    startup = ()
    shutdown = ("killall babeld",)
    validate = ("pidof babeld",)
    ipv6_routing = True

    @classmethod
    def generatefrrconfig(cls, node):
        cfg = "router babel\n"
        for ifc in node.netifs():
            if hasattr(ifc, "control") and ifc.control is True:
                continue
            cfg += "  network %s\n" % ifc.name
        cfg += "  redistribute static\n  redistribute ipv4 connected\n"
        return cfg

    @classmethod
    def generatefrrifcconfig(cls, node, ifc):
        if ifc.net and isinstance(ifc.net, (EmaneNet, WlanNode)):
            return "  babel wireless\n  no babel split-horizon\n"
        else:
            return "  babel wired\n  babel split-horizon\n"


class FRRpimd(FrrService):
    """
    PIM multicast routing based on XORP.
    """

    name = "FRRpimd"
    startup = ()
    shutdown = ("killall pimd",)
    validate = ("pidof pimd",)
    ipv4_routing = True

    @classmethod
    def generatefrrconfig(cls, node):
        ifname = "eth0"
        for ifc in node.netifs():
            if ifc.name != "lo":
                ifname = ifc.name
                break
        cfg = "router mfea\n!\n"
        cfg += "router igmp\n!\n"
        cfg += "router pim\n"
        cfg += "  !ip pim rp-address 10.0.0.1\n"
        cfg += "  ip pim bsr-candidate %s\n" % ifname
        cfg += "  ip pim rp-candidate %s\n" % ifname
        cfg += "  !ip pim spt-threshold interval 10 bytes 80000\n"
        return cfg

    @classmethod
    def generatefrrifcconfig(cls, node, ifc):
        return "  ip mfea\n  ip igmp\n  ip pim\n"