# SPDX-License-Identifier: MPL-2.0
import abc
import enum
import ipaddress
import typing

import attr

from cryptodatahub.common.exception import InvalidValue
from cryptodatahub.ike.algorithm import (
    Ikev1IdType,
    Ikev1PayloadType,
    Ikev2IdType,
    Ikev2PayloadType,
)

from cryptoparser.common.exception import InvalidType
from cryptoparser.common.parse import ParsableBase, ParserBinary, ComposerBinary


@attr.s
class IkePayloadTypeUnknown(ParsableBase):
    """Wrapper for an unknown IKE payload-type code (RFC 2408 §3.10, RFC 7296 §3.2)."""

    code: int = attr.ib(validator=attr.validators.instance_of(int))

    @classmethod
    def get_byte_num(cls):
        return 1

    @classmethod
    def _parse(cls, parsable):
        parser = ParserBinary(parsable)
        parser.parse_numeric('code', cls.get_byte_num())
        return cls(parser['code']), parser.parsed_length

    def compose(self):
        composer = ComposerBinary()
        composer.compose_numeric(self.code, self.get_byte_num())
        return composer.composed_bytes


class _IkePayloadTypeFactoryBase(ParsableBase):
    """Yield payload-type enum member or :class:`IkePayloadTypeUnknown` for unregistered wire codes."""

    @classmethod
    @abc.abstractmethod
    def _get_enum_class(cls):
        raise NotImplementedError()

    @classmethod
    def _parse(cls, parsable):
        parser = ParserBinary(parsable)
        parser.parse_numeric('code', 1)
        for enum_item in cls._get_enum_class():
            if enum_item.value.code == parser['code']:
                return enum_item, 1
        return IkePayloadTypeUnknown(parser['code']), 1

    def compose(self):
        raise NotImplementedError()


class Ikev1PayloadTypeFactory(_IkePayloadTypeFactoryBase):
    @classmethod
    def _get_enum_class(cls):
        return Ikev1PayloadType

    def compose(self):
        raise NotImplementedError()


class Ikev2PayloadTypeFactory(_IkePayloadTypeFactoryBase):
    @classmethod
    def _get_enum_class(cls):
        return Ikev2PayloadType

    def compose(self):
        raise NotImplementedError()


@attr.s
class IkeIdentificationFqdnMixin:
    """Fully-qualified domain name (RFC 2407 §4.6.2.1, RFC 7296 §3.5)."""

    identifier: str = attr.ib(validator=attr.validators.instance_of(str))

    @classmethod
    def get_id_type_ikev1(cls):
        return Ikev1IdType.FQDN

    @classmethod
    def get_id_type_ikev2(cls):
        return Ikev2IdType.FQDN

    @classmethod
    def _decode_identifier(cls, id_data):
        return id_data.decode('ascii', errors='replace')

    def _encode_identifier(self):
        return self.identifier.encode('ascii')


@attr.s
class IkeIdentificationUserFqdnMixin:
    """User fully-qualified domain name (RFC 2407 §4.6.2.1)."""

    identifier: str = attr.ib(validator=attr.validators.instance_of(str))

    @classmethod
    def get_id_type_ikev1(cls):
        return Ikev1IdType.USER_FQDN

    @classmethod
    def get_id_type_ikev2(cls):
        return None

    @classmethod
    def _decode_identifier(cls, id_data):
        return id_data.decode('ascii', errors='replace')

    def _encode_identifier(self):
        return self.identifier.encode('ascii')


@attr.s
class IkeIdentificationRfc822AddrMixin:
    """RFC 822 email address (RFC 7296 §3.5)."""

    identifier: str = attr.ib(validator=attr.validators.instance_of(str))

    @classmethod
    def get_id_type_ikev1(cls):
        return None

    @classmethod
    def get_id_type_ikev2(cls):
        return Ikev2IdType.RFC822_ADDR

    @classmethod
    def _decode_identifier(cls, id_data):
        return id_data.decode('ascii', errors='replace')

    def _encode_identifier(self):
        return self.identifier.encode('ascii')


@attr.s
class IkeIdentificationIpv4AddrMixin:
    """IPv4 address (RFC 2407 §4.6.2.1, RFC 7296 §3.5)."""

    identifier: ipaddress.IPv4Address = attr.ib(
        converter=ipaddress.IPv4Address,
        validator=attr.validators.instance_of(ipaddress.IPv4Address),
    )

    @classmethod
    def get_id_type_ikev1(cls):
        return Ikev1IdType.IPV4_ADDR

    @classmethod
    def get_id_type_ikev2(cls):
        return Ikev2IdType.IPV4_ADDR

    @classmethod
    def _decode_identifier(cls, id_data):
        if len(id_data) != 4:
            raise InvalidType()
        return ipaddress.IPv4Address(id_data)

    def _encode_identifier(self):
        return self.identifier.packed


@attr.s
class IkeIdentificationIpv6AddrMixin:
    """IPv6 address (RFC 2407 §4.6.2.1, RFC 7296 §3.5)."""

    identifier: ipaddress.IPv6Address = attr.ib(
        converter=ipaddress.IPv6Address,
        validator=attr.validators.instance_of(ipaddress.IPv6Address),
    )

    @classmethod
    def get_id_type_ikev1(cls):
        return Ikev1IdType.IPV6_ADDR

    @classmethod
    def get_id_type_ikev2(cls):
        return Ikev2IdType.IPV6_ADDR

    @classmethod
    def _decode_identifier(cls, id_data):
        if len(id_data) != 16:
            raise InvalidType()
        return ipaddress.IPv6Address(id_data)

    def _encode_identifier(self):
        return self.identifier.packed


@attr.s
class IkeIdentificationDerAsn1DnMixin:
    """DER-encoded ASN.1 X.500 Distinguished Name (RFC 2407 §4.6.2.1, RFC 7296 §3.5)."""

    identifier: typing.Union[bytes, bytearray] = attr.ib(
        validator=attr.validators.instance_of((bytes, bytearray)),
    )

    @classmethod
    def get_id_type_ikev1(cls):
        return Ikev1IdType.DER_ASN1_DN

    @classmethod
    def get_id_type_ikev2(cls):
        return Ikev2IdType.DER_ASN1_DN

    @classmethod
    def _decode_identifier(cls, id_data):
        return id_data

    def _encode_identifier(self):
        return bytes(self.identifier)


@attr.s
class IkeIdentificationKeyIdMixin:
    """Opaque vendor-specific key identifier (RFC 2407 §4.6.2.1, RFC 7296 §3.5)."""

    identifier: typing.Union[bytes, bytearray] = attr.ib(
        validator=attr.validators.instance_of((bytes, bytearray)),
    )

    @classmethod
    def get_id_type_ikev1(cls):
        return Ikev1IdType.KEY_ID

    @classmethod
    def get_id_type_ikev2(cls):
        return Ikev2IdType.KEY_ID

    @classmethod
    def _decode_identifier(cls, id_data):
        return id_data

    def _encode_identifier(self):
        return bytes(self.identifier)


@attr.s
class IkeIdentificationDerAsn1GnMixin:
    """DER-encoded ASN.1 X.500 General Name (RFC 2407 §4.6.2.1, RFC 7296 §3.5)."""

    identifier: typing.Union[bytes, bytearray] = attr.ib(
        validator=attr.validators.instance_of((bytes, bytearray)),
    )

    @classmethod
    def get_id_type_ikev1(cls):
        return Ikev1IdType.DER_ASN1_GN

    @classmethod
    def get_id_type_ikev2(cls):
        return Ikev2IdType.DER_ASN1_GN

    @classmethod
    def _decode_identifier(cls, id_data):
        return id_data

    def _encode_identifier(self):
        return bytes(self.identifier)


@attr.s
class IkeIdentificationFcNameMixin:
    """Fibre Channel name (RFC 4595, RFC 7296 §3.5)."""

    identifier: typing.Union[bytes, bytearray] = attr.ib(
        validator=attr.validators.instance_of((bytes, bytearray)),
    )

    @classmethod
    def get_id_type_ikev1(cls):
        return None

    @classmethod
    def get_id_type_ikev2(cls):
        return Ikev2IdType.FC_NAME

    @classmethod
    def _decode_identifier(cls, id_data):
        return id_data

    def _encode_identifier(self):
        return bytes(self.identifier)


@attr.s
class IkeIdentificationNullMixin:
    """NULL identification (RFC 7619)."""

    identifier: bytes = attr.ib(
        validator=attr.validators.instance_of((bytes, bytearray)),
    )

    @classmethod
    def get_id_type_ikev1(cls):
        return None

    @classmethod
    def get_id_type_ikev2(cls):
        return Ikev2IdType.NULL

    @classmethod
    def _decode_identifier(cls, id_data):
        return id_data

    def _encode_identifier(self):
        return bytes(self.identifier)


@attr.s
class DataAttributeBase(ParsableBase):
    """Data attribute base parser.

    .. code-block:: text

                              1                   2                   3
          0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
         +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         !A!       Attribute Type        !    AF=0  Attribute Length     !
         !F!                             !    AF=1  Attribute Value      !
         +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         .                   AF=0  Attribute Value                       .
         .                   AF=1  Not Transmitted                       .
         +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    """

    @classmethod
    @abc.abstractmethod
    def _parse(cls, parsable):
        raise NotImplementedError()

    @abc.abstractmethod
    def compose(self):
        raise NotImplementedError()

    @classmethod
    @abc.abstractmethod
    def get_type(cls):
        raise NotImplementedError()

    @classmethod
    def _get_format(cls):
        raise NotImplementedError()

    @classmethod
    def _parse_header(cls, parsable):
        parser = ParserBinary(parsable)

        parser.parse_numeric('format', 1, DataAttributeFormat)
        if parser['format'] != cls._get_format():
            raise InvalidType()

        parser.parse_numeric_enum_coded('type', type(cls.get_type()))
        if parser['type'] != cls.get_type():
            raise InvalidType()

        return parser

    def _compose_header(self):
        composer = ComposerBinary()

        composer.compose_numeric(self._get_format().value, 1)
        composer.compose_numeric_enum_coded(self.get_type())

        return composer


class DataAttributeFormat(enum.IntEnum):
    """Data attribute types."""
    TYPE_LENGTH_VALUE = 0x00
    TYPE_VALUE = 0x80


@attr.s
class DataAttributeTypeValue(DataAttributeBase):
    """Data attribute type/value parser."""

    @classmethod
    @abc.abstractmethod
    def get_type(cls):
        raise NotImplementedError()

    @classmethod
    @abc.abstractmethod
    def _parse(cls, parsable):
        raise NotImplementedError()

    @abc.abstractmethod
    def compose(self):
        raise NotImplementedError()

    @classmethod
    def _get_format(cls):
        return DataAttributeFormat.TYPE_VALUE


@attr.s
class DataAttributeTypeValueEnumCoded(DataAttributeTypeValue):
    """Data attribute type/value parser where the type is an enum."""

    value: typing.Any = attr.ib()

    @classmethod
    @abc.abstractmethod
    def get_type(cls):
        raise NotImplementedError()

    @classmethod
    @abc.abstractmethod
    def _get_enum_type(cls):
        raise NotImplementedError()

    @value.validator
    def _validate_value(self, _, value):
        enum_type = self._get_enum_type()
        if not isinstance(value, enum_type):
            raise InvalidValue(value, type(self), 'value')

    @classmethod
    def _parse(cls, parsable):
        parser = cls._parse_header(parsable)

        parser.parse_numeric_enum_coded('value', cls._get_enum_type())

        return cls(value=parser['value']), parser.parsed_length

    def compose(self):
        composer = self._compose_header()

        composer.compose_numeric_enum_coded(self.value)

        return composer.composed_bytes


@attr.s
class DataAttributeKeyLength(DataAttributeTypeValue):
    """Key Length transform attribute (TV format).

    The Key Length attribute has the following format:

    0                   1                   2                   3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |A|       Attribute Type        |    Key Length (in bits)       |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

    :ivar key_length: Key length in bits
    """

    value: int = attr.ib(validator=attr.validators.and_(
        attr.validators.instance_of(int),
        attr.validators.ge(0),
        attr.validators.lt(2**64)
    ))

    @classmethod
    @abc.abstractmethod
    def get_type(cls):
        raise NotImplementedError()

    @classmethod
    def _parse(cls, parsable):
        parser = cls._parse_header(parsable)
        parser.parse_numeric('value', 2)

        return cls(value=parser['value']), parser.parsed_length

    def compose(self):
        composer = self._compose_header()

        composer.compose_numeric(self.value, 2)

        return composer.composed_bytes


@attr.s
class DataAttributeLength(DataAttributeBase):
    """Length transform attribute (TLV format).

    The Key Length attribute has the following format:

    0                   1                   2                   3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |A|       Attribute Type        |    Key Length (in bits)       |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

    :ivar key_length: Key length in bits
    """

    value: int = attr.ib(validator=attr.validators.and_(
        attr.validators.instance_of(int),
        attr.validators.ge(0),
        attr.validators.lt(2**64)
    ))

    @classmethod
    @abc.abstractmethod
    def get_type(cls):
        raise NotImplementedError()

    @classmethod
    @abc.abstractmethod
    def _get_size(cls):
        raise NotImplementedError()

    @classmethod
    def _get_format(cls):
        return DataAttributeFormat.TYPE_VALUE

    @classmethod
    def _parse(cls, parsable):
        parser = cls._parse_header(parsable)

        if parser['format'] == DataAttributeFormat.TYPE_LENGTH_VALUE:
            parser.parse_numeric('size', 2)
            parser.parse_numeric('value', parser['size'])
        else:
            parser.parse_numeric('value', cls._get_size())

        return cls(value=parser['value']), parser.parsed_length

    def compose(self):
        composer = self._compose_header()

        if self._get_format() == DataAttributeFormat.TYPE_LENGTH_VALUE:
            composer.compose_numeric(self._get_size(), 2)
            composer.compose_numeric(self.value, self._get_size())
        else:
            composer.compose_numeric(self.value, self._get_size())

        return composer.composed_bytes
