"""
BGP extended community data type class
"""

# pylint: disable=too-few-public-methods

from typing import TYPE_CHECKING
from ipaddress import IPv4Address, AddressValueError

import nadap.types.base
import nadap.mixin.ranges
import nadap.mixin.allowed_value
import nadap.mixin.not_allowed_value
import nadap.schema
from nadap.base import ValEnv, CONVERT_DATA
from nadap.errors import SchemaDefinitionError
from nadap.doc import UnorderedTextList

if TYPE_CHECKING:
    from nadap.doc import TextField


DOC_DT_NAME = "BGP Extended Community"
DOC_DT_DESCRIPTION = """
A **bgp_ext_community** data type tests data for being an instance of
python's built-in class `str` and if it represents a BGP extended standard community (6 bytes).
"""
DOC_DT_FEATURES = """
- Validate BGP Extended Community format
- Supports BGP Extended Community format conversion
- Validate BGP Extended Community specific attributes
- Validate against allowed and not allowed BGP Extended Communities
- Supports **Referencing Feature**
"""
DOC_DT_YAML_EXAMPLE = r"""
type: bgp_ext_community
description: "Example bgp_ext_community definition"
default_value: 10.0.0.1:33

format: ip-address
convert_to_format: asn

not_allowed_values:
  - 10.0.0.0:1

not_allowed_ranges:
  - start: 11.0.0.0:0
    end: 11.255.255.255:65535

reference: ref_key
"""


IP_FORMAT = "ip-address"  # "192.1.1.1:100"
ASN_FORMAT = "asn"  # "65500:33"


class BgpExtCommunity(
    nadap.mixin.not_allowed_value.NotAllowedValueMixin,
    nadap.mixin.allowed_value.AllowedValueMixin,
    nadap.mixin.ranges.ValueRangesMixin,
    nadap.types.base.BaseType,
):
    """
    BGP Community datatype class
    """

    data_type_name = "bgp_ext_community"
    _cls_python_classes = [str]
    _convert_to_classes = {}
    _doc_data_type = "BGP Extended Community"

    def __init__(self, **kwargs):
        self._format = None
        self._detected_format = None
        self._convert_to_format = None
        super().__init__(**kwargs)

    @staticmethod
    def _check_format_str(format_, path):
        if format_ not in [IP_FORMAT, ASN_FORMAT]:
            raise SchemaDefinitionError(  # pylint: disable=raise-missing-from
                msg="Format unknown",
                path=path,
            )

    def _validate_options(self, schema_path: str):
        super()._validate_options(schema_path=schema_path)

        if self._format is not None:
            nadap.schema.is_str(self._format, f"{schema_path}.format")
            self._check_format_str(self._format, f"{schema_path}.format")

        if self._convert_to_format is not None:
            self._check_format_str(
                self._convert_to_format, f"{schema_path}.convert_to_format"
            )

    def _pop_options(self, definition: dict, schema_path: str):
        self._format = definition.pop("format", self._format)
        self._convert_to_format = definition.pop(
            "convert_to_format", self._convert_to_format
        )
        super()._pop_options(definition, schema_path)

    def _apply_data_format(self, data: "any", fmt: str = None) -> "any":
        part1 = int(data / (2**16))
        part2 = data % (2**16)
        if fmt == IP_FORMAT:
            part1 = IPv4Address(part1)
        return f"{part1}:{part2}"

    def _detect_data_format(self, data) -> str:
        p1 = data.split(":")[0]
        if "." in p1:
            return IP_FORMAT
        return ASN_FORMAT

    def _validate_format(
        self,
        data: any,
        path: str,
        env: "ValEnv" = None,
    ) -> str:
        # pylint: disable=too-many-branches
        def raise_fail(path, env):
            self._raise_exceptions(
                "Value is not a valid BGP extended community", path, env
            )

        def raise_asn_fail(path, env):
            self._raise_exceptions(
                "Value is not a valid BGP extended community in ASN format", path, env
            )

        def raise_ip_fail(path, env):
            self._raise_exceptions(
                "Value is not a valid BGP extended community in IP-Address format",
                path,
                env,
            )

        int_value = 0
        if data.count(":") != 1:
            raise_fail(path, env)
        part1, part2 = data.split(":")
        # Analyzing part 1 (before :)
        if part1.count(".") == 3:
            # Test for IP-format
            if self._format == ASN_FORMAT:
                raise_asn_fail(path, env)
            try:
                int_value = int(IPv4Address(part1))
                self._detected_format = IP_FORMAT
            except AddressValueError:
                raise_fail(path, env)
        else:
            # Test for ASN-Format
            if self._format == IP_FORMAT:
                raise_ip_fail(path, env)
            try:
                int_value = int(part1)
                self._detected_format = ASN_FORMAT
            except ValueError:
                raise_fail(path, env)
            if not 0 <= int_value < 2**32:
                raise_fail(path, env)
        int_value = 2**16 * int_value
        try:
            part2_int = int(part2)
        except ValueError:
            raise_fail(path, env)
        if not 0 <= part2_int < 2**16:
            raise_fail(path, env)
        int_value += int(part2)
        return int_value

    def _preprocess_data(self, data: any, env: "ValEnv"):
        if CONVERT_DATA in env.flags and self._convert_to_format is not None:
            data = self._apply_data_format(data, self._convert_to_format)
        else:
            data = self._apply_data_format(data, self._detected_format)
        return data

    def _test_data_type(self, data: str, path: str, env: "ValEnv" = None) -> "str":
        data = super()._test_data_type(data=data, path=path, env=env)
        data = self._validate_format(data=data, path=path, env=env)
        if not 0 <= data < 2**48:
            self._raise_exceptions(
                "BGP extended community value is out of range", path, env
            )
        return data

    @property
    def restrictions(self) -> "TextField":
        """
        Get all restrictions for valid data
        """
        tf = super().restrictions
        _restrictions = UnorderedTextList()
        if self._format:
            _restrictions.append((f"must be in format '{self._format}'"))
        if _restrictions:
            tf.append("BGP Extended Community:")
            tf.append(_restrictions)
        return tf

    @property
    def yaml_data_type(self) -> str:
        """Get YAML data type string"""
        return self._doc_data_type

    @property
    def doc_types(self) -> list[str]:
        """Get list of data type strings"""
        return [self._doc_data_type]

    @classmethod
    def _doc_options_md_upper_part(cls) -> list[str]:
        allowed_formats = (
            "allowed_values:<br>"
            + f"{cls._markdown_indent}- {ASN_FORMAT}<br>"
            + f"{cls._markdown_indent}- {IP_FORMAT}<br>"
        )
        f_desc = (
            f"- {ASN_FORMAT} example: 65500:33<br>"
            + f"- {IP_FORMAT} example: 1.1.1.1:33<br>"
        )
        return super()._doc_options_md_upper_part() + [
            f"| **format** | <code>enum</code> | | {allowed_formats} | {f_desc} |",
            f"| **convert_to_format** | <code>enum</code> | | {allowed_formats} | |",
        ]

    @classmethod
    def _doc_options_yaml_upper_part(cls) -> list[str]:
        return super()._doc_options_yaml_upper_part() + [
            f"format: <{ASN_FORMAT}|{IP_FORMAT}>",
            f"convert_to_format: <{ASN_FORMAT}|{IP_FORMAT}>",
        ]


DOC_DT_CLASS = BgpExtCommunity  # pylint: disable=invalid-name
