# Copyright 2021-2024 Nokia

import base64
from abc import ABC, abstractmethod
from collections import OrderedDict
from typing import (Any, Dict, Iterable, List, Mapping, NamedTuple, Optional,
                    Set, Tuple, Union)

from .errors import *
from .errors import make_exception
from .identifier import Identifier
from .model_path import ModelPath
from .singleton import Empty

INTEGRAL_LEAF_TYPE = ("int8", "int16", "int32", "int64", "uint8", "uint16", "uint32", "uint64")
DECIMAL_LEAF_TYPE = INTEGRAL_LEAF_TYPE + ("decimal64",)
YANG_PRIMITIVE_TYPES = INTEGRAL_LEAF_TYPE + (
    "binary", "boolean", "decimal64", "empty", "string", "instance-identifier"
)

INTEGRAL_TYPES_MIN = {
    'uint8':    "0",
    'uint16':   "0",
    'uint32':   "0",
    'uint64':   "0",
    'int8':     str(-(2**7)),
    'int16':    str(-(2**15)),
    'int32':    str(-(2**31)),
    'int64':    str(-(2**63) + 1),
}

INTEGRAL_TYPES_MAX = {
    'int8':     str((2 ** 7) - 1),
    'uint8':    str((2 ** 8) - 1),
    'int16':    str((2 ** 15) - 1),
    'uint16':   str((2 ** 16) - 1),
    'int32':    str((2 ** 31) - 1),
    'uint32':   str((2 ** 32) - 1),
    'int64':    str((2 ** 63) - 1),
    'uint64':   str((2 ** 64) - 1),
}

assert set(INTEGRAL_LEAF_TYPE) == set(INTEGRAL_TYPES_MIN.keys()) == set(INTEGRAL_TYPES_MAX.keys())

class YangTypeBase(ABC):
    """Abstract interface for all leaf types.

    .. Reviewed by TechComms 20210713
    """

    @abstractmethod
    def __str__(self):
        pass

    @abstractmethod
    def __repr__(self):
        pass

    @abstractmethod
    def __hash__(self):
        pass

    @abstractmethod
    def __eq__(self, other):
        if other is None:
            return False
        assert isinstance(other, YangTypeBase)
        return self.__class__ is other.__class__

    @property
    def name(self):
        return self.__str__()

    def to_string(self, val: Any) -> Tuple[str, Optional[str]]:
        """Translate value to xml text without any checking."""
        # User input is also checked by check_field_value, paths are not
        if val is None:
            return '', None
        if isinstance(val, bool) and isinstance(self, PrimitiveType):
            if self.identifier.name == 'boolean':
                return str(val).lower(), None
            assert self.identifier.name in INTEGRAL_LEAF_TYPE
            return str(int(val)), None
        if isinstance(val, (int, str)):
            return str(val), None
        if val is Empty:
            return '', None
        if isinstance(val, bytes):
            return base64.b64encode(val).decode('utf8'), None
        raise make_exception(
            pysros_err_unexpected_value_of_type,
            val_type=type(val), type=str(self)
        )

    def to_value(self, val: str) -> Any:
        """Transform string to native Python type.

            If the value does not fit the expected type, a TypeError exception is raised. This exception is tested
            in union."""
        raise make_exception(
            pysros_err_invalid_value_for_type, type=self, value=val
        )

    @abstractmethod
    def check_field_value(self, value: Any, json=False, strict=False, is_convert=False, metadata=None) -> bool:
        """Check whether value is acceptable for a specific yang_type."""
        return False

    def json_name(self) -> str:
        return self.__class__.__name__.lower()

    def as_storage_type(self, obj, is_convert, idref_cb):
        if obj is None:
            return ""
        return obj


def _is_valid_decimal64(value: str) -> bool:
    return isinstance(value, str)


def _is_valid_base64(value: Any) -> bool:
    return isinstance(value, (bytes, str))


class PrimitiveType(YangTypeBase):
    """Primitive built-in types representation, such as int8, str, and binary. All types are in YANG_PRIMITIVE_TYPES.

    .. Reviewed by TechComms 20210713
    """

    def __init__(self, name: str, yang_range: Optional[str] = None, fraction_digits: Optional[int] = None, length: Optional[str] = None):
        assert name in YANG_PRIMITIVE_TYPES
        self.identifier = Identifier.builtin(name)
        self.yang_range = yang_range
        self.fraction_digits = fraction_digits
        self.length = length

    def __str__(self):
        return str(self.identifier)

    def __repr__(self):
        if self.yang_range:
            repr_str = f"(name={self.identifier.name!r}, yang_range={self.yang_range!r})"
        else:
            repr_str = f"({self.identifier.name!r})"
        return self.__class__.__name__ + repr_str

    def __eq__(self, other):
        if not YangTypeBase.__eq__(self, other):
            return False
        return (
            self.identifier == other.identifier and
            self.yang_range == other.yang_range and
            self.fraction_digits == other.fraction_digits
        )

    def __hash__(self):
        return hash((
            self.identifier,
            self.yang_range,
            self.fraction_digits,
        ))

    def to_value(self, val: str) -> Any:
        t = self.identifier.name
        if val is None or val == "":
            if t == "empty":
                return Empty
            return ("" if t in ("string", "decimal64", "binary") else None)
        if t in INTEGRAL_LEAF_TYPE:
            return int(val)
        if t == "boolean":
            if isinstance(val, str) and val.lower() in ("true", "false"):
                return val.lower() == "true"
            elif isinstance(val, bool):
                return val
        if t == "empty":
            return Empty
        if t in ("string", "binary"):
            return val
        if t == "decimal64":
            return val
        return super().to_value(val)

    _check_field_value = {
        "string": lambda val: type(val) == str,
        "empty": lambda val:  val is Empty,
        "boolean": lambda val:  isinstance(val, bool),
        "decimal64": _is_valid_decimal64,
        "binary": _is_valid_base64
    }

    def check_field_value(self, value: Any, json=False, strict=False, is_convert=False, metadata=None) -> bool:
        name = self.identifier.name
        if not strict and value is None:
            if not is_convert and name == "empty":
                return metadata and metadata.get("ietf-netconf:operation") in ("remove", "delete")
            return True
        cmp = self._check_field_value.get(name, None)
        if cmp is not None:
            return cmp(value)

        if json and name in ("int64", "uint64"):
            return isinstance(name, str)
        elif name in INTEGRAL_LEAF_TYPE:
            return isinstance(value, int) and not isinstance(value, bool)
        return False

    def json_name(self) -> str:
        return self.identifier.name

    def as_storage_type(self, obj, is_convert, idref_cb):
        if obj is None:
            if self.identifier.name == "empty":
                return Empty
            elif self.identifier.name in ("string", "decimal64", "binary"):
                return ""
            else:
                return obj
        if self.identifier.name == "boolean" and type(obj) == int:
            return bool(obj)
        elif self.identifier.name in INTEGRAL_LEAF_TYPE and (type(obj) == bool or self.identifier.name in ("int64", "uint64")):
            return int(obj)
        return super().as_storage_type(obj, is_convert, idref_cb)


class UnresolvedIdentifier(YangTypeBase):
    """Identifier that has not been resolved yet."""

    def __init__(self, identifier: Identifier, yang_range: Optional[str] = None, fraction_digits: Optional[int] = None, length: Optional[str] = None):
        self.identifier = identifier
        self.yang_range = yang_range
        self.fraction_digits = fraction_digits
        self.length = length
        self.enums: Set[str] = set()  # Also for bits
        assert not self.identifier.is_builtin()

    def __str__(self):
        return str(self.identifier)

    def __repr__(self):
        if self.yang_range:
            repr_str = f"(identifier={self.identifier!r}, yang_range={self.yang_range!r})"
        else:
            repr_str = f"({self.identifier!r})"
        return self.__class__.__name__ + repr_str

    def __eq__(self, other):
        if not YangTypeBase.__eq__(self, other):
            return False
        return (
            self.identifier == other.identifier and
            self.yang_range == other.yang_range and
            self.fraction_digits == other.fraction_digits
        )

    def __hash__(self):
        return hash((
            self.identifier,
            self.yang_range,
            self.fraction_digits,
        ))

    def to_string(self, _val: Any) -> Tuple[str, Optional[str]]:
        raise make_exception(pysros_err_unresolved_type, type=self)

    def to_value(self, _val: str) -> Any:
        raise make_exception(pysros_err_unresolved_type, type=self)

    def check_field_value(self, value: Any, json=False, strict=False, is_convert=False, metadata=None) -> bool:
        return False

    def add_enum(self, name: str):
        self.enums.add(name)

    def add_bit(self, name: str):
        self.add_enum(name)

    def set_last_enum_value(self, val: int):
        pass


class YangUnion(YangTypeBase):
    def __init__(self, types: List["YangType"] = []):
        super().__init__()

        self._types: List["YangType"] = types.copy() if isinstance(types, list) else list(types)
        self.deduplicate()

    def __iter__(self):
        return self._types.__iter__()

    def __getitem__(self, *args):
        return self._types.__getitem__(*args)

    def append(self, t: "YangType"):
        self._types.append(t)

    def __str__(self):
        return f"union[{','.join(map(str, self._types))}]"

    def __repr__(self):
        return f"{self.__class__.__name__}(({', '.join(map(repr, self._types))}))"

    def __hash__(self):
        return hash(tuple(self._types))

    def __eq__(self, other):
        return (
            YangTypeBase.__eq__(self, other)
            and self._types == other._types
        )

    def __len__(self):
        return len(self._types)

    def to_string(self, val: Any) -> Tuple[str, Optional[str]]:
        if val is None:
            return '', None
        if isinstance(val, str):
            for t in self._types:
                if isinstance(t, IdentityRef) and t._find_identity(val):
                    return t.to_string(val)
        if isinstance(val, bool):
            return "true" if val else "false", None
        return super().to_string(val)

    def to_value(self, val: Any) -> Any:
        if val is None:
            return ""
        return val

    def check_field_value(self, value: Any, json=False, strict=False, is_convert=False, metadata=None) -> bool:
        if not strict and value is None:
            return True
        if isinstance(value, str):
            return True
        return any(t.check_field_value(value, json=json, strict=strict, is_convert=is_convert, metadata=metadata) for t in self._types)

    def json_name(self) -> str:
        return "union"

    def deduplicate(self):
        types = list(OrderedDict.fromkeys(self._types))
        if len(types) != len(self._types):
            self._types = types

    def _find_identity(self, val: str, xml_nsmap=None):
        for t in self._types:
            if isinstance(t, IdentityRef):
                identity = t._find_identity(val, xml_nsmap)
                if identity is not None:
                    return identity
        return None

    def as_storage_type(self, obj, is_convert, idref_cb):
        if obj is None:
            return ""
        if isinstance(obj, str):
            identity = self._find_identity(obj)
            if identity is not None:
                if idref_cb:
                    idref_cb(identity)
                return identity.module + ':' + identity.name

        return super().as_storage_type(obj,  is_convert, idref_cb)


class Enumeration(YangTypeBase):
    def __init__(self, *args, **kwargs):
        self.enums = OrderedDict(*args, **kwargs)

    def __str__(self):
        return f"enumeration[{'|'.join(map(str, self.keys()))}]"

    def __hash__(self):
        return hash(tuple(self))

    def __iter__(self):
        return iter(self.enums)

    def __reversed__(self):
        return reversed(self.enums)

    def __len__(self):
        return len(self.enums)

    def __getitem__(self, name):
        return self.enums[name]

    def __setitem__(self, name, val):
        self.enums[name] = val

    def __delitem__(self, name):
        del self.enums[name]

    def __eq__(self, rhs):
        return self.enums == rhs.enums

    def __repr__(self):
        return f"Enumeration({repr(self.enums)})"

    def keys(self):
        return self.enums.keys()

    def values(self):
        return self.enums.values()

    def items(self):
        return self.enums.items()

    def add_enum(self, name: str):
        val = 1 + max(self.values()) if self else 0
        self[name] = val

    def add_enums(self, *names: str):
        for name in names:
            self.add_enum(name)

    def set_last_enum_value(self, val: int):
        assert self
        last_used_key = next(reversed(self))
        self[last_used_key] = val

    def to_value(self, val: str) -> Any:
        if val is None:
            return ""
        return str(val)

    def check_field_value(self, value: Any, json=False, strict=False, is_convert=False, metadata=None) -> bool:
        if not strict and value is None:
            return True
        return isinstance(value, str)


class Bits(YangTypeBase):
    def __init__(self, vals=()):
        self.enums = {}
        for i in vals:
            self.add_bit(i)

    def __iter__(self):
        return iter(self.enums)

    def __reversed__(self):
        return reversed(self.enums)

    def __len__(self):
        return len(self.enums)

    def __eq__(self, rhs):
        return self.enums == rhs.enums

    def __repr__(self):
        return f"Bits({repr(list(self.keys()))})"

    def keys(self):
        return self.enums.keys()

    def values(self):
        return self.enums.values()

    def items(self):
        return self.enums.items()

    def __str__(self):
        return f"bits[{' '.join(sorted(self))}]"

    def __hash__(self):
        return hash(frozenset(self))

    def is_valid_value(self, val: Any) -> bool:
        return isinstance(val, str)

    def to_value(self, val: str) -> Any:
        if val is None:
            return ""
        if self.is_valid_value(val):
            return val
        return super().to_value(val)

    def check_field_value(self, value: Any, json=False, strict=False, is_convert=False, metadata=None) -> bool:
        if not strict and value is None:
            return True
        return self.is_valid_value(value)

    def add_bit(self, name):
        self.enums[name] = None


class LeafRef(YangTypeBase):
    def __init__(self, path=None):
        if path is None or isinstance(path, ModelPath):
            self._path: Optional[ModelPath] = path
            return
        assert isinstance(path, tuple) and all(isinstance(p, Identifier) for p in path)
        self._path = ModelPath(path)

    def set_path(self, path: ModelPath):
        self._path = path

    def __eq__(self, other):
        return YangTypeBase.__eq__(self, other) and self._path == other._path

    def __ne__(self, other):
        return not (self == other)

    def __str__(self):
        return f"leafref({self._path!s})"

    def __repr__(self):
        return f"LeafRef({self._path.repr_path()})"

    def __hash__(self):
        return hash((self.__class__.__name__, self._path))

    def to_string(self, _val: Any) -> Tuple[str, Optional[str]]:
        raise make_exception(pysros_err_unresolved_leafref, type=str(self))

    def to_value(self, _val: str) -> Any:
        raise make_exception(pysros_err_unresolved_leafref, type=str(self))

    def check_field_value(self, value: Any, json=False, strict=False, is_convert=False, metadata=None) -> bool:
        return False

    @property
    def path(self):
        return self._path


class Identity(NamedTuple):
    name: str
    module: str
    namespace: str


class IdentityRef(YangTypeBase):
    def __init__(self, bases=(), identities: Optional[Iterable[Identity]] = None):
        self.bases:  List[Identifier] = list(bases)
        self.values: Dict[str, Identity] = {} if identities is None else {i.name: i for i in identities}

    def add_base(self, base: Identifier):
        assert isinstance(base, Identifier)
        self.bases.append(base)

    def set_values(self, derived: Mapping[Identifier, Set[Identifier]], ns_map):
        assert not self.values
        self.values = {}
        for b in self.bases:
            if b not in derived:
                continue
            for id in derived[b]:
                if id.prefix not in ns_map:
                    raise RuntimeError(
                        f"Module {id.prefix} is not in map of known modules"
                    )
                self.values[id.name] = Identity(id.name, id.prefix, ns_map[id.prefix])

    def __eq__(self, other):
        return (
            YangTypeBase.__eq__(self, other)
            and self.bases == other.bases
            and self.values == other.values
        )

    def __str__(self):
        return f"identityref[{', '.join(b.__str__() for b in self.bases)}]"

    def __repr__(self):
        # values has to be stable order
        ordered_values = sorted(self.values.values(), key=lambda id: id.name)
        return f"IdentityRef({self.bases!r}, {ordered_values!r})"

    def __hash__(self):
        return hash((self.__class__.__name__, *self.bases, frozenset(self.values)))

    def to_string(self, val: str) -> Tuple[str, Optional[str]]:
        if val == "":
            return "", None
        identity = self._find_identity(val)
        if not identity:
            raise make_exception(
                pysros_err_invalid_value_for_type, type=self, value=val
            )
        return (
            identity.module + ':' + identity.name,
            identity.module
        )

    def _find_identity(self, val: str, xml_nsmap=None):
        if ':' in val:
            mod, value = val.split(':', 1)
            if not mod:
                return None
            result = self.values.get(value, None)
            if result is None:
                return None
            if xml_nsmap is None:
                return result if result.module == mod else None
            else:
                return result if result.namespace == xml_nsmap.get(mod, None) else None
        else:
            return self.values.get(val, None)

    def to_value(self, _val: str) -> Any:
        if _val == "":
            return None
        return _val

    def as_storage_type(self, obj, is_convert, idref_cb):
        if obj is None:
            return ""
        identinty = self._find_identity(obj)
        if idref_cb:
            idref_cb(identinty)
        return identinty.module + ':' + identinty.name

    def check_field_value(self, value: Any, json=False, strict=False, is_convert=False, metadata=None) -> bool:
        if not strict and value is None:
            return True
        return (
            isinstance(value, str) and
            self._find_identity(value) is not None
        )


YangType = Union[
    PrimitiveType,
    UnresolvedIdentifier,
    Enumeration,
    Bits,
    YangUnion,
    LeafRef,
    IdentityRef]


_KNOWN_TYPES = {
    "leafref":     LeafRef,
    "bits":        Bits,
    "enumeration": Enumeration,
    "union":       YangUnion,
    "identityref": IdentityRef,
}


def should_be_buildin(name):
    return name in _KNOWN_TYPES or name in YANG_PRIMITIVE_TYPES


def type_from_name(identifier: Identifier):
    if identifier.is_builtin():
        t = _KNOWN_TYPES.get(identifier.name)
        if t is not None:
            return t()
        return PrimitiveType(identifier.name)
    return UnresolvedIdentifier(identifier)
