
"""operational.py

Created by Thomas Mangin on 2013-09-01.
Copyright (c) 2009-2017 Exa Networks. All rights reserved.
License: 3-clause BSD. (See the COPYRIGHT file)
"""

from __future__ import annotations

import sys
from struct import pack
from struct import unpack

from exabgp.protocol.family import AFI
from exabgp.protocol.family import SAFI
from exabgp.bgp.message.open.routerid import RouterID
from exabgp.bgp.message.message import Message

# ========================================================================= Type
#

MAX_ADVISORY = 2048  # 2K


class Type(int):
    def pack(self):
        return pack('!H', self)

    def extract(self):
        return [pack('!H', self)]

    def __len__(self):
        return 2

    def __str__(self):
        pass


# ================================================================== Operational
#


@Message.register
class Operational(Message):
    ID = Message.CODE.OPERATIONAL
    TYPE = bytes([Message.CODE.OPERATIONAL])

    registered_operational = dict()

    has_family = False
    has_routerid = False
    is_fault = False

    # really this should be called ID if not for the naming conflict
    class CODE:
        NOP = 0x00  # Not defined by the RFC
        # ADVISE
        ADM = 0x01  # 01: Advisory Demand Message
        ASM = 0x02  # 02: Advisory Static Message
        # STATE
        RPCQ = 0x03  # 03: Reachable Prefix Count Request
        RPCP = 0x04  # 04: Reachable Prefix Count Reply
        APCQ = 0x05  # 05: Adj-Rib-Out Prefix Count Request
        APCP = 0x06  # 06: Adj-Rib-Out Prefix Count Reply
        LPCQ = 0x07  # 07: BGP Loc-Rib Prefix Count Request
        LPCP = 0x08  # 08: BGP Loc-Rib Prefix Count Reply
        SSQ = 0x09  # 09: Simple State Request
        # DUMP
        DUP = 0x0A  # 10: Dropped Update Prefixes
        MUP = 0x0B  # 11: Malformed Update Prefixes
        MUD = 0x0C  # 12: Malformed Update Dump
        SSP = 0x0D  # 13: Simple State Response
        # CONTROL
        MP = 0xFFFE  # 65534: Max Permitted
        NS = 0xFFFF  # 65535: Not Satisfied

    # XXX: FIXME: should be upper case
    name = ''
    category = ''
    code = CODE.NOP

    def __init__(self, what):
        Message.__init__(self)
        self.what = Type(what)

    def _message(self, data):
        return Message._message(self, self.what.pack() + pack('!H', len(data)) + data)

    def __str__(self):
        return self.extensive()

    def extensive(self):
        return f'operational {self.name}'

    @staticmethod
    def register(klass):
        Operational.registered_operational[klass.code] = (klass.category, klass)
        return klass

    @classmethod
    def unpack_message(cls, data, direction, negotiated):  # pylint: disable=W0613
        what = Type(unpack('!H', data[0:2])[0])
        length = unpack('!H', data[2:4])[0]

        decode, klass = cls.registered_operational.get(what, ('unknown', None))

        if decode == 'advisory':
            afi = unpack('!H', data[4:6])[0]
            safi = data[6]
            data = data[7 : length + 4]
            return klass(afi, safi, data)
        if decode == 'query':
            afi = unpack('!H', data[4:6])[0]
            safi = data[6]
            routerid = RouterID.unpack(data[7:11])
            sequence = unpack('!L', data[11:15])[0]
            return klass(afi, safi, routerid, sequence)
        if decode == 'counter':
            afi = unpack('!H', data[4:6])[0]
            safi = data[6]
            routerid = RouterID.unpack(data[7:11])
            sequence = unpack('!L', data[11:15])[0]
            counter = unpack('!L', data[15:19])[0]
            return klass(afi, safi, routerid, sequence, counter)
        sys.stdout.write('ignoring ATM this kind of message\n')


# ============================================================ OperationalFamily
#


class OperationalFamily(Operational):
    has_family = True

    def __init__(self, what, afi, safi, data=b''):
        Operational.__init__(self, what)
        self.afi = AFI.create(afi)
        self.safi = SAFI.create(safi)
        self.data = data

    def family(self):
        return (self.afi, self.safi)

    def _message(self, data):
        return Operational._message(self, self.afi.pack() + self.safi.pack() + data)

    def message(self, negotiated):
        return self._message(self.data)


# =================================================== SequencedOperationalFamily
#


class SequencedOperationalFamily(OperationalFamily):
    __sequence_number = {}
    has_routerid = True

    def __init__(self, what, afi, safi, routerid, sequence, data=b''):
        OperationalFamily.__init__(self, what, afi, safi, data)
        self.routerid = routerid if routerid else None
        self.sequence = sequence if sequence else None
        self._sequence = self.sequence
        self._routerid = self.routerid

    def message(self, negotiated):
        self.sent_routerid = self.routerid if self.routerid else negotiated.sent_open.router_id
        if self.sequence is None:
            self.sent_sequence = (self.__sequence_number.setdefault(self.routerid, 0) + 1) % 0xFFFFFFFF
            self.__sequence_number[self.sent_routerid] = self.sent_sequence
        else:
            self.sent_sequence = self.sequence

        return self._message(self.sent_routerid.pack() + pack('!L', self.sent_sequence) + self.data)


# =========================================================================== NS
#


class NS:
    MALFORMED = 0x01  # Request TLV Malformed
    UNSUPPORTED = 0x02  # TLV Unsupported for this neighbor
    MAXIMUM = 0x03  # Max query frequency exceeded
    PROHIBITED = 0x04  # Administratively prohibited
    BUSY = 0x05  # Busy
    NOTFOUND = 0x06  # Not Found

    class _NS(OperationalFamily):
        is_fault = True

        def __init__(self, afi, safi, sequence):
            OperationalFamily.__init__(self, Operational.CODE.NS, afi, safi, sequence + self.ERROR_SUBCODE)

        def extensive(self):
            return f'operational NS {self.name} {self.afi}/{self.safi}'

    class Malformed(_NS):
        name = 'NS malformed'
        ERROR_SUBCODE = b'\x00\x01'  # pack('!H',MALFORMED)

    class Unsupported(_NS):
        name = 'NS unsupported'
        ERROR_SUBCODE = b'\x00\x02'  # pack('!H',UNSUPPORTED)

    class Maximum(_NS):
        name = 'NS maximum'
        ERROR_SUBCODE = b'\x00\x03'  # pack('!H',MAXIMUM)

    class Prohibited(_NS):
        name = 'NS prohibited'
        ERROR_SUBCODE = b'\x00\x04'  # pack('!H',PROHIBITED)

    class Busy(_NS):
        name = 'NS busy'
        ERROR_SUBCODE = b'\x00\x05'  # pack('!H',BUSY)

    class NotFound(_NS):
        name = 'NS notfound'
        ERROR_SUBCODE = b'\x00\x06'  # pack('!H',NOTFOUND)


# ===================================================================== Advisory
#


class Advisory:
    class _Advisory(OperationalFamily):
        category = 'advisory'

        def extensive(self):
            return f'operational {self.name} afi {self.afi} safi {self.safi} "{self.data}"'

    @Operational.register
    class ADM(_Advisory):
        name = 'ADM'
        code = Operational.CODE.ADM

        def __init__(self, afi, safi, advisory, routerid=None):
            # Handle both string and bytes input
            if isinstance(advisory, bytes):
                utf8 = advisory
            else:
                utf8 = advisory.encode('utf-8')
            if len(utf8) > MAX_ADVISORY:
                utf8 = utf8[: MAX_ADVISORY - 3] + b'...'
            Advisory._Advisory.__init__(self, Operational.CODE.ADM, afi, safi, utf8)

    @Operational.register
    class ASM(_Advisory):
        name = 'ASM'
        code = Operational.CODE.ASM

        def __init__(self, afi, safi, advisory, routerid=None):
            # Handle both string and bytes input
            if isinstance(advisory, bytes):
                utf8 = advisory
            else:
                utf8 = advisory.encode('utf-8')
            if len(utf8) > MAX_ADVISORY:
                utf8 = utf8[: MAX_ADVISORY - 3] + b'...'
            Advisory._Advisory.__init__(self, Operational.CODE.ASM, afi, safi, utf8)


# ======================================================================== Query
#


class Query:
    class _Query(SequencedOperationalFamily):
        category = 'query'

        def __init__(self, afi, safi, routerid, sequence):
            SequencedOperationalFamily.__init__(self, self.code, afi, safi, routerid, sequence)

        def extensive(self):
            if self._routerid and self._sequence:
                return f'operational {self.name} afi {self.afi} safi {self.safi} router-id {self._routerid} sequence {self._sequence}'
            return f'operational {self.name} afi {self.afi} safi {self.safi}'

    @Operational.register
    class RPCQ(_Query):
        name = 'RPCQ'
        code = Operational.CODE.RPCQ

    @Operational.register
    class APCQ(_Query):
        name = 'APCQ'
        code = Operational.CODE.APCQ

    @Operational.register
    class LPCQ(_Query):
        name = 'LPCQ'
        code = Operational.CODE.LPCQ


# ===================================================================== Response
#


class Response:
    class _Counter(SequencedOperationalFamily):
        category = 'counter'

        def __init__(self, afi, safi, routerid, sequence, counter):
            self.counter = counter
            SequencedOperationalFamily.__init__(self, self.code, afi, safi, routerid, sequence, pack('!L', counter))

        def extensive(self):
            if self._routerid and self._sequence:
                return f'operational {self.name} afi {self.afi} safi {self.safi} router-id {self._routerid} sequence {self._sequence} counter {self.counter}'
            return f'operational {self.name} afi {self.afi} safi {self.safi} counter {self.counter}'

    @Operational.register
    class RPCP(_Counter):
        name = 'RPCP'
        code = Operational.CODE.RPCP

    @Operational.register
    class APCP(_Counter):
        name = 'APCP'
        code = Operational.CODE.APCP

    @Operational.register
    class LPCP(_Counter):
        name = 'LPCP'
        code = Operational.CODE.LPCP


# ========================================================================= Dump
#


class Dump:
    pass
