"""
PDU Implementation classes.
"""
import struct
from enum import Enum, unique

from . import util, constants
from .constants import PduTypes
from .encodings import ObjectIdentifier, SearchRange, OctetString, ValueRepresentation
from .pdu import PDU, ContextOptionalPDU


class OpenPDU(PDU):
    """
    https://tools.ietf.org/html/rfc2741#section-6.2.1

    An agentx-Open-PDU is generated by a subagent to request
    establishment of an AgentX session with the master agent.
    """
    header_type_ = PduTypes.OPEN

    def __init__(self, header=None, payload=None, timeout=None, oid=None, descr=None):
        super().__init__(header=header, payload=payload)

        # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        # |  o.timeout    |                     <reserved>                |
        # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        self.timeout = timeout
        self.reserved = util.pad4bytes(1)
        self.oid = oid

        if payload is not None:
            self.timeout, self.reserved = struct.unpack(self.header.endianness + 'B3s', self._trailing_bytes[:4])
            self._trailing_bytes = self._trailing_bytes[4:]
            self.oid = ObjectIdentifier.from_bytes(self._trailing_bytes, self.header.endianness)
            self._trailing_bytes = self._trailing_bytes[self.oid.size:]
            self.descr = OctetString.from_bytes(self._trailing_bytes, self.header.endianness)
            self._trailing_bytes = self._trailing_bytes[self.descr.size:]
        else:
            self.descr = OctetString.from_string(descr)
            self.header = self.header._replace(payload_length=self.payload_length)

            # end of object stream

    def encode(self):
        ret = super().encode()
        fmt = self.header.endianness + 'B3s'
        ret += struct.pack(fmt, self.timeout, self.reserved)
        ret += self.oid.to_bytes(self.header.endianness)
        ret += self.descr.to_bytes(self.header.endianness)
        return ret


class ClosePDU(PDU):
    """
    https://tools.ietf.org/html/rfc2741#section-6.2.2

    An agentx-Close-PDU issued by either a subagent or the master agent
    terminates an AgentX session.
    """
    header_type_ = PduTypes.CLOSE

    @unique
    class Reason(int, Enum):
        OTHER = 1
        """
        None of the following reasons
        """
        PARSE_ERROR = 2
        """
        Too many AgentX parse errors from peer
        """
        PROTOCOL_ERROR = 3
        """
        Too many AgentX protocol errors from peer
        """
        TIMEOUTS = 4
        """
        Too many timeouts waiting for peer
        """
        SHUTDOWN = 5
        """
        Sending entity is shutting down
        """
        BY_MANAGER = 6
        """
        Due to Set operation; this reason code can be used only
        by the master agent, in response to an SNMP management
        request.
        """

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        # | c.reason |                  < reserved >                      |
        # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        self.reason, self.reason_reserved = struct.unpack(self.header.endianness + 'B3s', self._trailing_bytes[:4])
        self.header = self.header._replace(payload_length=4)
        # end of object stream


class RegisterPDU(ContextOptionalPDU):
    """
    https://tools.ietf.org/html/rfc2741#section-6.2.3

    An agentx-Register-PDU is generated by a subagent for each region of
    the MIB variable naming tree (within one or more contexts) that it
    wishes to support.
    """
    header_type_ = PduTypes.REGISTER

    def __init__(self, header=None, context=None, payload=None, timeout=None, priority=None, range_subid=None,
                 subtree=None, upper_bound=None):
        super().__init__(header=header, context=context, payload=payload)

        # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        # | r.timeout | r.priority | r.range_subid |     < reserved >     |
        # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        if payload is not None:
            self.timeout, self.priority, self.range_subid, self.range_subid_reserved = \
                struct.unpack(self.header.endianness + 'BBBB', self._trailing_bytes[:4])
            self._trailing_bytes = self._trailing_bytes[4:]
            self.subtree = ObjectIdentifier.from_bytes(self._trailing_bytes, self.header.endianness)
            self._trailing_bytes = self._trailing_bytes[self.subtree.size:]
            # From the RFC:
            # This field is present only if r.range_subid is not 0.
            # r.upper_bound
            self.upper_bound = None
            if self.range_subid:
                self.upper_bound = struct.unpack(self.header.endianness + 'L', self._trailing_bytes[:4])[0]
                self._trailing_bytes = self._trailing_bytes[4:]
                # end of stream
        else:
            self.timeout, self.priority, self.range_subid, self.range_subid_reserved = \
                timeout, priority, range_subid, 0
            self.subtree, self.upper_bound = subtree, upper_bound
            self.header = self.header._replace(payload_length=self.payload_length)

    def encode(self):
        ret = super().encode()
        ret += struct.pack(self.header.endianness + 'BBBB', self.timeout, self.priority, self.range_subid,
                           self.range_subid_reserved)
        ret += self.subtree.to_bytes(self.header.endianness)
        if self.upper_bound is not None:
            ret += struct.pack(self.header.endianness + 'L', self.upper_bound)
        return ret


# class UnRegisterPDU(OptionalContextPDU):
#    """
#    https://tools.ietf.org/html/rfc2741#section-6.2.4
#    """
#    header_type_ = PduTypes.UNREGISTER
# TODO: Implement


class GetPDU(ContextOptionalPDU):
    """
    https://tools.ietf.org/html/rfc2741#section-6.2.5
    """
    header_type_ = PduTypes.GET

    def __init__(self, header=None, payload=None, context=None, oids=None):
        super().__init__(header=header, payload=payload, context=context)
        self.sr = []

        if payload is not None:
            # consume the remaining bytestream
            bytes_read = 0
            while self._trailing_bytes and bytes_read < self.header.payload_length:
                # unpack the OID
                search_oid = SearchRange.from_bytes(self._trailing_bytes, self.header.endianness)
                # move the pointer
                self._trailing_bytes = self._trailing_bytes[search_oid.size:]
                bytes_read += search_oid.size
                # remember the OID
                self.sr.append(search_oid)
                # end of stream post-loop
        else:
            for oid in oids:
                self.sr.append(
                    SearchRange(start=oid, end=ObjectIdentifier.null_oid())
                )
            self.header = self.header._replace(payload_length=len(self.encode()))

    def encode(self):
        ret = super().encode()
        for sr in self.sr:
            ret += sr.to_bytes(self.header.endianness)
        return ret

    def make_response(self, lut):
        """
        From https://tools.ietf.org/html/rfc2741#section-7.2.3.1:

           Upon the subagent's receipt of an agentx-Get-PDU, each SearchRange in
           the request is processed as follows:

           (1)  The starting OID is copied to v.name.

           (2)  If the starting OID exactly matches the name of a variable
                instantiated by this subagent within the indicated context and
                session, v.type and v.data are encoded to represent the
                variable's syntax and value, as described in section 5.4, "Value
                Representation".

           (3)  Otherwise, if the starting OID does not match the object
                identifier prefix of any variable instantiated within the
                indicated context and session, the VarBind is set to
                `noSuchObject', in the manner described in section 5.4, "Value
                Representation".

           (4)  Otherwise, the VarBind is set to `noSuchInstance' in the manner
                described in section 5.4, "Value Representation".

        :param session_id:
        :param lut:
        :return:
        """

        var_bind_list = []

        for sr in self.sr:
            vr = lut.get(sr)
            var_bind_list.append(vr)

        response_pdu = ResponsePDU(
            header=self.header._replace(
                type_=constants.PduTypes.RESPONSE,
            ),
            sys_up_time=0,  # ignored for this PDU type.
            error=ResponsePDU.Errors.NO_AGENT_X_ERROR,
            index=0,
            values=var_bind_list
        )
        # TODO: 'generr' on other failure
        return response_pdu


class GetNextPDU(GetPDU):
    """
    https://tools.ietf.org/html/rfc2741#section-6.2.6
    """
    # TODO: 'generr' on other failure
    header_type_ = PduTypes.GET_NEXT

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # Functionally equivalent processing to GetPDU -- end OIDs are populated instead of 0-0-0-0.

    def make_response(self, lut):
        """
        Upon the subagent's receipt of an agentx-GetNext-PDU, each
        SearchRange in the request is processed as follows:

        (1)  The subagent searches for a variable within the
            lexicographically ordered list of variable names for all
            variables it instantiates (without regard to registration of
            regions) within the indicated context and session, as follows:

            -  if the "include" field of the starting OID is 0, the
               variable's name is the closest lexicographical successor to
               the starting OID.

            -  if the "include" field of the starting OID is 1, the
               variable's name is either equal to, or the closest
               lexicographical successor to, the starting OID.

            -  If the ending OID is not null, the variable's name
               lexicographically precedes the ending OID.

            If a variable is successfully located, v.name is set to that
            variable's name.  v.type and v.data are encoded to represent the
            variable's syntax and value, as described in section 5.4, "Value
            Representation".

        (2)  If the subagent cannot locate an appropriate variable, v.name is
            set to the starting OID, and the VarBind is set to `
            endOfMibView', in the manner described in section 5.4, "Value
            Representation".

        :param lut:
        :return:
        """

        var_bind_list = []

        for sr in self.sr:
            vr = lut.get_next(sr)
            var_bind_list.append(vr)

        response_pdu = ResponsePDU(
            header=self.header._replace(
                type_=constants.PduTypes.RESPONSE,
            ),
            sys_up_time=0,  # ignored for this PDU type.
            error=ResponsePDU.Errors.NO_AGENT_X_ERROR,
            index=0,
            values=var_bind_list
        )
        # TODO: 'generr' on other failure
        return response_pdu


# class GetBulkPDU(ContextOptionalPDU):
#     """
#     https://tools.ietf.org/html/rfc2741#section-6.2.7
#     """
# TODO: 'generr' on other failure
#     header_type_ = PduTypes.GET_BULK
# TODO: Implement.



class TestSetPDU(ContextOptionalPDU):
    """
    https://tools.ietf.org/html/rfc2741#section-6.2.8
    """
    header_type_ = PduTypes.TEST_SET

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.vbs = []

        while self._trailing_bytes:
            # unpack the VarBind
            vb = ValueRepresentation.from_bytes(self._trailing_bytes, self.header.endianness)
            # move up the pointer
            self._trailing_bytes = self._trailing_bytes[vb.size:]


class CommitSetPDU(PDU):
    """
    https://tools.ietf.org/html/rfc2741#section-6.2.9
    """
    header_type_ = PduTypes.COMMIT_SET

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # header only.


class UndoSetPDU(PDU):
    """
    https://tools.ietf.org/html/rfc2741#section-6.2.9
    """
    header_type_ = PduTypes.UNDO_SET

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # header only.


class CleanupSetPDU(PDU):
    """
    https://tools.ietf.org/html/rfc2741#section-6.2.9
    """
    header_type_ = PduTypes.CLEANUP_SET

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # header only.


# class NotifyPDU(ContextOptionalPDU):
#     """
#         https://tools.ietf.org/html/rfc2741#section-6.2.10
#         """
#     header_type_ = PduTypes.NOTIFY
# TODO: Impl




class PingPDU(ContextOptionalPDU):
    """
    https://tools.ietf.org/html/rfc2741#section-6.2.11
    """
    header_type_ = PduTypes.PING

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # header & context only.


# class IndexAllocatePDU(ContextOptionalPDU):
#     """
#     https://tools.ietf.org/html/rfc2741#section-6.2.12
#     """
#     header_type_ = PduTypes.INDEX_ALLOCATE
#
#     def __init__(self, *args, **kwargs):
#         super().__init__(*args, **kwargs)
#         # TODO: impl

# class IndexDeallocatePDU(ContextOptionalPDU):
#     """
#     https://tools.ietf.org/html/rfc2741#section-6.2.13
#     """
#     header_type_ = PduTypes.INDEX_DEALLOCATE
#
#     def __init__(self, *args, **kwargs):
#         super().__init__(*args, **kwargs)
#         # TODO: impl

# class AddAgentCaps(ContextOptionalPDU):
#     """
#     https://tools.ietf.org/html/rfc2741#section-6.2.14
#     """
#     header_type_ = PduTypes.ADD_AGENT_CAPS
#
#     def __init__(self, *args, **kwargs):
#         super().__init__(*args, **kwargs)
#         # TODO: impl

# class RemoveAgentCaps(ContextOptionalPDU):
#     """
#     https://tools.ietf.org/html/rfc2741#section-6.2.15
#     """
#     header_type_ = PduTypes.REMOVE_AGENT_CAPS
#
#     def __init__(self, *args, **kwargs):
#         super().__init__(*args, **kwargs)
#         # TODO: impl

class ResponsePDU(PDU):
    """
    https://tools.ietf.org/html/rfc2741#section-6.2.16
    """
    header_type_ = PduTypes.RESPONSE

    @unique
    class Errors(int, Enum):
        NO_AGENT_X_ERROR = 0,
        OPEN_FAILED = 256,
        NOT_OPEN = 257,
        INDEX_WRONG_TYPE = 258,
        INDEX_ALREADY_ALLOCATED = 259,
        INDEX_NONE_AVAILABLE = 260,
        INDEX_NOT_ALLOCATED = 261,
        UNSUPPORTED_CONTEXT = 262,
        DUPLICATE_REGISTRATION = 263,
        UNKNOWN_REGISTRATION = 264,
        UNKNOWN_AGENT_CAPS = 265,
        PARSE_ERROR = 266,
        REQUEST_DENIED = 267,
        PROCESSING_ERROR = 268,
        """
        From the RFC:

        Within responses to the set of "SNMP request processing" PDU
        types listed in section 6.1, "AgentX PDU Header", values may
        also include those defined for errors in the SNMPv2 PDU (RFC
        1905 [13]).
        """
        # https://tools.ietf.org/html/rfc1905#section-3
        # SNMP2_NO_ERROR = 0
        SNMP2_TOO_BIG = 1,
        SNMP2_NO_SUCH_NAME = 2,
        SNMP2_BAD_VALUE = 3,
        SNMP2_READ_ONLY = 4,
        SNMP2_GEN_ERR = 5,
        SNMP2_NO_ACCESS = 6,
        SNMP2_WRONG_TYPE = 7,
        SNMP2_WRONG_LENGTH = 8,
        SNMP2_WRONG_ENCODING = 9,
        SNMP2_WRONG_VALUE = 10,
        SNMP2_NO_CREATION = 11,
        SNMP2_INCONSISTENT_VALUE = 12,
        SNMP2_RESOURCE_UNAVAILABLE = 13,
        SNMP2_COMMIT_FAILED = 14,
        SNMP2_UNDO_FAILED = 15,
        SNMP2_AUTHORIZATION_ERROR = 16,
        SNMP2_NOT_WRITEABLE = 17,
        SNMP2_INCONSISTENT_NAME = 18,

    def __init__(self, header=None, payload=None, sys_up_time=None, error=None, index=None, values=()):
        super().__init__(header=header, payload=payload)

        if payload is not None:
            self.sys_up_time, self.error, self.index = struct.unpack(
                self.header.endianness + 'LHH', self._trailing_bytes[:8])
            self._trailing_bytes = self._trailing_bytes[8:]

            self.values = []
            bytes_read = 0
            while self._trailing_bytes and bytes_read < self.header.payload_length - 8:
                vr_next = ValueRepresentation.from_bytes(self._trailing_bytes, self.header.endianness)
                self.values.append(vr_next)
                bytes_read += vr_next.size
                self._trailing_bytes = self._trailing_bytes[vr_next.size:]

        else:
            self.sys_up_time, self.error, self.index = sys_up_time, error, index
            # end of stream
            self.values = list(values)
            self.header = self.header._replace(payload_length=self.payload_length)

    def encode(self):
        ret = super().encode()
        fmt = self.header.endianness + 'LHH'
        ret += struct.pack(fmt, self.sys_up_time, self.error, self.index)
        if self.values:
            for value in self.values:
                ret += value.to_bytes(self.header.endianness)
        return ret

    def make_response(self, lut):
        raise NotImplementedError(
            "Yo dawg, I heard you like responses. "
            "So I made a response to your response, "
            "so you can respond while responding."
        )

    def parse(self):
        pass
