from typing import Iterator
from enum import IntFlag, auto
from ..parameters import Parameter
from ... import exceptions as exc
from ..__class_init__ import *
from ...types import choices
from ...types.implementations import arrays, enums, bitstrings, long_unsigneds, structs
from ... import pdu_enums as pdu
from . import mechanism_id, authentication_mechanism_name
from . import method
from . import abstract
from ..overview import VERSION_0


class AccessMode(abstract.AccessMode, elements=(0, 1, 2, 3)):
    """ TODO: """
    def is_writable(self) -> bool:
        return True if int(self) >= 2 else False

    def is_readable(self) -> bool:
        return True if int(self) in (1, 3) else False


class AttributeAccessItem(abstract.AttributeAccessItem):
    """ Implemented attribute and it access . Use in Association LN """
    DEFAULT = b'\x02\x03\x0f\x01\x16\x00\x00'
    attribute_id: cdt.Integer
    access_mode: AccessMode
    access_selectors: choices.access_selectors

    def abstract_marker(self):
        ...


class AttributeAccessDescriptor(abstract.AttributeAccessDescriptor):
    """ Array of attribute_access_item """
    TYPE = AttributeAccessItem

    def set_read_access(self, attribute_id: cdt.Integer):
        it: AttributeAccessItem
        for it in self:
            if it.attribute_id == attribute_id:
                it.access_mode.set(1)
                break
        else:
            self.append(AttributeAccessItem((attribute_id, AccessMode.parse("1"), None)))


class MethodAccessItem(cdt.Structure):
    """ Implemented method and it access . Use in Association LN """
    method_id: cdt.Integer
    access_mode: cdt.Boolean


class MethodAccessDescriptor(cdt.Array):
    """ Contain all implemented methods """
    TYPE = MethodAccessItem


class AccessRight(cdt.Structure):
    """ TODO: """
    attribute_access: AttributeAccessDescriptor
    method_access: MethodAccessDescriptor


class ObjectListElement(structs.ObjectListElement, access_rights=AccessRight):
    """ Visible COSEM objects with their class_id, version, logical name and the access rights to their attributes and methods within the given application association"""


class ObjectListType(arrays.SelectionAccess):
    """ Array of object_list_element. The range for the client_SAP is 0…0x7F. The range for the server_SAP is 0x000…0x3FFF."""
    TYPE = ObjectListElement
    __getitem__: ObjectListElement

    def is_writable(self, ln: cst.LogicalName, indexes: set[int]) -> bool:
        """ index - DLMS object attribute index.
         True: AccessRight is WriteOnly or ReadAndWrite """
        el: ObjectListElement = next(filter(lambda it: it.logical_name == ln, self), None)
        if el is None:
            raise exc.NoObject(F"not find {ln} in object_list")
        item: AttributeAccessItem
        for index in indexes:
            for item in el.access_rights.attribute_access:
                if int(item.attribute_id) == index:
                    if int(item.access_mode) not in (2, 3):
                        return False
                    else:
                        break
                else:
                    continue
            else:
                raise ValueError(F"not find in {ln} attribute index: {index}")
        return True

    def __get_access_right(self, ln: cst.LogicalName | ut.CosemObjectInstanceId) -> AccessRight:
        """return object_list_element of object_list AssociationLN"""
        el: ObjectListElement = next(filter(lambda it: it.logical_name == ln, self), None)
        if el is None:
            raise exc.NoObject(F"not find {ln} in object_list")
        else:
            return el.access_rights

    def get_attr_access(self, ln: cst.LogicalName, index: int) -> pdu.AttributeAccess:
        """ index - DLMS object attribute index """
        for item in self.__get_access_right(ln).attribute_access:  # item: AttributeAccessItem
            item: AttributeAccessItem
            if int(item.attribute_id) == index:
                return pdu.AttributeAccess(int(item.access_mode))
            else:
                continue
        else:
            raise ValueError(F"access for {ln}: {index} is absense")

    def get_access_mode(self, ln: cst.LogicalName, i: int) -> AccessMode:
        """ index - DLMS object attribute index """
        for item in self.__get_access_right(ln).attribute_access:  # item: AttributeAccessItem
            item: AttributeAccessItem
            if int(item.attribute_id) == i:
                return item.access_mode
            else:
                continue
        else:
            raise ValueError(F"access for {ln}: {i} is absense")

    def get_meth_access(self, ln: cst.LogicalName | ut.CosemObjectInstanceId, index: int) -> pdu.MethodAccess:
        """ index - DLMS object method index """
        for item in self.__get_access_right(ln).method_access:  # item: MethodAccessItem
            item: MethodAccessItem
            if int(item.method_id) == index:
                return pdu.MethodAccess(int(item.access_mode))
            else:
                continue
        else:
            raise exc.ITEApplication(F"not find method access rules in object_list for {ln.get_report()}:{index}")  # todo: make custom error


class AssociatedPartnersType(cdt.Structure):
    """ Contains the identifiers of the COSEM client and the COSEM server (logical device) application processes within the physical devices
    hosting these processes, which belong to the application association modelled by the “Association LN” object. """
    DEFAULT = (0x10, 1)
    client_SAP: enums.ClientSAP
    server_SAP: long_unsigneds.ServerSAP


class ApplicationContextName(cdt.Structure):
    """ In the COSEM environment, it is intended that an application context pre-exists and is referenced by its name during the establishment of an
    application association. This attribute contains the name of the application context for that association."""
    DEFAULT = (2, 16, 116, 5, 8, 1, 1)
    joint_iso_ctt_element: cdt.Unsigned
    country_element: cdt.Unsigned
    country_name_element: cdt.LongUnsigned
    identified_organization_element: cdt.Unsigned
    DLMS_UA_element: cdt.Unsigned
    application_context_element: cdt.Unsigned
    context_id_element: cdt.Unsigned

class ContextNameType(cdt.AXDR, ApplicationContextName):
    """ In the COSEM environment, it is intended that an application context pre-exists and is referenced by its name during the establishment of an
    application association. This attribute contains the name of the application context for that association."""
    DEFAULT = b'\x09\x07\x60\x85\x74\x05\x08\x01\x01'


class XDLMSContextType(cdt.Structure):
    """ Contains all the necessary information on the xDLMS context for the given association. """
    DEFAULT = (None, 1024, 1024, 6, 0, b'\x09\x07\x60\x85\x74\x05\x08\x02\x00')
    conformance: bitstrings.Conformance
    max_receive_pdu_size: cdt.LongUnsigned
    max_send_pdu_size: cdt.LongUnsigned
    dlms_version_number: cdt.Unsigned
    quality_of_service: cdt.Integer
    cyphering_info: cdt.OctetString


class AssociationStatus(cdt.Enum, elements=(0, 1, 2)):
    """ Enum of access mode for methods """


class ClassList(cdt.Array):
    """ Access by class. In this case, only those object_list_elements of the object_list shall be included in the response, which have a class_id
    equal to one of the class_id-s of the class-list. No access_right information is included """
    TYPE = long_unsigneds.ClassId


class ObjectId(cdt.Structure):
    DEFAULT = b'\x02\x02\x12\x00\x08\x09\x06\x00\x00\x01\x00\x00\xff'
    class_id: long_unsigneds.ClassId
    logical_name: cst.LogicalName


class ObjectIdList(cdt.Array):
    """ Access by object. The full information record of object instances on the object_Id_list shall be returned. """
    TYPE = ObjectId


class Representation(IntFlag):
    HEX = 0
    ASCII = auto()
    HIDDEN = auto()
    ASCII_HIDDEN = ASCII | HIDDEN


class LLCSecret(cdt.OctetString):
    """ representation for secret """
    __representation: Representation = Representation.HEX
    # used for set class property from instance

    def __init__(self, value: bytes | bytearray | str | int = None):

        super(LLCSecret, self).__init__(value)
        # self.representation = Representation(0)

    def __setattr__(self, key, value):
        match key:
            case 'representation' if not isinstance(value, Representation): raise ValueError(F"Error representation type")
            case 'representation' as rep:                                   LLCSecret.__representation = value
            case _:                                                         super().__setattr__(key, value)

    @property
    def representation(self) -> Representation:
        """used for set class property from instance"""
        return self.__representation

    def __getattr__(self, item):
        raise AttributeError(F'LLCSecret not has {item}')

    @staticmethod
    def __hide_all(value: str) -> str:
        for char_ in filter(lambda it: it != ' ', value):
            value = value.replace(char_, '*')
        return value

    def from_str(self, value: str) -> bytes:
        if self.__representation & Representation.ASCII:
            return cdt.VisibleString.from_str(self, value)
        else:
            return super(LLCSecret, self).from_str(value)

    def __str__(self):
        match self.__representation:
            case Representation.ASCII:                       return cdt.VisibleString.__str__(self)
            case Representation.HIDDEN:                      return self.__hide_all(super(LLCSecret, self).__str__())
            case Representation.ASCII_HIDDEN:                return self.__hide_all(cdt.VisibleString.__str__(self))
            case _:                                          return super(LLCSecret, self).__str__()


class LLCSecretHigh(LLCSecret):
    DEFAULT = b'0000000000000000'

    def validation(self):
        """ check for length equal 16 """
        if len(self) != 16:
            raise ValueError(F'Got length of High secret: {len(self)}, expected 16')

    def validate_from(self, value: str, cursor_position=None) -> tuple[str, int]:
        try:
            correct = type(self)(value)
            return str(correct), cursor_position + (len(str(correct))-len(value))
        except ValueError:
            match self.representation & 0b1:
                case Representation.HEX:
                    cursor_position: int = len(value)-1 if cursor_position is None else cursor_position
                    type(self)(F'{value[:cursor_position]}0{value[cursor_position:]}')  # check possible
                case Representation.ASCII:
                    type(self)(value.zfill(16))
            return value, cursor_position


class AccessSelector(ut.Unsigned8):
    """ Unsigned8 1..4 """
    def __init__(self, value: int | str | ut.Unsigned8 = 1):
        super(AccessSelector, self).__init__(value)
        if int(self) > 4 or int(self) < 1:
            raise ValueError(F'The {self.__class__.__name__} got {self}, expected 1..4')


class Data(ut.Data):
    ELEMENTS = {1: ut.SequenceElement('All information', cdt.NullData),
                2: ut.SequenceElement('Access by class', ClassList),
                3: ut.SequenceElement('Access by object', ObjectIdList),
                4: ut.SequenceElement('Full object information', ObjectId)}


class SelectiveAccessDescriptor(ut.SelectiveAccessDescriptor):
    """ Selective access specification always starts with an access selector, followed by an access-specific access parameter list.
    Specified IS/IEC 62056-53 : 2006, 7.4.1.6 Selective access """
    access_selector: AccessSelector
    access_parameters: Data
    ELEMENTS = (ut.SequenceElement('access_selector', AccessSelector),
                ut.SequenceElement('access_parameters', Data))


class CosemAttributeDescriptorWithSelection(ut.CosemAttributeDescriptorWithSelection):
    access_selection: SelectiveAccessDescriptor
    ELEMENTS = (ut.SequenceElement('cosem_attribute_descriptor', ut.CosemAttributeDescriptor),
                ut.SequenceElement('access_selection', SelectiveAccessDescriptor))


class AssociationLN(ic.COSEMInterfaceClasses):
    """5.4.5 Association LN"""
    CLASS_ID = ClassID.ASSOCIATION_LN
    VERSION = VERSION_0
    A_ELEMENTS = (ic.ICAElement("object_list", ObjectListType, selective_access=SelectiveAccessDescriptor),
                  ic.ICAElement("associated_partners_id", AssociatedPartnersType),
                  ic.ICAElement("application_context_name", ContextNameType),
                  ic.ICAElement("xDLMS_context_info", XDLMSContextType),
                  ic.ICAElement("authentication_mechanism_name", authentication_mechanism_name.MechanismNameType),
                  ic.ICAElement("LLS_secret", LLCSecret, classifier=ic.Classifier.NOT_SPECIFIC),
                  ic.ICAElement("association_status", AssociationStatus, classifier=ic.Classifier.DYNAMIC))
    M_ELEMENTS = (ic.ICMElement("reply_to_HLS_authentication", method.ReplyToHLSAuthentication),
                  ic.ICMElement("change_HLS_secret", LLCSecret),
                  ic.ICMElement("add_object", ObjectListElement),
                  ic.ICMElement("remove_object", ObjectListElement))

    def characteristics_init(self):
        # self.object_list.selective_access = SelectiveAccessDescriptor()
        self.set_attr(4, None)
        self.set_attr(5, None)
        self.set_attr(8, None)
        # init secret after set authentication_mechanism_name(6)
        self._cbs_attr_post_init.update({
            6: self.__init_secret,
            7: self.__check_mechanism_id_existing})

    @property
    def object_list(self) -> ObjectListType:
        return self.get_attr(2)

    @property
    def associated_partners_id(self) -> AssociatedPartnersType:
        return self.get_attr(3)

    @property
    def application_context_name(self) -> ApplicationContextName:
        return self.get_attr(4)

    @property
    def xDLMS_context_info(self) -> XDLMSContextType:
        return self.get_attr(5)

    @property
    def authentication_mechanism_name(self) -> authentication_mechanism_name.AuthenticationMechanismName:
        return self.get_attr(6)

    @property
    def LLS_secret(self) -> LLCSecret:
        return self.get_attr(7)

    @property
    def association_status(self) -> AssociationStatus:
        return self.get_attr(8)

    @property
    def reply_to_HLS_authentication(self) -> method.ReplyToHLSAuthentication:
        return self.get_meth(1)

    @property
    def change_HLS_secret(self) -> LLS_secret:
        return self.get_meth(2)

    @property
    def add_object(self) -> ObjectListElement:
        return self.get_meth(3)

    @property
    def remove_object(self) -> ObjectListElement:
        return self.get_meth(4)

    def __check_mechanism_id_existing(self):
        """check for existing mechanism ID else ERASE setting"""
        if self.authentication_mechanism_name is None:
            self.clear_attr(7)
            self._cbs_attr_post_init[7] = self.__check_mechanism_id_existing
        else:
            """nothing do it"""

    def __init_secret(self):
        """ before initiating secret need knowledge what kind of mechanism ID """
        match self.authentication_mechanism_name.mechanism_id_element, self.LLS_secret:
            case mechanism_id.NONE | mechanism_id.LOW, LLCSecret(): """keep secret value"""
            case mechanism_id.NONE | mechanism_id.LOW, _:           self.set_attr_link(7, LLCSecret())
            case mechanism_id.HIGH, LLCSecretHigh():                          """keep secret value"""
            case mechanism_id.HIGH, _:                                        self.set_attr_link(7, LLCSecretHigh())
            case unknown, _:                                                            raise ValueError(F'Not support Secret with {unknown}')

    def get_attr_descriptor(self,
                            value: int,
                            with_selection: bool = False) -> ut.CosemAttributeDescriptor | CosemAttributeDescriptorWithSelection:
        """ with selection for object_list. TODO: Copypast ProfileGeneric"""
        descriptor: ut.CosemAttributeDescriptor = super(AssociationLN, self).get_attr_descriptor(value)
        if value == 2 and with_selection and self.object_list:
            return CosemAttributeDescriptorWithSelection((descriptor, self.object_list.selective_access))
        else:
            return descriptor

    def get_lns(self) -> list[cst.LogicalName]:
        """return all LogicalNames"""
        el: ObjectListElement
        return [el.logical_name  for el in self.object_list]

    def iter_pars(self) -> Iterator[Parameter]:
        return (Parameter(el.logical_name.contents) for el in self.object_list)

    def __check_empty_object_list(self):
        if self.object_list is None:
            raise exc.ITEApplication(F"empty <{self.get_attr_element(2).NAME}> in {self}")

    def is_readable(self,
                    ln: cst.LogicalName,
                    index: int,
                    security_policy: pdu.SecurityPolicy = pdu.SecurityPolicyVer0.NOTHING
                    ) -> bool:
        self.__check_empty_object_list()
        match self.object_list.get_attr_access(ln, index):
            case pdu.AttributeAccess.NO_ACCESS | pdu.AttributeAccess.WRITE_ONLY | pdu.AttributeAccess.AUTHENTICATED_WRITE_ONLY:
                return False
            case pdu.AttributeAccess.READ_ONLY | pdu.AttributeAccess.READ_AND_WRITE:
                return True
            case pdu.AttributeAccess.AUTHENTICATED_READ_ONLY | pdu.AttributeAccess.AUTHENTICATED_READ_AND_WRITE:
                if isinstance(security_policy, pdu.SecurityPolicyVer0):
                    match security_policy:
                        case pdu.SecurityPolicyVer0.AUTHENTICATED | pdu.SecurityPolicyVer0.AUTHENTICATED_AND_ENCRYPTED:
                            return True
                        case _:
                            return False
                elif isinstance(security_policy, pdu.SecurityPolicyVer1):
                    if bool(security_policy & (pdu.SecurityPolicyVer1.AUTHENTICATED_REQUEST | pdu.SecurityPolicyVer1.AUTHENTICATED_RESPONSE)):
                        return True
                    else:
                        return False
                else:
                    raise TypeError(F"unknown {security_policy.__class__}: {security_policy}")
            case err:
                raise exc.ITEApplication(F"unsupport access: {err}")

    def is_writable(self,
                    ln: cst.LogicalName,
                    index: int,
                    security_policy: pdu.SecurityPolicy = pdu.SecurityPolicyVer0.NOTHING
                    ) -> bool:
        self.__check_empty_object_list()
        return is_attr_writable(
            mode=self.object_list.get_access_mode(ln, index),
            security_policy=security_policy)

    def is_accessible(self, ln: cst.LogicalName,
                      index: int,
                      m_id: mechanism_id.MechanismIdElement = None
                      ) -> bool:
        """for ver 0 and 1 only"""
        self.__check_empty_object_list()
        match self.object_list.get_meth_access(ln, index):
            case pdu.MethodAccess.NO_ACCESS:
                return False
            case pdu.MethodAccess.ACCESS:
                return True
            case pdu.MethodAccess.AUTHENTICATED_ACCESS:
                if not m_id:
                    m_id = self.authentication_mechanism_name.mechanism_id_element
                if m_id > mechanism_id.NONE:
                    return True
                else:
                    return False
            case err:
                raise exc.ITEApplication(F"unsupport access: {err}")


def is_attr_writable(
        mode: AccessMode,
        security_policy: pdu.SecurityPolicy = pdu.SecurityPolicyVer0.NOTHING) -> bool:
    match int(mode):
        case pdu.AttributeAccess.NO_ACCESS | pdu.AttributeAccess.READ_ONLY | pdu.AttributeAccess.AUTHENTICATED_READ_ONLY:
            return False
        case pdu.AttributeAccess.WRITE_ONLY | pdu.AttributeAccess.READ_AND_WRITE:
            return True
        case pdu.AttributeAccess.AUTHENTICATED_WRITE_ONLY | pdu.AttributeAccess.AUTHENTICATED_READ_AND_WRITE:
            if isinstance(security_policy, pdu.SecurityPolicyVer0):
                match security_policy:
                    case pdu.SecurityPolicyVer0.AUTHENTICATED | pdu.SecurityPolicyVer0.AUTHENTICATED_AND_ENCRYPTED:
                        return True
                    case _:
                        return False
            elif isinstance(security_policy, pdu.SecurityPolicyVer1):
                if bool(security_policy & (pdu.SecurityPolicyVer1.AUTHENTICATED_REQUEST | pdu.SecurityPolicyVer1.AUTHENTICATED_RESPONSE)):
                    return True
                else:
                    return False
            else:
                raise TypeError(F"unknown {security_policy.__class__}: {security_policy}")
        case err:
            raise exc.ITEApplication(F"unsupport access: {err}")
