#!/usr/bin/env python3.7
from util import logsetup
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("kickbot.log", FORMAT, LOGLEVEL)

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 + f' (errno: {errno})')
        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 kick_idlers(sq_client):
    bot_members = (await sq_client.request('servergroupclientlist sgid=28')).split('|')
    bot_members = [member.kvs()['cldbid'] for member in bot_members]
    clients = (await sq_client.request('clientlist')).split('|')
    for client in clients:
        info = client.kvs()
        clid = info['clid']
        allowed = await check_client_allowed(sq_client, client.kvs(), bot_members)
        if not allowed:
            try:
                await sq_client.request(fr'clientmove clid={clid} cid=109')
            except SQError as e:
                if (e.errno != 770): raise

async def ask_some_questions(config):
    sq_config = config['serverquery']
    sq_client = await ServerQueryClient.create(host=sq_config['host'],
                                               username=sq_config['username'],
                                               password=sq_config['password'])

    await sq_client.request('clientupdate client_nickname=AFKBot')

    while True:
        await asyncio.sleep(600)
        await kick_idlers(sq_client)

    await sq_client

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

asyncio.run(ask_some_questions(config))
