# SPDX-License-Identifier: ISC

# Copyright 2023 6WIND S.A.
# Authored by Farid Mihoub <farid.mihoub@6wind.com>
#
"""
BMP main module:
    - dissect monitoring messages in the way to get updated/withdrawed prefixes
    - XXX: missing RFCs references
    - XXX: more bmp messages types to dissect
    - XXX: complete bgp message dissection
"""
import ipaddress
import json
import os
import struct
import sys

from datetime import datetime

from bgp.update import BGPUpdate
from bgp.update.rd import RouteDistinguisher


SEQ = 0
LOG_DIR = "/var/log/"
LOG_FILE = "/var/log/bmp.log"

IS_ADJ_RIB_OUT = 1 << 4
IS_AS_PATH = 1 << 5
IS_POST_POLICY = 1 << 6
IS_IPV6 = 1 << 7
IS_FILTERED = 1 << 7

if not os.path.exists(LOG_DIR):
    os.makedirs(LOG_DIR)


def bin2str_ipaddress(ip_bytes, is_ipv6=False):
    if is_ipv6:
        return str(ipaddress.IPv6Address(ip_bytes))
    return str(ipaddress.IPv4Address(ip_bytes[-4:]))


def log2file(logs, log_file):
    """
    XXX: extract the useful information and save it in a flat dictionnary
    """
    with open(log_file, "a") as f:
        f.write(json.dumps(logs) + "\n")


def timestamp_print(message, file=sys.stderr):
    """Helper function to timestamp_print messages with timestamps."""

    current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    print(f"[{current_time}] {message}", file=file)


# ------------------------------------------------------------------------------
class BMPCodes:
    """
    XXX: complete the list, provide RFCs.
    """

    VERSION = 0x3

    BMP_MSG_TYPE_ROUTE_MONITORING = 0x00
    BMP_MSG_TYPE_STATISTICS_REPORT = 0x01
    BMP_MSG_TYPE_PEER_DOWN_NOTIFICATION = 0x02
    BMP_MSG_TYPE_PEER_UP_NOTIFICATION = 0x03
    BMP_MSG_TYPE_INITIATION = 0x04
    BMP_MSG_TYPE_TERMINATION = 0x05
    BMP_MSG_TYPE_ROUTE_MIRRORING = 0x06
    BMP_MSG_TYPE_ROUTE_POLICY = 0x64

    # initiation message types
    BMP_INIT_INFO_STRING = 0x00
    BMP_INIT_SYSTEM_DESCRIPTION = 0x01
    BMP_INIT_SYSTEM_NAME = 0x02
    BMP_INIT_VRF_TABLE_NAME = 0x03
    BMP_INIT_ADMIN_LABEL = 0x04

    # peer types
    BMP_PEER_GLOBAL_INSTANCE = 0x00
    BMP_PEER_RD_INSTANCE = 0x01
    BMP_PEER_LOCAL_INSTANCE = 0x02
    BMP_PEER_LOC_RIB_INSTANCE = 0x03

    # peer header flags
    BMP_PEER_FLAG_IPV6 = 0x80
    BMP_PEER_FLAG_POST_POLICY = 0x40
    BMP_PEER_FLAG_AS_PATH = 0x20
    BMP_PEER_FLAG_ADJ_RIB_OUT = 0x10

    # peer loc-rib flag
    BMP_PEER_FLAG_LOC_RIB = 0x80
    BMP_PEER_FLAG_LOC_RIB_RES = 0x7F

    # statistics type
    BMP_STAT_PREFIX_REJ = 0x00
    BMP_STAT_PREFIX_DUP = 0x01
    BMP_STAT_WITHDRAW_DUP = 0x02
    BMP_STAT_CLUSTER_LOOP = 0x03
    BMP_STAT_AS_LOOP = 0x04
    BMP_STAT_INV_ORIGINATOR = 0x05
    BMP_STAT_AS_CONFED_LOOP = 0x06
    BMP_STAT_ROUTES_ADJ_RIB_IN = 0x07
    BMP_STAT_ROUTES_LOC_RIB = 0x08
    BMP_STAT_ROUTES_PER_ADJ_RIB_IN = 0x09
    BMP_STAT_ROUTES_PER_LOC_RIB = 0x0A
    BMP_STAT_UPDATE_TREAT = 0x0B
    BMP_STAT_PREFIXES_TREAT = 0x0C
    BMP_STAT_DUPLICATE_UPDATE = 0x0D
    BMP_STAT_ROUTES_PRE_ADJ_RIB_OUT = 0x0E
    BMP_STAT_ROUTES_POST_ADJ_RIB_OUT = 0x0F
    BMP_STAT_ROUTES_PRE_PER_ADJ_RIB_OUT = 0x10
    BMP_STAT_ROUTES_POST_PER_ADJ_RIB_OUT = 0x11

    # peer down reason code
    BMP_PEER_DOWN_LOCAL_NOTIFY = 0x01
    BMP_PEER_DOWN_LOCAL_NO_NOTIFY = 0x02
    BMP_PEER_DOWN_REMOTE_NOTIFY = 0x03
    BMP_PEER_DOWN_REMOTE_NO_NOTIFY = 0x04
    BMP_PEER_DOWN_INFO_NO_LONGER = 0x05
    BMP_PEER_DOWN_SYSTEM_CLOSED = 0x06

    # termincation message types
    BMP_TERM_TYPE_STRING = 0x00
    BMP_TERM_TYPE_REASON = 0x01

    # termination reason code
    BMP_TERM_REASON_ADMIN_CLOSE = 0x00
    BMP_TERM_REASON_UNSPECIFIED = 0x01
    BMP_TERM_REASON_RESOURCES = 0x02
    BMP_TERM_REASON_REDUNDANT = 0x03
    BMP_TERM_REASON_PERM_CLOSE = 0x04

    # policy route tlv
    BMP_ROUTE_POLICY_TLV_VRF = 0x00
    BMP_ROUTE_POLICY_TLV_POLICY = 0x01
    BMP_ROUTE_POLICY_TLV_PRE_POLICY = 0x02
    BMP_ROUTE_POLICY_TLV_POST_POLICY = 0x03
    BMP_ROUTE_POLICY_TLV_STRING = 0x04


# ------------------------------------------------------------------------------
class BMPMsg:
    """
    XXX: should we move register_msg_type and look_msg_type
    to generic Type class.
    """

    TYPES = {}
    UNKNOWN_TYPE = None
    HDR_STR = "!BIB"
    MIN_LEN = struct.calcsize(HDR_STR)
    TYPES_STR = {
        BMPCodes.BMP_MSG_TYPE_INITIATION: "initiation",
        BMPCodes.BMP_MSG_TYPE_PEER_DOWN_NOTIFICATION: "peer down notification",
        BMPCodes.BMP_MSG_TYPE_PEER_UP_NOTIFICATION: "peer up notification",
        BMPCodes.BMP_MSG_TYPE_ROUTE_MONITORING: "route monitoring",
        BMPCodes.BMP_MSG_TYPE_STATISTICS_REPORT: "statistics report",
        BMPCodes.BMP_MSG_TYPE_TERMINATION: "termination",
        BMPCodes.BMP_MSG_TYPE_ROUTE_MIRRORING: "route mirroring",
        BMPCodes.BMP_MSG_TYPE_ROUTE_POLICY: "route policy",
    }

    @classmethod
    def register_msg_type(cls, msgtype):
        def _register_type(subcls):
            cls.TYPES[msgtype] = subcls
            return subcls

        return _register_type

    @classmethod
    def lookup_msg_type(cls, msgtype):
        return cls.TYPES.get(msgtype, cls.UNKNOWN_TYPE)

    @classmethod
    def dissect_header(cls, data):
        """
        0 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |    Version    |
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |                        Message Length                         |
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        | Message Type  |
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        """
        if len(data) < cls.MIN_LEN:
            pass
        else:
            _version, _len, _type = struct.unpack(cls.HDR_STR, data[0 : cls.MIN_LEN])
            return _version, _len, _type

    @classmethod
    def dissect(cls, data, log_file=None):
        global SEQ
        version, msglen, msgtype = cls.dissect_header(data)

        msg_data = data[cls.MIN_LEN : msglen]
        data = data[msglen:]

        if version != BMPCodes.VERSION:
            timestamp_print(
                f"Expected BMP version {BMPCodes.VERSION} but got version {version}."
            )
            return data

        msg_cls = cls.lookup_msg_type(msgtype)
        if msg_cls == cls.UNKNOWN_TYPE:
            timestamp_print(f"Got unknown message type ")
            return data

        timestamp_print(f"Got message type: {msg_cls}")

        msg_cls.MSG_LEN = msglen - cls.MIN_LEN
        logs = msg_cls.dissect(msg_data)
        logs["seq"] = SEQ
        log2file(logs, log_file if log_file else LOG_FILE)
        SEQ += 1

        return data


# ------------------------------------------------------------------------------
class BMPPerPeerMessage:
    """
    0 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |   Peer Type   | Peer Flags    |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                                                               |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                Peer Address (16 bytes)                        |
    ~                                                               ~
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                          Peer AS                              |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                         Peer BGP ID                           |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                      Timestamp (seconds)                      |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                    Timestamp (microseconds)                   |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    """

    PEER_UNPACK_STR = "!BB8s16sI4sII"
    PEER_TYPE_STR = {
        BMPCodes.BMP_PEER_GLOBAL_INSTANCE: "global instance",
        BMPCodes.BMP_PEER_RD_INSTANCE: "route distinguisher instance",
        BMPCodes.BMP_PEER_LOCAL_INSTANCE: "local instance",
        BMPCodes.BMP_PEER_LOC_RIB_INSTANCE: "loc-rib instance",
    }

    @classmethod
    def dissect(cls, data):
        (
            peer_type,
            peer_flags,
            peer_distinguisher,
            peer_address,
            peer_asn,
            peer_bgp_id,
            timestamp_secs,
            timestamp_microsecs,
        ) = struct.unpack_from(cls.PEER_UNPACK_STR, data)

        msg = {"peer_type": cls.PEER_TYPE_STR[peer_type]}

        if peer_type == 0x03:
            msg["is_filtered"] = bool(peer_flags & IS_FILTERED)
            msg["policy"] = "loc-rib"
        else:
            # peer_flags = 0x0000 0000
            # ipv6, post-policy, as-path, adj-rib-out, reserverdx4
            is_adj_rib_out = bool(peer_flags & IS_ADJ_RIB_OUT)
            is_as_path = bool(peer_flags & IS_AS_PATH)
            is_post_policy = bool(peer_flags & IS_POST_POLICY)
            is_ipv6 = bool(peer_flags & IS_IPV6)
            msg["policy"] = "post-policy" if is_post_policy else "pre-policy"
            msg["ipv6"] = is_ipv6
            msg["peer_ip"] = bin2str_ipaddress(peer_address, is_ipv6)

        peer_bgp_id = bin2str_ipaddress(peer_bgp_id)
        timestamp = float(timestamp_secs) + timestamp_microsecs * (10**-6)

        data = data[struct.calcsize(cls.PEER_UNPACK_STR) :]
        msg.update(
            {
                "peer_distinguisher": str(RouteDistinguisher(peer_distinguisher)),
                "peer_asn": peer_asn,
                "peer_bgp_id": peer_bgp_id,
                "timestamp": str(datetime.fromtimestamp(timestamp)),
            }
        )

        return data, msg


# ------------------------------------------------------------------------------
@BMPMsg.register_msg_type(BMPCodes.BMP_MSG_TYPE_ROUTE_MONITORING)
class BMPRouteMonitoring(BMPPerPeerMessage):
    @classmethod
    def dissect(cls, data):
        data, peer_msg = super().dissect(data)
        data, update_msg = BGPUpdate.dissect(data)
        return {**peer_msg, **update_msg}


# ------------------------------------------------------------------------------
class BMPStatisticsReport:
    """
    0 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                        Stats Count                            |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |         Stat Type             |          Stat Len             |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                        Stat Data                              |
    ~                                                               ~
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    """

    pass


# ------------------------------------------------------------------------------
@BMPMsg.register_msg_type(BMPCodes.BMP_MSG_TYPE_PEER_DOWN_NOTIFICATION)
class BMPPeerDownNotification(BMPPerPeerMessage):
    """
    0 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |    Reason     |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |            Data (present if Reason = 1, 2 or 3)               |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    """

    @classmethod
    def dissect(cls, data):
        data, peer_msg = super().dissect(data)

        msg = {
            **peer_msg,
            **{
                "bmp_log_type": "peer down",
            },
        }

        # XXX: dissect the bgp open message

        return msg


# ------------------------------------------------------------------------------
@BMPMsg.register_msg_type(BMPCodes.BMP_MSG_TYPE_PEER_UP_NOTIFICATION)
class BMPPeerUpNotification(BMPPerPeerMessage):
    """
    0 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                 Local Address (16 bytes)                      |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |          Local Port           |           Remote Port         |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                    Sent OPEN Message                         #|
    ~                                                               ~
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                  Received OPEN Message                        |
    ~                                                               ~
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    """

    UNPACK_STR = "!16sHH"
    MIN_LEN = struct.calcsize(UNPACK_STR)
    MSG_LEN = None

    @classmethod
    def dissect(cls, data):
        data, peer_msg = super().dissect(data)

        (local_addr, local_port, remote_port) = struct.unpack_from(cls.UNPACK_STR, data)

        msg = {
            **peer_msg,
            **{
                "bmp_log_type": "peer up",
                "local_ip": bin2str_ipaddress(local_addr, peer_msg.get("ipv6")),
                "local_port": int(local_port),
                "remote_port": int(remote_port),
            },
        }

        # XXX: dissect the bgp open message

        return msg


# ------------------------------------------------------------------------------
@BMPMsg.register_msg_type(BMPCodes.BMP_MSG_TYPE_INITIATION)
class BMPInitiation:
    """
    0 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |      Information Type         |        Information Length     |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                   Information (variable)                      |
    ~                                                               ~
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    """

    TLV_STR = "!HH"
    MIN_LEN = struct.calcsize(TLV_STR)
    FIELD_TO_STR = {
        BMPCodes.BMP_INIT_INFO_STRING: "information",
        BMPCodes.BMP_INIT_ADMIN_LABEL: "admin_label",
        BMPCodes.BMP_INIT_SYSTEM_DESCRIPTION: "system_description",
        BMPCodes.BMP_INIT_SYSTEM_NAME: "system_name",
        BMPCodes.BMP_INIT_VRF_TABLE_NAME: "vrf_table_name",
    }

    @classmethod
    def dissect(cls, data):
        msg = {}
        while len(data) > cls.MIN_LEN:
            _type, _len = struct.unpack_from(cls.TLV_STR, data[0 : cls.MIN_LEN])
            _value = data[cls.MIN_LEN : cls.MIN_LEN + _len].decode()

            msg[cls.FIELD_TO_STR[_type]] = _value
            data = data[cls.MIN_LEN + _len :]

        return msg


# ------------------------------------------------------------------------------
class BMPTermination:
    """
    0 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |          Information Type     |       Information Length      |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                 Information (variable)                        |
    ~                                                               ~
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    """

    pass


# ------------------------------------------------------------------------------
class BMPRouteMirroring:
    pass


# ------------------------------------------------------------------------------
class BMPRoutePolicy:
    pass
