from util import logsetup
import os
import json
import asyncio
import toml
import logging
import coloredlogs
import traceback
from datetime import timedelta

LOGLEVEL = logging.INFO
FORMAT   = "[%(asctime)s %(msecs)3d] [%(levelname)7s] %(name)7s: %(message)s"

logger = logsetup("tsstatus.log", FORMAT, LOGLEVEL, "tsstatus")
sq_client = None

def kv_parse(string):
    data = {}
    for kv in string.split(' '):
        try:
            val = kv.split('=', 1)[1]
        except IndexError:
            val = ''
        data[kv.split('=')[0]] = val
    return data

class SQResult:
    def __init__(self, data):
        self.data = data

    def kvs(self):
        return kv_parse(self.data)

    def split(self, token):
        return [SQResult(part) for part in self.data.split(token)]

class SQError(Exception):
    def __init__(self, message, errno):
        super().__init__(message)
        self.errno = errno

class ServerQueryClient:
    def __init__(self, username: str, password: str,
                 host: str = '127.0.0.1', port: int = 10011):
        self.running = True
        self.host = host
        self.port = port
        self.username = username
        self.password = password
        self.request_queue = asyncio.Queue()
        self.task = asyncio.create_task(self.async_client())
        self.ping_task = asyncio.create_task(self.ping())
        self.loop = asyncio.get_event_loop()
        self.debug = True

    @classmethod
    async def create(cls, *args, **kwargs):
        self = cls(*args, **kwargs)
        await self.raw_request(f'login {self.username} {self.password}')
        await self.raw_request(f'use 1')
        return self

    async def ping(self):
        while self.running:
            await asyncio.sleep(10)
            response = await self.raw_request('version')

    def raw_request(self, req: str):
        fut = self.loop.create_future()
        # The queue has no limit, so put_nowait works.
        # This allows us to just return the future, which means we won't
        # get odd syntax like await await self.raw_request(...)
        self.request_queue.put_nowait((req, fut))
        return fut

    def request(self, req: str):
        fut = self.raw_request(req)
        new_fut = self.loop.create_future()
        def _callback(fut):
            try:
                result = fut.result()
                new_fut.set_result(SQResult(result))
            except SQError as e:
                new_fut.set_exception(e)
        fut.add_done_callback(_callback)
        return new_fut

    async def stop(self):
        self.running = False
        self.task.cancel()
        self.ping_task.cancel()

    def __await__(self):
        return iter(asyncio.gather(self.task, self.ping_task))

    def get_error(self, data):
        try:
            errstr = data.split('\n')[-2].strip()
        except IndexError:
            traceback.print_exc()
            print(data)
        assert errstr.startswith('error')
        data = kv_parse(errstr)
        return data['msg'].replace(r'\s', ' '), int(data['id'])

    async def async_client(self):
        reader, writer = await asyncio.open_connection(self.host, self.port)
        welcome_message = await reader.read(16384)

        logger.debug("[sq_client] Read welcome message.")

        try:
            while self.running:
                request, future = await self.request_queue.get()

                writer.write((request + '\n').encode('utf-8'))
                await writer.drain()
                data = (await reader.read(16384)).decode('utf-8')
                if self.debug and 'login' not in request and 'version' not in request:
                    logger.debug(f"[sq_client] Response to request {request}: {data}")

                err = self.get_error(data)
                if err[1] != 0:
                    future.set_exception(SQError(*err))
                else:
                    future.set_result(data.rsplit('\n', 2)[0].strip())
        finally:
            writer.close()
            await writer.wait_closed()

async def check_client_allowed(sq_client, cinfo, bot_members):
    details = await sq_client.request(f'clientinfo clid={cinfo["clid"]}')
    details = details.kvs()
    if details['client_unique_identifier'] == 'serveradmin':
        return True

    if details['client_database_id'] in bot_members:
        logger.info(f"Skipping client {details['client_nickname']}, is a bot.")
        return True
    logger.info(details)
    logger.info(f"{details['client_nickname']} has been idle for {details['client_idle_time']} ms")

    return int(details['client_idle_time']) <= (timedelta(hours=3).seconds * 1000)

async def get_client_stati(sq_client):
    clients = (await sq_client.request('clientlist')).split('|')
    details = []
    for client in clients:
        data = client.kvs()
        clid = data['clid']
        detail = (await sq_client.request(f'clientinfo clid={clid}')).kvs()
        details.append(detail)
    return details

async def handle_conn(reader, writer):
    logger.info(f"Accepted connection.")
    logger.info("Sending status...")
    data = await get_client_stati(sq_client)
    data_encoded = json.dumps(data)
    writer.write(data_encoded.encode('utf-8'))
    await writer.drain()
    writer.close()
    await writer.wait_closed()
    logger.info("Done.")

async def init(config):
    global sq_client
    sq_client = await ServerQueryClient.create(host=config['host'],
                                               username=config['username'],
                                               password=config['password'])

    socket_path = '/tmp/ts-status.sock'
    if os.path.exists(socket_path):
        try:
            os.unlink(socket_path)
        except OSError:
            raise

    server = await asyncio.start_unix_server(handle_conn, path=socket_path)
    async with server:
        await server.serve_forever()


with open('config.toml') as f:
    config = toml.load(f)

asyncio.run(init(config['serverquery']))
