__all__ = [
    "GetRequestPDU", "GetNextRequestPDU", "GetBulkRequestPDU",
    "SetRequestPDU",
    "ResponsePDU", "ReportPDU",
    "InformRequestPDU", "SNMPv2TrapPDU",
    "ErrorStatus", "ErrorResponse", "NoSuchName", "ImproperResponse",
]

import enum
import os

from snmp.asn1 import Constructed
from snmp.ber import *
from snmp.exception import *
from snmp.smi import *
from snmp.utils import typename

class ErrorStatus(enum.IntEnum):
    noError             = 0
    tooBig              = 1
    noSuchName          = 2
    badValue            = 3
    readOnly            = 4
    genErr              = 5
    noAccess            = 6
    wrongType           = 7
    wrongLength         = 8
    wrongEncoding       = 9
    wrongValue          = 10
    noCreation          = 11
    inconsistentValue   = 12
    resourceUnavailable = 13
    commitFailed        = 14
    undoFailed          = 15
    authorizationError  = 16
    notWritable         = 17
    inconsistentName    = 18

class PDU(Constructed):
    READ_CLASS = False
    WRITE_CLASS = False
    RESPONSE_CLASS = False
    NOTIFICATION_CLASS = False
    INTERNAL_CLASS = False
    CONFIRMED_CLASS = False

    def __init__(self,
        *args,
        requestID = 0,
        errorStatus = ErrorStatus.noError,
        errorIndex = 0,
        variableBindings = None,
    ):
        self.requestID = requestID
        self.errorStatus = errorStatus
        self.errorIndex = errorIndex

        if variableBindings is None:
            self.variableBindings = VarBindList(*args)
        else:
            self.variableBindings = variableBindings

    def __iter__(self):
        yield Integer(self.requestID)
        yield Integer(self.errorStatus)
        yield Integer(self.errorIndex)
        yield self.variableBindings

    def __len__(self):
        return 4

    def __repr__(self):
        arguments = []
        if self.requestID:
            arguments.append("requestID={}".format(self.requestID))

        if self.errorStatus:
            arguments.append("errorStatus={}".format(self.errorStatus))

        if self.errorIndex:
            arguments.append("errorIndex={}".format(self.errorIndex))

        arguments.append(f"variableBindings={self.variableBindings!r}")

        args = ", ".join(arguments)
        return f"{typename(self)}({args})"

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

    def checkResponse(self, response):
        if not self.validResponse(response.variableBindings):
            raise ImproperResponse(response.variableBindings)

    def toString(self, depth = 0, tab = "    "):
        indent = tab * depth
        subindent = indent + tab
        return "\n".join((
            "{}{}:",
            "{}Request ID: {}",
            "{}Error Status: {}",
            "{}Error Index: {}",
            "{}Variable Bindings:",
            "{}"
        )).format(
            indent, typename(self),
            subindent, self.requestID,
            subindent, self.errorStatus.name,
            subindent, self.errorIndex,
            subindent, self.variableBindings.toString(subindent + tab)
        )

    def withRequestID(self, requestID):
        return self.__class__(
            requestID=requestID,
            errorStatus=self.errorStatus,
            errorIndex=self.errorIndex,
            variableBindings=self.variableBindings,
        )

    @classmethod
    def deserialize(cls, data):
        _requestID, errorStatusData     = Integer.decode(data)
        _errorStatus, errorIndexData    = Integer.decode(errorStatusData)
        _errorIndex, data               = Integer.decode(errorIndexData)
        variableBindings                = VarBindList.decodeExact(data)

        requestID = _requestID.value
        errorStatus = _errorStatus.value
        errorIndex = _errorIndex.value

        try:
            errorStatus = ErrorStatus(errorStatus)
        except ValueError as err:
            msg = f"Invalid errorStatus: {errorStatus}"
            raise ParseError(msg, errorStatusData, errorIndexData) from err

        if (errorStatus != ErrorStatus.noError
        and (errorIndex < 0 or errorIndex > len(variableBindings))):
            msg = f"Error index {errorIndex} not valid" \
                f" with {len(variableBindings)} variable bindings"
            raise ParseError(msg, errorIndexData, data)

        return cls(
            requestID=requestID,
            errorStatus=errorStatus,
            errorIndex=errorIndex,
            variableBindings=variableBindings,
        )

class BulkPDU(Constructed):
    READ_CLASS = False
    WRITE_CLASS = False
    RESPONSE_CLASS = False
    NOTIFICATION_CLASS = False
    INTERNAL_CLASS = False
    CONFIRMED_CLASS = False

    def __init__(self,
        *args,
        requestID = 0,
        nonRepeaters = 0,
        maxRepetitions = 1,
        variableBindings = None,
    ):
        if nonRepeaters < 0:
            errmsg = f"nonRepeaters may not be less than 0: {nonRepeaters}"
            raise ValueError(errmsg)

        if maxRepetitions < 0:
            errmsg = f"maxRepetitions may not be less than 0: {maxRepetitions}"
            raise ValueError(errmsg)

        self.requestID = requestID
        self.nonRepeaters = nonRepeaters
        self.maxRepetitions = maxRepetitions

        if variableBindings is None:
            self.variableBindings = VarBindList(*args)
        else:
            self.variableBindings = variableBindings

        if self.nonRepeaters > len(self.variableBindings):
            errmsg = f"The nonRepeaters parameter ({self.nonRepeaters})" \
                " must not exceed the number of variable bindings" \
                f" in the request ({len(self.variableBindings)})"
            raise ValueError(errmsg)

    def __iter__(self):
        yield Integer(self.requestID)
        yield Integer(self.nonRepeaters)
        yield Integer(self.maxRepetitions)
        yield self.variableBindings

    def __len__(self):
        return 4

    def __repr__(self):
        arguments = []
        if self.requestID:
            arguments.append(f"requestID={self.requestID}")

        if self.nonRepeaters:
            arguments.append(f"nonRepeaters={self.nonRepeaters}")

        if self.maxRepetitions:
            arguments.append(f"maxRepetitions={self.maxRepetitions}")

        arguments.append(f"variableBindings={self.variableBindings!r}")

        args = ", ".join(arguments)
        return f"{typename(self)}({args})"

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

    def checkResponse(self, response):
        if not self.validResponse(response.variableBindings):
            raise ImproperResponse(response.variableBindings)

    def toString(self, depth = 0, tab = "    "):
        indent = tab * (depth + 1)
        return "\n".join((
            "{}:",
            "{}Request ID: {}",
            "{}Non-Repeaters: {}",
            "{}Max Repetitions: {}",
            "{}Variable Bindings:",
            "{}"
        )).format(
            typename(self),
            indent, self.requestID,
            indent, self.nonRepeaters,
            indent, self.maxRepetitions,
            indent, self.variableBindings.toString(tab * (depth + 2))
        )

    def withRequestID(self, requestID):
        return self.__class__(
            requestID=requestID,
            nonRepeaters=self.nonRepeaters,
            maxRepetitions=self.maxRepetitions,
            variableBindings=self.variableBindings,
        )

    @classmethod
    def deserialize(cls, data):
        requestID, nrdata = Integer.decode(data)
        nonRepeaters, mrdata = Integer.decode(nrdata)
        maxRepetitions, vbdata = Integer.decode(mrdata)
        variableBindings = VarBindList.decodeExact(vbdata)

        if nonRepeaters.value < 0:
            msg = "nonRepeaters may not be less than 0"
            raise ParseError(msg, nrdata, mrdata)
        elif maxRepetitions.value < 0:
            msg = "maxRepetitions may not be less than 0"
            raise ParseError(msg, mrdata, vbdata)

        return cls(
            requestID=requestID.value,
            nonRepeaters=nonRepeaters.value,
            maxRepetitions=maxRepetitions.value,
            variableBindings=variableBindings,
        )

class GetRequestPDU(PDU):
    CONFIRMED_CLASS = True
    READ_CLASS = True
    TAG = Tag(0, True, Tag.Class.CONTEXT_SPECIFIC)

    def validResponse(self, vblist):
        if len(vblist) != len(self.variableBindings):
            return False

        for request_vb, response_vb in zip(self.variableBindings, vblist):
            if response_vb.name != request_vb.name:
                return False

        return True

class GetNextRequestPDU(PDU):
    CONFIRMED_CLASS = True
    READ_CLASS = True
    TAG = Tag(1, True, Tag.Class.CONTEXT_SPECIFIC)

    def validResponse(self, vblist):
        if len(vblist) != len(self.variableBindings):
            return False

        for request_vb, response_vb in zip(self.variableBindings, vblist):
            if response_vb.name < request_vb.name:
                return False
            elif (response_vb.name == request_vb.name
            and response_vb.value != EndOfMibView()):
                return False

        return True

class ResponsePDU(PDU):
    RESPONSE_CLASS = True
    TAG = Tag(2, True, Tag.Class.CONTEXT_SPECIFIC)

    def checkErrorStatus(self, request):
        if self.errorStatus == ErrorStatus.noError:
            return
        elif self.errorStatus == ErrorStatus.noSuchName:
            raise NoSuchName(self.errorIndex, request)

        raise ErrorResponse(self.errorStatus, self.errorIndex, request)

class SetRequestPDU(PDU):
    CONFIRMED_CLASS = True
    WRITE_CLASS = True
    TAG = Tag(3, True, Tag.Class.CONTEXT_SPECIFIC)

    def __init__(self, *varbinds, **kwargs):
        if kwargs.get("variableBindings") is None:
            vblist = VarBindList(*varbinds, unpack=True)
            kwargs["variableBindings"] = vblist

        super().__init__(**kwargs)

    def validResponse(self, vblist):
        if len(vblist) != len(self.variableBindings):
            return False

        for request_vb, response_vb in zip(self.variableBindings, vblist):
            if response_vb.name != request_vb.name:
                return False

        return True

class GetBulkRequestPDU(BulkPDU):
    CONFIRMED_CLASS = True
    READ_CLASS = True
    TAG = Tag(5, True, Tag.Class.CONTEXT_SPECIFIC)

    def validResponse(self, vblist):
        if len(vblist) < self.nonRepeaters:
            return False

        for i in range(self.nonRepeaters):
            request_vb = self.variableBindings[i]
            response_vb = vblist[i]

            if response_vb.name < request_vb.name:
                return False
            elif (response_vb.name == request_vb.name
            and response_vb.value != EndOfMibView()):
                return False

        repeaters = self.variableBindings[self.nonRepeaters:]
        m = len(repeaters)

        if m > 0:
            repetitions, leftovers = divmod(len(vblist) - self.nonRepeaters, m)

            if repetitions < min(self.maxRepetitions, 1):
                return False
            elif repetitions > self.maxRepetitions:
                return False
            elif leftovers != 0:
                return False

            prev = repeaters
            for r in range(repetitions):
                start = r * m + self.nonRepeaters
                stop = start + m
                successor = vblist[start:stop]

                for i in range(m):
                    request_vb = prev[i]
                    response_vb = successor[i]

                    if response_vb.name < request_vb.name:
                        return False
                    elif (response_vb.name == request_vb.name
                    and response_vb.value != EndOfMibView()):
                        return False

                prev = successor

        return True

class InformRequestPDU(PDU):
    CONFIRMED_CLASS = True
    NOTIFICATION_CLASS = True
    TAG = Tag(6, True, Tag.Class.CONTEXT_SPECIFIC)

class SNMPv2TrapPDU(PDU):
    NOTIFICATION_CLASS = True
    TAG = Tag(7, True, Tag.Class.CONTEXT_SPECIFIC)

class ReportPDU(PDU):
    INTERNAL_CLASS = True
    RESPONSE_CLASS = True
    TAG = Tag(8, True, Tag.Class.CONTEXT_SPECIFIC)

class ErrorResponse(SNMPException):
    def __init__(self, status, index, request):
        self.status = status
        self.index = min(index, len(request.variableBindings))
        self.variableBindings = request.variableBindings

        if self.index == 0:
            self.oid = None
            details = f"{os.linesep}{request}"
        else:
            self.oid = self.variableBindings[self.index-1].name
            details = f" {self.oid}"

        super().__init__(f"{status.name}:{details}")

class NoSuchName(ErrorResponse):
    def __init__(self, index, request):
        super().__init__(ErrorStatus.noSuchName, index, request)

class ImproperResponse(SNMPException):
    def __init__(self, variableBindings):
        self.variableBindings = variableBindings
