'''
'''

from functools import partial

from pyroute2.netlink.nfnetlink import nfgen_msg
from pyroute2.netlink.nfnetlink.nftsocket import (
    DATA_TYPE_ID_TO_NAME,
    DATA_TYPE_NAME_TO_INFO,
    NFT_MSG_DELCHAIN,
    NFT_MSG_DELRULE,
    NFT_MSG_DELSET,
    NFT_MSG_DELSETELEM,
    NFT_MSG_DELTABLE,
    NFT_MSG_GETCHAIN,
    NFT_MSG_GETRULE,
    NFT_MSG_GETSET,
    NFT_MSG_GETSETELEM,
    NFT_MSG_GETTABLE,
    NFT_MSG_NEWCHAIN,
    NFT_MSG_NEWRULE,
    NFT_MSG_NEWSET,
    NFT_MSG_NEWSETELEM,
    NFT_MSG_NEWTABLE,
    AsyncNFTSocket,
    NFTSocket,
    nft_chain_msg,
    nft_rule_msg,
    nft_set_elem_list_msg,
    nft_set_msg,
    nft_table_msg,
)


class NFTSet:
    __slots__ = ('table', 'name', 'key_type', 'timeout', 'counter', 'comment')

    def __init__(self, table, name, **kwargs):
        self.table = table
        self.name = name

        for attrname in self.__slots__:
            if attrname in kwargs:
                setattr(self, attrname, kwargs[attrname])
            elif attrname not in ("table", "name"):
                setattr(self, attrname, None)

    def as_netlink(self):
        attrs = {"NFTA_SET_TABLE": self.table, "NFTA_SET_NAME": self.name}
        set_flags = set()

        if self.key_type is not None:
            key_type, key_len, _ = DATA_TYPE_NAME_TO_INFO.get(self.key_type)
            attrs["NFTA_SET_KEY_TYPE"] = key_type
            attrs["NFTA_SET_KEY_LEN"] = key_len

        if self.timeout is not None:
            set_flags.add("NFT_SET_TIMEOUT")
            attrs["NFTA_SET_TIMEOUT"] = self.timeout

        if self.counter is True:
            attrs["NFTA_SET_EXPR"] = {'attrs': [('NFTA_EXPR_NAME', 'counter')]}

        if self.comment is not None:
            attrs["NFTA_SET_USERDATA"] = [
                ("NFTNL_UDATA_SET_COMMENT", self.comment)
            ]

        # ID is used for bulk create, but not implemented
        attrs['NFTA_SET_ID'] = 1
        attrs["NFTA_SET_FLAGS"] = set_flags
        return attrs

    @classmethod
    def from_netlink(cls, msg):
        data_type_name = DATA_TYPE_ID_TO_NAME.get(
            msg.get_attr("NFTA_SET_KEY_TYPE"),
            msg.get_attr("NFTA_SET_KEY_TYPE"),  # fallback to raw value
        )

        counter = False
        expr = msg.get_attr('NFTA_SET_EXPR')
        if expr:
            expr = expr.get_attrs('NFTA_EXPR_NAME')
            if expr and "counter" in expr:
                counter = True

        comment = None
        udata = msg.get_attr("NFTA_SET_USERDATA")
        if udata:
            for key, value in udata:
                if key == "NFTNL_UDATA_SET_COMMENT":
                    comment = value
                    break

        return cls(
            table=msg.get_attr('NFTA_SET_TABLE'),
            name=msg.get_attr('NFTA_SET_NAME'),
            key_type=data_type_name,
            timeout=msg.get_attr('NFTA_SET_TIMEOUT'),
            counter=counter,
            comment=comment,
        )

    @classmethod
    def from_dict(cls, d):
        return cls(
            **{
                name: value
                for name, value in d.items()
                if name in cls.__slots__
            }
        )

    def as_dict(self):
        return {name: getattr(self, name) for name in self.__slots__}

    def __repr__(self):
        return str(self.as_dict())


class NFTSetElem:
    __slots__ = (
        'value',
        'timeout',
        'expiration',
        'counter_bytes',
        'counter_packets',
        'comment',
    )

    def __init__(self, value, **kwargs):
        self.value = value
        for name in self.__slots__:
            if name in kwargs:
                setattr(self, name, kwargs[name])
            elif name != "value":
                setattr(self, name, None)

    @classmethod
    def from_netlink(cls, msg, modifier):
        value = msg.get_attr('NFTA_SET_ELEM_KEY').get_attr("NFTA_DATA_VALUE")
        if modifier is not None:
            # Need to find a better way
            modifier.data = value
            modifier.length = 4 + len(modifier.data)
            modifier.decode()
            value = modifier.value

        kwarg = {
            "expiration": msg.get_attr('NFTA_SET_ELEM_EXPIRATION'),
            "timeout": msg.get_attr('NFTA_SET_ELEM_TIMEOUT'),
        }

        elem_expr = msg.get_attr('NFTA_SET_ELEM_EXPR')
        if elem_expr:
            if elem_expr.get_attr('NFTA_EXPR_NAME') == "counter":
                elem_expr = elem_expr.get_attr("NFTA_EXPR_DATA")
                kwarg.update(
                    {
                        "counter_bytes": elem_expr.get_attr(
                            "NFTA_COUNTER_BYTES"
                        ),
                        "counter_packets": elem_expr.get_attr(
                            "NFTA_COUNTER_PACKETS"
                        ),
                    }
                )

        udata = msg.get_attr('NFTA_SET_ELEM_USERDATA')
        if udata:
            for type_name, data in udata:
                if type_name == "NFTNL_UDATA_SET_ELEM_COMMENT":
                    kwarg["comment"] = data

        return cls(value=value, **kwarg)

    def as_netlink(self, modifier):
        if modifier is not None:
            modifier.value = self.value
            modifier.encode()
            value = modifier["value"]
        else:
            value = self.value

        attrs = [
            ['NFTA_SET_ELEM_KEY', {'attrs': [('NFTA_DATA_VALUE', value)]}]
        ]

        if self.timeout is not None:
            attrs.append(['NFTA_SET_ELEM_TIMEOUT', self.timeout])

        if self.expiration is not None:
            attrs.append(['NFTA_SET_ELEM_EXPIRATION', self.expiration])

        if self.comment is not None:
            attrs.append(
                [
                    'NFTA_SET_ELEM_USERDATA',
                    [("NFTNL_UDATA_SET_ELEM_COMMENT", self.comment)],
                ]
            )

        return {'attrs': attrs}

    @classmethod
    def from_dict(cls, d):
        return cls(
            **{
                name: value
                for name, value in d.items()
                if name in cls.__slots__
            }
        )

    def as_dict(self):
        return {name: getattr(self, name) for name in self.__slots__}

    def __repr__(self):
        return str(self.as_dict())


class AsyncNFTables(AsyncNFTSocket):
    # TODO: documentation
    # TODO: tests
    # TODO: dump()/load() with support for json and xml

    async def get_tables(self):
        return await self.request_get(nfgen_msg(), NFT_MSG_GETTABLE)

    async def get_chains(self):
        return await self.request_get(nfgen_msg(), NFT_MSG_GETCHAIN)

    async def get_rules(self):
        return await self.request_get(nfgen_msg(), NFT_MSG_GETRULE)

    async def get_sets(self):
        return await self.request_get(nfgen_msg(), NFT_MSG_GETSET)

    #
    # The nft API is in the prototype stage and may be
    # changed until the release. The planned release for
    # the API is 0.5.2
    #

    async def table(self, cmd, **kwarg):
        '''
        Example::

            nft.table('add', name='test0')
        '''
        commands = {
            'add': NFT_MSG_NEWTABLE,
            'create': NFT_MSG_NEWTABLE,
            'del': NFT_MSG_DELTABLE,
            'get': NFT_MSG_GETTABLE,
        }
        return await self._command(nft_table_msg, commands, cmd, kwarg)

    async def chain(self, cmd, **kwarg):
        '''
        Example::

            #
            # default policy 'drop' for input
            #
            nft.chain('add',
                      table='test0',
                      name='test_chain0',
                      hook='input',
                      type='filter',
                      policy=0)
        '''
        commands = {
            'add': NFT_MSG_NEWCHAIN,
            'create': NFT_MSG_NEWCHAIN,
            'del': NFT_MSG_DELCHAIN,
            'get': NFT_MSG_GETCHAIN,
        }
        # TODO: What about 'ingress' (netdev family)?
        hooks = {
            'prerouting': 0,
            'input': 1,
            'forward': 2,
            'output': 3,
            'postrouting': 4,
        }
        if 'hook' in kwarg:
            kwarg['hook'] = {
                'attrs': [
                    ['NFTA_HOOK_HOOKNUM', hooks[kwarg['hook']]],
                    ['NFTA_HOOK_PRIORITY', kwarg.pop('priority', 0)],
                ]
            }
        if 'type' not in kwarg:
            kwarg['type'] = 'filter'
        return await self._command(nft_chain_msg, commands, cmd, kwarg)

    async def rule(self, cmd, **kwarg):
        '''
        Example::

            from pyroute2.nftables.expressions import ipv4addr, verdict
            #
            # allow all traffic from 192.168.0.0/24
            #
            nft.rule('add',
                     table='test0',
                     chain='test_chain0',
                     expressions=(ipv4addr(src='192.168.0.0/24'),
                                  verdict(code=1)))
        '''
        commands = {
            'add': NFT_MSG_NEWRULE,
            'create': NFT_MSG_NEWRULE,
            'insert': NFT_MSG_NEWRULE,
            'replace': NFT_MSG_NEWRULE,
            'del': NFT_MSG_DELRULE,
            'get': NFT_MSG_GETRULE,
        }

        if 'expressions' in kwarg:
            expressions = []
            for exp in kwarg['expressions']:
                expressions.extend(exp)
            kwarg['expressions'] = expressions
        return await self._command(nft_rule_msg, commands, cmd, kwarg)

    async def sets(self, cmd, **kwarg):
        '''
        Example::
            nft.sets("add", table="filter", name="test0", key_type="ipv4_addr",
                     timeout=10000, counter=True,
                     comment="my comment max 252 bytes")
            nft.sets("get", table="filter", name="test0")
            nft.sets("del", table="filter", name="test0")
            my_set = nft.sets("add", set=NFTSet(table="filter", name="test1",
                              key_type="ipv4_addr")
            nft.sets("del", set=my_set)
        '''
        commands = {
            'add': NFT_MSG_NEWSET,
            'get': NFT_MSG_GETSET,
            'del': NFT_MSG_DELSET,
        }

        if "set" in kwarg:
            nft_set = kwarg.pop("set")
        else:
            nft_set = NFTSet(**kwarg)
        kwarg = nft_set.as_netlink()
        msg = [
            x
            async for x in await self._command(
                nft_set_msg, commands, cmd, kwarg
            )
        ]
        if cmd == "get":
            return NFTSet.from_netlink(msg)
        return nft_set

    async def set_elems(self, cmd, **kwarg):
        '''
        Example::
            nft.set_elems("add", table="filter", set="test0",
                          elements={"10.2.3.4", "10.4.3.2"})
            nft.set_elems("add", set=NFTSet(table="filter", name="test0"),
                          elements=[{"value": "10.2.3.4", "timeout": 10000}])
            nft.set_elems("add", table="filter", set="test0",
                          elements=[NFTSetElem(value="10.2.3.4",
                                               timeout=10000,
                                               comment="hello world")])
            nft.set_elems("get", table="filter", set="test0")
            nft.set_elems("del", table="filter", set="test0",
                          elements=["10.2.3.4"])
        '''
        commands = {
            'add': NFT_MSG_NEWSETELEM,
            'get': NFT_MSG_GETSETELEM,
            'del': NFT_MSG_DELSETELEM,
        }
        if isinstance(kwarg["set"], NFTSet):
            nft_set = kwarg.pop("set")
            kwarg["table"] = nft_set.table
            kwarg["set"] = nft_set.name
        else:
            nft_set = await self.sets(
                "get", table=kwarg["table"], name=kwarg["set"]
            )

        found = DATA_TYPE_NAME_TO_INFO.get(nft_set.key_type)
        if found:
            _, _, modifier = found
            modifier = modifier()
            modifier.header = None
        else:
            modifier = None

        if cmd == "get":
            msg = nft_set_elem_list_msg()
            msg['attrs'] = [
                ["NFTA_SET_ELEM_LIST_TABLE", kwarg["table"]],
                ["NFTA_SET_ELEM_LIST_SET", kwarg["set"]],
            ]
            msg = [
                x
                async for x in await self.request_get(msg, NFT_MSG_GETSETELEM)
            ][0]
            elements = set()
            for elem in msg.get_attr('NFTA_SET_ELEM_LIST_ELEMENTS'):
                elements.add(NFTSetElem.from_netlink(elem, modifier))
            return elements

        elements = []
        for elem in kwarg.pop("elements"):
            if isinstance(elem, dict):
                elem = NFTSetElem.from_dict(elem)
            elif not isinstance(elem, NFTSetElem):
                elem = NFTSetElem(value=elem)
            elements.append(elem.as_netlink(modifier))
        kwarg["elements"] = elements
        return await self._command(nft_set_elem_list_msg, commands, cmd, kwarg)


class NFTables(NFTSocket):
    def __init__(self, version=1, attr_version=0, nfgen_family=2):
        self.asyncore = AsyncNFTables(version, attr_version, nfgen_family)
        self.asyncore.local.keep_event_loop = True
        self.asyncore.event_loop.run_until_complete(
            self.asyncore.setup_endpoint()
        )

    @staticmethod
    async def _collect_dump(func):
        return [x async for x in await func()]

    def __getattr__(self, name):
        async_dump_methods = [
            'get_tables',
            'get_chains',
            'get_rules',
            'get_sets',
        ]
        async_cmd_methods = ['table', 'chain', 'rule', 'sets', 'set_elems']
        symbol = getattr(self.asyncore, name)
        if name in async_dump_methods:
            return partial(
                self._run_with_cleanup, partial(self._collect_dump, symbol)
            )
        elif name in async_cmd_methods:
            return partial(self._run_with_cleanup, symbol)
        return symbol
