import json
import os
import pwd
import struct

from pyroute2.common import AddrPool
from pyroute2.netlink.core import (
    AsyncCoreSocket,
    CoreMessageQueue,
    CoreStreamProtocol,
)
from pyroute2.netlink.coredata import CoreConfig, CoreSocketSpec
from pyroute2.plan9 import (
    Marshal9P,
    msg_tattach,
    msg_tcall,
    msg_tread,
    msg_tversion,
    msg_twalk,
    msg_twrite,
)


class Plan9ClientSocket(AsyncCoreSocket):
    '''9p2000 client.

    * address -- `('address', port)` to listen on
    * use_socket -- alternatively, provide a connected SOCK_STRAM socket
    '''

    def __init__(self, address=None, use_socket=None, use_event_loop=None):
        self.spec = CoreSocketSpec(
            CoreConfig(
                tag_field='tag',
                target='localhost',
                netns=None,
                address=address,
                use_socket=use_socket is not None,
                use_event_loop=use_event_loop is not None,
            )
        )
        self.marshal = Marshal9P()
        self.wnames = {'': 0}
        self.cwd = 0
        self.fid_pool = AddrPool(minaddr=0x00000001, maxaddr=0x0000FFFF)
        super().__init__(use_socket=use_socket, use_event_loop=use_event_loop)

    def enqueue(self, data, addr):
        tag = struct.unpack_from('H', data, 5)[0]
        return self.msg_queue.put_nowait(tag, data)

    async def setup_endpoint(self, loop=None):
        if getattr(self.local, 'transport', None) is not None:
            return
        self.local.msg_queue = CoreMessageQueue(event_loop=self.event_loop)
        if self.status['use_socket']:
            address = {'sock': self.use_socket}
        else:
            address = {
                'host': self.status['address'][0],
                'port': self.status['address'][1],
            }
        self.local.transport, self.local.protocol = (
            await self.event_loop.create_connection(
                lambda: CoreStreamProtocol(
                    self.connection_lost,
                    self.enqueue,
                    self._error_event,
                    self.status,
                ),
                **address,
            )
        )

    async def start_session(self):
        '''Initiate 9p2000 session.

        One must await this routine before running any other requests.
        '''
        await self.setup_endpoint()
        await self.version()
        await self.auth()
        await self.attach()

    async def request(self, msg, tag=0):
        await self.setup_endpoint()
        if tag == 0:
            tag = self.addr_pool.alloc()
        try:
            msg['header']['tag'] = tag
            msg.reset()
            msg.encode()
            self.msg_queue.ensure_tag(tag)
            self.transport.write(msg.data)
            return [x async for x in self.get(msg_seq=tag)][0]
        finally:
            self.addr_pool.free(tag, ban=0xFF)

    async def version(self):
        '''`Tverion` request. No arguments required.'''
        m = msg_tversion()
        m['header']['tag'] = 0xFFFF
        m['msize'] = 8192
        m['version'] = '9P2000'
        return await self.request(m, tag=0xFFFF)

    async def auth(self):
        pass

    async def attach(self, aname=''):
        '''`Tattach` request.

        * `aname` (optional) -- aname to attach to
        '''
        m = msg_tattach()
        m['fid'] = 0
        m['afid'] = 0xFFFFFFFF
        m['uname'] = pwd.getpwuid(os.getuid()).pw_name
        m['aname'] = aname
        return await self.request(m)

    async def walk(self, path, newfid=None, fid=None):
        '''`Twalk` request.

        * `path` -- string path to the file
        * `newfid` (optional) -- use this fid to store the info
        * `fid` (optional) -- use this fid to walk from, otherwise walk
          from the current directory for this client session
        '''
        m = msg_twalk()
        m['fid'] = self.cwd if fid is None else fid
        m['newfid'] = newfid if newfid is not None else self.fid_pool.alloc()
        m['wname'] = path.split(os.path.sep)
        self.wnames[path] = m['newfid']
        return await self.request(m)

    async def fid(self, path):
        '''Walk the path and return `fid` to the required file.

        * `path` -- string path to the file
        '''
        if path not in self.wnames:
            newfid = self.fid_pool.alloc()
            await self.walk(path, newfid)
            self.wnames[path] = newfid
        return self.wnames[path]

    async def read(self, fid, offset=0, count=8192):
        '''`Tread` request.

        * `fid` -- fid of the file to read from
        * `offset` (optional, default 0) -- read offset
        * `count` (optional, default 8192) -- read count
        '''
        m = msg_tread()
        m['fid'] = fid
        m['offset'] = offset
        m['count'] = count
        return await self.request(m)

    async def write(self, fid, data, offset=0):
        '''`Twrite` request.

        * `fid` -- fid of the file to write to
        * `data` -- bytes to write
        * `offset` (optional, default 0) -- write offset
        '''
        m = msg_twrite()
        m['fid'] = fid
        m['offset'] = 0
        m['data'] = data
        return await self.request(m)

    async def call(
        self,
        fid,
        argv=None,
        kwarg=None,
        data=b'',
        data_arg='data',
        loader=json.loads,
    ):
        '''`Tcall` request.

        * `fid` -- fid of the file that represents a registered function
        * `argv` (optional) -- positional arguments as an iterable
        * `kwarg` (optional) -- keyword arguments as a dictionary
        * `data` (opional) -- optional binary data
        * `data_arg` (optional) -- name of the argument to use with
          the binary data
        * `loader` (optional, default `json.loads`) -- loader for the
          response data
        '''
        spec = {
            'argv': argv if argv is not None else [],
            'kwarg': kwarg if kwarg is not None else {},
            'data_arg': data_arg,
        }
        m = msg_tcall()
        m['fid'] = fid
        m['text'] = json.dumps(spec)
        m['data'] = data
        response = await self.request(m)
        return loader(response['data'])
