from typing import Dict, List, cast


async def run_app():
    import asyncio
    import datetime
    import json
    import logging
    import time
    import traceback

    import aiohttp.web
    import jwt
    import nest_asyncio
    from multidict import MultiDict
    from passlib.hash import pbkdf2_sha512
    from requests_toolbelt.multipart import decoder

    from nawah import data as Data
    from nawah.classes import (ATTR, IP_QUOTA, NAWAH_ENV, ConvertAttrException,
                               InvalidAttrException, JSONEncoder)
    from nawah.config import Config
    from nawah.enums import Event
    from nawah.utils import (_compile_anon_session, _compile_anon_user,
                             _config_data, _import_modules, _process_file_obj,
                             validate_doc)

    nest_asyncio.apply()

    logger = logging.getLogger('nawah')

    # [DOC] Use import_modules to load and initialise modules
    await _import_modules()
    await _config_data()
    # [DOC] Populate get_routes, post_routes
    get_routes = []
    post_routes = []
    for module in Config.modules.values():
        for method in module.methods.values():
            if method.get_method:
                for get_args_set in method.query_args:
                    if get_args_set:
                        get_args = f'/{{{"}/{".join(list(get_args_set.keys()))}}}'
                    else:
                        get_args = ''

                    get_routes.append(
                        f'/{module.module_name}/{method._callable.method}{get_args}'
                    )
            elif method.post_method:
                for post_args_set in method.query_args:
                    if post_args_set:
                        post_args = f'/{{{"}/{".join(list(post_args_set.keys()))}}}'
                    else:
                        post_args = ''

                    post_routes.append(
                        f'/{module.module_name}/{method._callable.method}{post_args}'
                    )

    logger.debug(
        'Loaded modules: %s',
        {module: Config.modules[module].attrs for module in Config.modules.keys()},
    )
    logger.debug(
        'Config has attrs: %s',
        {
            k: str(v)
            for k, v in Config.__dict__.items()
            if not type(v) == classmethod and not k.startswith('_')
        },
    )
    logger.debug(f'Generated get_routes: {get_routes}')
    logger.debug(f'Generated post_routes: {post_routes}')

    sessions: List[NAWAH_ENV] = []
    ip_quota: Dict[str, IP_QUOTA] = {}

    async def not_found_handler(request):
        headers = MultiDict(
            [
                ('Server', 'Nawah'),
                ('Powered-By', 'Nawah, https://nawah.masaar.com'),
                ('Access-Control-Allow-Origin', '*'),
                ('Access-Control-Allow-Methods', 'GET,POST'),
                ('Access-Control-Allow-Headers', 'Content-Type'),
                ('Access-Control-Expose-Headers', 'Content-Disposition'),
            ]
        )
        return aiohttp.web.Response(
            status=404,
            headers=headers,
            body=JSONEncoder().encode({'status': 404, 'msg': '404 NOT FOUND'}),
        )

    async def not_allowed_handler(request):
        headers = MultiDict(
            [
                ('Server', 'Nawah'),
                ('Powered-By', 'Nawah, https://nawah.masaar.com'),
                ('Access-Control-Allow-Origin', '*'),
                ('Access-Control-Allow-Methods', '*'),
                ('Access-Control-Allow-Headers', 'Content-Type'),
                ('Access-Control-Expose-Headers', 'Content-Disposition'),
            ]
        )
        return aiohttp.web.Response(
            status=405,
            headers=headers,
            body=JSONEncoder().encode({'status': 405, 'msg': '404 NOT ALLOWED'}),
        )

    async def root_handler(request: aiohttp.web.Request):
        headers = MultiDict(
            [
                ('Server', 'Nawah'),
                ('Powered-By', 'Nawah, https://nawah.masaar.com'),
                ('Access-Control-Allow-Origin', '*'),
                ('Access-Control-Allow-Methods', 'GET'),
                ('Access-Control-Allow-Headers', 'Content-Type'),
                ('Access-Control-Expose-Headers', 'Content-Disposition'),
            ]
        )
        return aiohttp.web.Response(
            status=200,
            headers=headers,
            body=JSONEncoder().encode(
                {
                    'status': 200,
                    'msg': f'Welcome to {Config._app_name}!',
                    'args': {'version': Config._app_version, 'powered_by': 'Nawah'},
                }
            ),
        )

    async def http_handler(request: aiohttp.web.Request):
        headers = MultiDict(
            [
                ('Server', 'Nawah'),
                ('Powered-By', 'Nawah, https://nawah.masaar.com'),
                ('Access-Control-Allow-Origin', '*'),
                ('Access-Control-Allow-Methods', 'GET,POST,OPTIONS'),
                (
                    'Access-Control-Allow-Headers',
                    'Content-Type,X-Auth-Bearer,X-Auth-Token,X-Auth-App',
                ),
                ('Access-Control-Expose-Headers', 'Content-Disposition'),
            ]
        )

        logger.debug(f'Received new {request.method} request: {request.match_info}')

        if request.method == 'OPTIONS':
            return aiohttp.web.Response(
                status=200,
                headers=headers,
                body=JSONEncoder().encode(
                    {
                        'status': 200,
                        'msg': 'OPTIONS request is allowed.',
                    }
                ),
            )

        # [DOC] Check for IP quota
        if str(request.remote) not in ip_quota:
            ip_quota[str(request.remote)] = {
                'counter': Config.quota_ip_min,
                'last_check': datetime.datetime.utcnow(),
            }
        else:
            if (
                datetime.datetime.utcnow() - ip_quota[str(request.remote)]['last_check']
            ).seconds > 259:
                ip_quota[str(request.remote)]['last_check'] = datetime.datetime.utcnow()
                ip_quota[str(request.remote)]['counter'] = Config.quota_ip_min
            else:
                if ip_quota[str(request.remote)]['counter'] - 1 <= 0:
                    logger.warning(
                        f'Denying \'{request.method}\' request from \'{request.remote}\' for hitting IP quota.'
                    )
                    headers['Content-Type'] = 'application/json; charset=utf-8'
                    return aiohttp.web.Response(
                        status=429,
                        headers=headers,
                        body=JSONEncoder().encode(
                            {
                                'status': 429,
                                'msg': 'You have hit calls quota from this IP.',
                                'args': {'code': 'CORE_REQ_IP_QUOTA_HIT'},
                            }
                        ),
                    )
                else:
                    ip_quota[str(request.remote)]['counter'] -= 1

        module = request.url.parts[1].lower()
        method = request.url.parts[2].lower()
        request_args = dict(request.match_info.items())

        # [DOC] Extract Args Sets based on request.method
        args_sets = Config.modules[module].methods[method].query_args
        args_sets = cast(List[Dict[str, ATTR]], args_sets)

        # [DOC] Attempt to validate query as doc
        for args_set in args_sets:
            if len(args_set.keys()) == len(args_set.keys()) and sum(
                1 for arg in args_set.keys() if arg in args_set.keys()
            ) == len(args_set.keys()):
                # [DOC] Check presence and validate all attrs in doc args
                try:
                    exception: Exception
                    await validate_doc(mode='create', doc=request_args, attrs=args_set)  # type: ignore
                except InvalidAttrException as e:
                    exception = e
                    headers['Content-Type'] = 'application/json; charset=utf-8'
                    return aiohttp.web.Response(
                        status=400,
                        headers=headers,
                        body=JSONEncoder()
                        .encode(
                            {
                                'status': 400,
                                'msg': f'{str(e)} for \'{request.method}\' request on module \'{Config.modules[module].package_name.upper()}_{module.upper()}\'.',
                                'args': {
                                    'code': f'{Config.modules[module].package_name.upper()}_{module.upper()}_INVALID_ATTR'
                                },
                            }
                        )
                        .encode('utf-8'),
                    )
                except ConvertAttrException as e:
                    exception = e
                    headers['Content-Type'] = 'application/json; charset=utf-8'
                    return aiohttp.web.Response(
                        status=400,
                        headers=headers,
                        body=JSONEncoder()
                        .encode(
                            {
                                'status': 400,
                                'msg': f'{str(e)} for \'{request.method}\' request on module \'{Config.modules[module].package_name.upper()}_{module.upper()}\'.',
                                'args': {
                                    'code': f'{Config.modules[module].package_name.upper()}_{module.upper()}_CONVERT_INVALID_ATTR'
                                },
                            }
                        )
                        .encode('utf-8'),
                    )
                break

        conn = Data.create_conn()
        env: NAWAH_ENV = {
            'conn': conn,
            'REMOTE_ADDR': request.remote,
            'client_app': '__public',
        }

        try:
            env['HTTP_USER_AGENT'] = request.headers['user-agent']
            env['HTTP_ORIGIN'] = request.headers['origin']
        except:
            env['HTTP_USER_AGENT'] = ''
            env['HTTP_ORIGIN'] = ''

        if 'X-Auth-Bearer' in request.headers or 'X-Auth-Token' in request.headers:
            logger.debug('Detected \'X-Auth\' header[s].')
            if (
                'X-Auth-Bearer' not in request.headers
                or 'X-Auth-Token' not in request.headers
                or 'X-Auth-App' not in request.headers
            ):
                logger.debug('Denying request due to missing \'X-Auth\' header.')
                headers['Content-Type'] = 'application/json; charset=utf-8'
                return aiohttp.web.Response(
                    status=400,
                    headers=headers,
                    body=JSONEncoder()
                    .encode(
                        {
                            'status': 400,
                            'msg': 'One \'X-Auth\' headers was set but not the other.',
                        }
                    )
                    .encode('utf-8'),
                )
            # [TODO] Add condition to now allow a connection from a client with client_app = __sys
            if len(Config.client_apps.keys()) and (
                request.headers['X-Auth-App'] not in Config.client_apps.keys()
                or (
                    Config.client_apps[request.headers['X-Auth-App']]['type'] == 'web'
                    and env['HTTP_ORIGIN']
                    not in Config.client_apps[request.headers['X-Auth-App']]['origin']
                )
            ):
                logger.debug('Denying request due to unauthorised client_app.')
                headers['Content-Type'] = 'application/json; charset=utf-8'
                return aiohttp.web.Response(
                    status=403,
                    headers=headers,
                    body=JSONEncoder()
                    .encode(
                        {
                            'status': 403,
                            'msg': 'X-Auth headers could not be verified.',
                            'args': {'code': 'CORE_SESSION_INVALID_XAUTH'},
                        }
                    )
                    .encode('utf-8'),
                )
            try:
                session_results = await Config.modules['session'].read(
                    skip_events=[Event.PERM],
                    env=env,
                    query=[
                        {
                            '_id': request.headers['X-Auth-Bearer'],
                        }
                    ],
                )
            except:
                headers['Content-Type'] = 'application/json; charset=utf-8'
                if Config.debug:
                    return aiohttp.web.Response(
                        status=500,
                        headers=headers,
                        body=JSONEncoder()
                        .encode(
                            {
                                'status': 500,
                                'msg': f'Unexpected error has occurred [{str(exception)}].',
                                'args': {
                                    'code': 'CORE_SERVER_ERROR',
                                    'err': str(exception),
                                },
                            }
                        )
                        .encode('utf-8'),
                    )
                else:
                    return aiohttp.web.Response(
                        status=500,
                        headers=headers,
                        body=JSONEncoder()
                        .encode(
                            {
                                'status': 500,
                                'msg': 'Unexpected error has occurred.',
                                'args': {'code': 'CORE_SERVER_ERROR'},
                            }
                        )
                        .encode('utf-8'),
                    )

            if not session_results['args']['count'] or not pbkdf2_sha512.verify(
                request.headers['X-Auth-Token'],
                session_results['args']['docs'][0]['token_hash'],
            ):
                logger.debug(
                    'Denying request due to missing failed Call Authorisation.'
                )
                headers['Content-Type'] = 'application/json; charset=utf-8'
                return aiohttp.web.Response(
                    status=403,
                    headers=headers,
                    body=JSONEncoder()
                    .encode(
                        {
                            'status': 403,
                            'msg': 'X-Auth headers could not be verified.',
                            'args': {'code': 'CORE_SESSION_INVALID_XAUTH'},
                        }
                    )
                    .encode('utf-8'),
                )
            else:
                session = session_results['args']['docs'][0]
                session_results = await Config.modules['session'].reauth(
                    skip_events=[Event.PERM],
                    env=env,
                    query=[
                        {
                            '_id': request.headers['X-Auth-Bearer'],
                            'token': request.headers['X-Auth-Token'],
                        }
                    ],
                )
                logger.debug('Denying request due to fail to reauth.')
                if session_results['status'] != 200:
                    headers['Content-Type'] = 'application/json; charset=utf-8'
                    return aiohttp.web.Response(
                        status=403,
                        headers=headers,
                        body=JSONEncoder().encode(session_results).encode('utf-8'),
                    )
                else:
                    session = session_results['args']['session']
        else:
            anon_user = _compile_anon_user()
            anon_session = _compile_anon_session()
            anon_session['user'] = anon_user
            session = anon_session

        env['session'] = session

        doc_content = await request.content.read()
        try:
            doc = json.loads(doc_content)
        except:
            try:
                multipart_content_type = request.headers['Content-Type']
                doc = {
                    part.headers[b'Content-Disposition']
                    .decode('utf-8')
                    .replace('form-data; name=', '')
                    .replace('"', '')
                    .split(';')[0]: part.content
                    for part in decoder.MultipartDecoder(
                        doc_content, multipart_content_type
                    ).parts
                }
            except Exception as e:
                doc = {}

        results = await Config.modules[module].methods[method](
            env=env, query=[request_args], doc=doc
        )

        logger.debug('Closing connection.')
        env['conn'].close()

        if 'return' not in results['args'] or results['args']['return'] == 'json':
            if 'return' in results['args']:
                del results['args']['return']
            headers['Content-Type'] = 'application/json; charset=utf-8'
            if results['status'] == 404:
                return aiohttp.web.Response(
                    status=results['status'],
                    headers=headers,
                    body=JSONEncoder()
                    .encode({'status': 404, 'msg': 'Requested content not found.'})
                    .encode('utf-8'),
                )
            return aiohttp.web.Response(
                status=results['status'],
                headers=headers,
                body=JSONEncoder().encode(results),
            )
        elif results['args']['return'] == 'file':
            del results['args']['return']
            expiry_time = datetime.datetime.utcnow() + datetime.timedelta(days=30)
            headers['lastModified'] = str(results['args']['docs'][0]['lastModified'])
            headers['Content-Type'] = results['args']['docs'][0]['type']
            headers['Cache-Control'] = 'public, max-age=31536000'
            headers['Expires'] = expiry_time.strftime('%a, %d %b %Y %H:%M:%S GMT')
            return aiohttp.web.Response(
                status=results['status'],
                headers=headers,
                body=results['args']['docs'][0]['content'],
            )
        elif results['args']['return'] == 'msg':
            del results['args']['return']
            headers['Content-Type'] = 'application/json; charset=utf-8'
            return aiohttp.web.Response(
                status=results['status'], headers=headers, body=results['msg']
            )

        headers['Content-Type'] = 'application/json; charset=utf-8'
        return aiohttp.web.Response(
            status=405,
            headers=headers,
            body=JSONEncoder().encode({'status': 405, 'msg': 'METHOD NOT ALLOWED'}),
        )

    async def websocket_handler(request: aiohttp.web.Request):
        conn = Data.create_conn()
        logger.info(
            f'Websocket connection starting with client at \'{request.remote}\''
        )
        ws = aiohttp.web.WebSocketResponse()
        await ws.prepare(request)

        env: NAWAH_ENV = {
            'id': len(sessions),
            'conn': conn,
            'REMOTE_ADDR': request.remote,
            'ws': ws,
            'watch_tasks': {},
            'init': False,
            'last_call': datetime.datetime.utcnow(),
            'quota': {
                'counter': Config.quota_anon_min,
                'last_check': datetime.datetime.utcnow(),
            },
        }
        sessions.append(env)
        try:
            env['HTTP_USER_AGENT'] = request.headers['user-agent']
            env['HTTP_ORIGIN'] = request.headers['origin']
        except:
            env['HTTP_USER_AGENT'] = ''
            env['HTTP_ORIGIN'] = ''

        logger.info(
            f'Websocket connection #\'{env["id"]}\' ready with client at \'{env["REMOTE_ADDR"]}\''
        )

        await ws.send_str(
            JSONEncoder().encode(
                {
                    'status': 200,
                    'msg': 'Connection ready',
                    'args': {'code': 'CORE_CONN_READY'},
                }
            )
        )

        async for msg in ws:
            if 'conn' not in env:
                await ws.close()
                break
            logger.info(
                f'Received new message from session #\'{env["id"]}\': {msg.data[:256]}'
            )
            if msg.type == aiohttp.WSMsgType.TEXT:
                logger.info(f'ip_quota on session #\'{env["id"]}\': {ip_quota}')
                logger.info(
                    f'session_quota: on session #\'{env["id"]}\': {env["quota"]}'
                )
                # [DOC] Check for IP quota
                if str(request.remote) not in ip_quota:
                    ip_quota[str(request.remote)] = {
                        'counter': Config.quota_ip_min,
                        'last_check': datetime.datetime.utcnow(),
                    }
                else:
                    if (
                        datetime.datetime.utcnow()
                        - ip_quota[str(request.remote)]['last_check']
                    ).seconds > 59:
                        ip_quota[str(request.remote)][
                            'last_check'
                        ] = datetime.datetime.utcnow()
                        ip_quota[str(request.remote)]['counter'] = Config.quota_ip_min
                    else:
                        if ip_quota[str(request.remote)]['counter'] - 1 <= 0:
                            logger.warning(
                                f'Denying Websocket request from \'{request.remote}\' for hitting IP quota.'
                            )
                            asyncio.create_task(
                                handle_msg(
                                    env=env,
                                    msg=msg,
                                    decline_quota='ip',
                                )
                            )
                            continue
                        else:
                            ip_quota[str(request.remote)]['counter'] -= 1
                # [DOC] Check for session quota
                if (
                    datetime.datetime.utcnow() - env['quota']['last_check']
                ).seconds > 59:
                    env['quota']['last_check'] = datetime.datetime.utcnow()
                    env['quota']['counter'] = (
                        (Config.quota_anon_min - 1)
                        if not env['session']
                        or env['session']['token'] == Config.anon_token
                        else (Config.quota_auth_min - 1)
                    )
                    asyncio.create_task(handle_msg(env=env, msg=msg))
                else:
                    if env['quota']['counter'] - 1 <= 0:
                        asyncio.create_task(
                            handle_msg(
                                env=env,
                                msg=msg,
                                decline_quota='session',
                            )
                        )
                        continue
                    else:
                        env['quota']['counter'] -= 1
                        asyncio.create_task(handle_msg(env=env, msg=msg))

        if 'id' in env.keys():
            await close_session(env['id'])

        return ws

    async def handle_msg(
        env: NAWAH_ENV,
        msg: aiohttp.WSMessage,
        decline_quota: str = None,
    ):
        try:
            env['last_call'] = datetime.datetime.utcnow()
            try:
                env['session']['token']
            except Exception:
                anon_user = _compile_anon_user()
                anon_session = _compile_anon_session()
                anon_session['user'] = anon_user
                env['session'] = anon_session
            res = json.loads(msg.data)
            try:
                res = jwt.decode(
                    res['token'], env['session']['token'], algorithms=['HS256']
                )
            except Exception:
                try:
                    res = jwt.decode(
                        res['token'], env['prev_session']['token'], algorithms=['HS256']
                    )
                except Exception:
                    await env['ws'].send_str(
                        JSONEncoder().encode(
                            {
                                'status': 403,
                                'msg': 'Request token is not accepted.',
                                'args': {
                                    'call_id': res['call_id']
                                    if 'call_id' in res.keys()
                                    else None,
                                    'code': 'CORE_REQ_INVALID_TOKEN',
                                },
                            }
                        )
                    )
                    if env['init'] == False:
                        await env['ws'].close()
                        return
                    else:
                        return

            # [DOC] Check if msg should be denied for quota hit
            if decline_quota == 'ip':
                await env['ws'].send_str(
                    JSONEncoder().encode(
                        {
                            'status': 429,
                            'msg': 'You have hit calls quota from this IP.',
                            'args': {
                                'call_id': res['call_id']
                                if 'call_id' in res.keys()
                                else None,
                                'code': 'CORE_REQ_IP_QUOTA_HIT',
                            },
                        }
                    )
                )
                return
            elif decline_quota == 'session':
                await env['ws'].send_str(
                    JSONEncoder().encode(
                        {
                            'status': 429,
                            'msg': 'You have hit calls quota.',
                            'args': {
                                'call_id': res['call_id']
                                if 'call_id' in res.keys()
                                else None,
                                'code': 'CORE_REQ_SESSION_QUOTA_HIT',
                            },
                        }
                    )
                )
                return

            logger.info(f'Decoded request: {JSONEncoder().encode(res)}')

            if 'endpoint' not in res.keys():
                await env['ws'].send_str(
                    JSONEncoder().encode(
                        {
                            'status': 400,
                            'msg': 'Request missing endpoint.',
                            'args': {
                                'call_id': res['call_id']
                                if 'call_id' in res.keys()
                                else None,
                                'code': 'CORE_REQ_NO_ENDPOINT',
                            },
                        }
                    )
                )
                return

            if env['init'] == False:
                if res['endpoint'] != 'conn/verify':
                    await env['ws'].send_str(
                        JSONEncoder().encode(
                            {
                                'status': 1008,
                                'msg': 'Request token is not accepted.',
                                'args': {
                                    'call_id': res['call_id']
                                    if 'call_id' in res.keys()
                                    else None,
                                    'code': 'CORE_REQ_NO_VERIFY',
                                },
                            }
                        )
                    )
                    await env['ws'].close()
                    return
                else:
                    # [TODO] Add condition to now allow a connection from a client with client_app = __sys
                    if len(Config.client_apps.keys()) and (
                        'doc' not in res.keys()
                        or 'app' not in res['doc'].keys()
                        or res['doc']['app'] not in Config.client_apps.keys()
                        or (
                            Config.client_apps[res['doc']['app']]['type'] == 'web'
                            and env['HTTP_ORIGIN']
                            not in Config.client_apps[res['doc']['app']]['origin']
                        )
                    ):
                        await env['ws'].send_str(
                            JSONEncoder().encode(
                                {
                                    'status': 1008,
                                    'msg': 'Request token is not accepted.',
                                    'args': {
                                        'call_id': res['call_id']
                                        if 'call_id' in res.keys()
                                        else None,
                                        'code': 'CORE_REQ_NO_VERIFY',
                                    },
                                }
                            )
                        )
                        await env['ws'].close()
                        return
                    else:
                        env['init'] = True
                        # [TODO] Add condition to now allow a connection from a client with client_app = __sys
                        if not Config.client_apps:
                            env['client_app'] = '__public'
                        else:
                            env['client_app'] = res['doc']['app']
                        logger.info(
                            f'Connection on session #\'{env["id"]}\' is verified.'
                        )
                        if Config.analytics_events['app_conn_verified']:
                            asyncio.create_task(
                                Config.modules['analytic'].create(
                                    skip_events=[Event.PERM],
                                    env=env,
                                    doc={
                                        'event': 'CONN_VERIFIED',
                                        'subevent': env['client_app'],
                                        'args': {
                                            'REMOTE_ADDR': env['REMOTE_ADDR'],
                                            'HTTP_USER_AGENT': env['HTTP_USER_AGENT'],
                                        },
                                    },
                                )
                            )
                        await env['ws'].send_str(
                            JSONEncoder().encode(
                                {
                                    'status': 200,
                                    'msg': 'Connection established',
                                    'args': {
                                        'call_id': res['call_id']
                                        if 'call_id' in res.keys()
                                        else None,
                                        'code': 'CORE_CONN_OK',
                                    },
                                }
                            )
                        )
                        return

            if res['endpoint'] == 'conn/close':
                logger.info(
                    f'Received connection close instructions on session #\'{env["id"]}\'.'
                )
                await env['ws'].close()
                return

            if res['endpoint'] == 'heart/beat':
                logger.info(
                    f'Received connection heartbeat on session #\'{env["id"]}\'.'
                )
                await env['ws'].send_str(
                    JSONEncoder().encode(
                        {
                            'status': 200,
                            'msg': 'Heartbeat received.',
                            'args': {
                                'call_id': res['call_id']
                                if 'call_id' in res.keys()
                                else None,
                                'code': 'CORE_HEARTBEAT_OK',
                            },
                        }
                    )
                )
                return

            res['endpoint'] = res['endpoint'].lower()

            if (
                res['endpoint'] in ['session/auth', 'session/reauth']
                and str(env['session']['_id']) != 'f00000000000000000000012'
            ):
                await env['ws'].send_str(
                    JSONEncoder().encode(
                        {
                            'status': 400,
                            'msg': 'You are already authed.',
                            'args': {
                                'call_id': res['call_id']
                                if 'call_id' in res.keys()
                                else None,
                                'code': 'CORE_SESSION_ALREADY_AUTHED',
                            },
                        }
                    )
                )
                return

            if (
                res['endpoint'] == 'session/signout'
                and str(env['session']['_id']) == 'f00000000000000000000012'
            ):
                await env['ws'].send_str(
                    JSONEncoder().encode(
                        {
                            'status': 400,
                            'msg': 'Singout is not allowed for \'__ANON\' user.',
                            'args': {
                                'call_id': res['call_id']
                                if 'call_id' in res.keys()
                                else None,
                                'code': 'CORE_SESSION_ANON_SIGNOUT',
                            },
                        }
                    )
                )
                return

            if 'query' not in res.keys():
                res['query'] = []
            if 'doc' not in res.keys():
                res['doc'] = {}
            if 'call_id' not in res.keys():
                res['call_id'] = ''

            request = {
                'call_id': res['call_id'],
                'sid': res['sid'] or False,
                'query': res['query'],
                'doc': res['doc'],
                'path': res['endpoint'].split('/'),
            }

            if len(request['path']) != 2:
                await env['ws'].send_str(
                    JSONEncoder().encode(
                        {
                            'status': 400,
                            'msg': 'Endpoint path is invalid.',
                            'args': {
                                'call_id': request['call_id'],
                                'code': 'CORE_REQ_INVALID_PATH',
                            },
                        }
                    )
                )
                return

            module = request['path'][0].lower()

            if module == 'watch' and request['path'][1].lower() == 'delete':
                logger.info(
                    'Received watch task delete request for: %s',
                    request['query'][0]['watch'],
                )
                try:
                    if request['query'][0]['watch'] == '__all':
                        for watch_task in env['watch_tasks'].values():
                            watch_task['stream'].close()
                            watch_task['task'].cancel()
                        await env['ws'].send_str(
                            JSONEncoder().encode(
                                {
                                    'status': 200,
                                    'msg': 'All watch tasks deleted.',
                                    'args': {
                                        'call_id': request['call_id'],
                                        'watch': list(env['watch_tasks'].keys()),
                                    },
                                }
                            )
                        )
                        env['watch_tasks'] = {}
                    else:
                        env['watch_tasks'][request['query'][0]['watch']][
                            'stream'
                        ].close()
                        env['watch_tasks'][request['query'][0]['watch']][
                            'task'
                        ].cancel()
                        await env['ws'].send_str(
                            JSONEncoder().encode(
                                {
                                    'status': 200,
                                    'msg': 'Watch task deleted.',
                                    'args': {
                                        'call_id': request['call_id'],
                                        'watch': [request['query'][0]['watch']],
                                    },
                                }
                            )
                        )
                        del env['watch_tasks'][request['query'][0]['watch']]
                except:
                    await env['ws'].send_str(
                        JSONEncoder().encode(
                            {
                                'status': 400,
                                'msg': 'Watch is invalid.',
                                'args': {
                                    'call_id': request['call_id'],
                                    'code': 'CORE_WATCH_INVALID_WATCH',
                                },
                            }
                        )
                    )
                return

            if module not in Config.modules.keys():
                await env['ws'].send_str(
                    JSONEncoder().encode(
                        {
                            'status': 400,
                            'msg': 'Endpoint module is invalid.',
                            'args': {
                                'call_id': request['call_id'],
                                'code': 'CORE_REQ_INVALID_MODULE',
                            },
                        }
                    )
                )
                return

            if request['path'][1].lower() not in Config.modules[module].methods.keys():
                await env['ws'].send_str(
                    JSONEncoder().encode(
                        {
                            'status': 400,
                            'msg': 'Endpoint method is invalid.',
                            'args': {
                                'call_id': request['call_id'],
                                'code': 'CORE_REQ_INVALID_METHOD',
                            },
                        }
                    )
                )
                return

            if Config.modules[module].methods[request['path'][1].lower()].get_method:
                await env['ws'].send_str(
                    JSONEncoder().encode(
                        {
                            'status': 400,
                            'msg': 'Endpoint method is a GET method.',
                            'args': {
                                'call_id': request['call_id'],
                                'code': 'CORE_REQ_GET_METHOD',
                            },
                        }
                    )
                )
                return

            if not request['sid']:
                request['sid'] = 'f00000000000000000000012'

            method = Config.modules[module].methods[request['path'][1].lower()]
            query = request['query']
            doc = request['doc']
            await _process_file_obj(doc=doc, modules=Config.modules, env=env)
            asyncio.create_task(
                method(
                    skip_events=[],
                    env=env,
                    query=query,
                    doc=doc,
                    call_id=request['call_id'],
                )
            )

        except Exception as e:
            logger.error(f'An error occurred. Details: {traceback.format_exc()}.')
            if Config.debug:
                await env['ws'].send_str(
                    JSONEncoder().encode(
                        {
                            'status': 500,
                            'msg': f'Unexpected error has occurred [{str(e)}].',
                            'args': {'code': 'CORE_SERVER_ERROR', 'err': str(e)},
                        }
                    )
                )
            else:
                await env['ws'].send_str(
                    JSONEncoder().encode(
                        {
                            'status': 500,
                            'msg': 'Unexpected error has occurred.',
                            'args': {'code': 'CORE_SERVER_ERROR'},
                        }
                    )
                )

    async def close_session(id):
        if sessions[id].keys():
            logger.info(
                f'Cleaning up watch tasks before connection for session #\'{sessions[id]["id"]}\' close.'
            )
            for watch_task in sessions[id]['watch_tasks'].values():
                try:
                    await watch_task['stream'].close()
                except Exception as e:
                    logger.error(f'stream close error: {e}')
                try:
                    watch_task['task'].cancel()
                except Exception as e:
                    logger.error(f'task close error: {e}')

            logger.info(
                f'Closing data connection for session #\'{sessions[id]["id"]}\''
            )
            sessions[id]['conn'].close()

            logger.info('Done closing data connection.')
            logger.info(f'Websocket connection status: {not sessions[id]["ws"].closed}')

            if not sessions[id]['ws'].closed:
                await sessions[id]['ws'].close()
            logger.info(f'Websocket connection for session #\'{id}\' closed.')

            sessions[id] = {}
        else:
            logger.info(f'Skipped closing session #\'{id}\'.')

    async def jobs_loop():
        while True:
            await asyncio.sleep(60)
            try:
                # [DOC] Connection Timeout Workflow
                logger.info('Time to check for sessions!')
                logger.info(f'Current sessions: {sessions}')
                for i in range(len(sessions)):
                    session = sessions[i]
                    if 'last_call' not in session.keys():
                        continue
                    if datetime.datetime.utcnow() > (
                        session['last_call']
                        + datetime.timedelta(seconds=Config.conn_timeout)
                    ):
                        logger.info(
                            f'Session #\'{session["id"]}\' with REMOTE_ADDR \'{session["REMOTE_ADDR"]}\' HTTP_USER_AGENT: \'{session["HTTP_USER_AGENT"]}\' is idle. Closing.'
                        )
                        await close_session(i)
            except Exception:
                logger.error(f'An error occurred. Details: {traceback.format_exc()}.')

            try:
                # [DOC] Calls Quota Workflow - Clean-up Sequence
                logger.info('Time to check for IPs quotas!')
                del_ip_quota = []
                for ip in ip_quota.keys():
                    if (
                        datetime.datetime.utcnow() - ip_quota[ip]['last_check']
                    ).seconds > 59:
                        logger.info(
                            f'IP \'{ip}\' with quota \'{ip_quota[ip]["counter"]}\' is idle. Cleaning-up.'
                        )
                        del_ip_quota.append(ip)
                for ip in del_ip_quota:
                    del ip_quota[ip]
            except Exception:
                logger.error(f'An error occurred. Details: {traceback.format_exc()}.')

            try:
                # [DOC] Jobs Workflow
                current_time = datetime.datetime.utcnow().isoformat()[:16]
                logger.info('Time to check for jobs!')
                for job_name in Config.jobs:
                    job = Config.jobs[job_name]
                    logger.info(f'Checking: {job_name}')
                    if job._disabled:
                        logger.info('-Job is disabled. Skipping..')
                        continue
                    # [DOC] Check if job is scheduled for current_time
                    if current_time == job._next_time:
                        logger.info('-Job is due, running!')
                        # [DOC] Update job next_time
                        job._next_time = datetime.datetime.fromtimestamp(
                            job._cron_schedule.get_next(), datetime.timezone.utc
                        ).isoformat()[:16]
                        try:
                            job.job(env=Config._sys_env)
                        except Exception as e:
                            logger.error(
                                'Job \'{job_name}\' has failed with exception.'
                            )
                            logger.error('Exception details:')
                            logger.error(traceback.format_exc())
                            if job.prevent_disable:
                                logger.warning(
                                    '-Detected job prevent_disable. Skipping disabling job..'
                                )
                            else:
                                logger.warning('-Disabling job.')
                                job._disabled = True
                    else:
                        logger.info('-Not yet due.')
            except Exception:
                logger.error(f'An error occurred. Details: {traceback.format_exc()}.')

            try:
                logger.info('Time to check for files timeout!')
                files_results = await Config.modules['file'].delete(
                    skip_events=[Event.PERM],
                    env=Config._sys_env,
                    query=[
                        {
                            'create_time': {
                                '$lt': (
                                    datetime.datetime.utcnow()
                                    - datetime.timedelta(
                                        seconds=Config.file_upload_timeout
                                    )
                                ).isoformat()
                            }
                        }
                    ],
                )
                logger.info('Files timeout results:')
                logger.info(f'-status: {files_results["status"]}')
                logger.info(f'-msg: {files_results["msg"]}')
                logger.info(f'-args.docs: {files_results["args"]["docs"]}')
            except Exception:
                logger.error(f'An error occurred. Details: {traceback.format_exc()}.')

    def create_error_middleware(overrides):
        @aiohttp.web.middleware
        async def error_middleware(request, handler):
            try:
                response = await handler(request)
                override = overrides.get(response.status)
                if override:
                    return await override(request)
                return response
            except aiohttp.web.HTTPException as ex:
                override = overrides.get(ex.status)
                if override:
                    return await override(request)
                raise

        return error_middleware

    async def web_loop():
        app = aiohttp.web.Application()
        app.middlewares.append(
            create_error_middleware(
                {
                    404: not_found_handler,
                    405: not_allowed_handler,
                }
            )
        )
        app.router.add_route('GET', '/', root_handler)
        app.router.add_route('*', '/ws', websocket_handler)
        for route in get_routes:
            app.router.add_route('GET', route, http_handler)
        for route in post_routes:
            app.router.add_route('POST', route, http_handler)
            app.router.add_route('OPTIONS', route, http_handler)
        logger.info('Welcome to Nawah')
        await aiohttp.web.run_app(app, host='0.0.0.0', port=Config.port)

    async def loop_gather():
        await asyncio.gather(jobs_loop(), web_loop())

    try:
        asyncio.run(loop_gather())
    except KeyboardInterrupt:
        if time.localtime().tm_hour >= 21 or time.localtime().tm_hour <= 4:
            msg = 'night'
        elif time.localtime().tm_hour >= 18:
            msg = 'evening'
        elif time.localtime().tm_hour >= 12:
            msg = 'afternoon'
        elif time.localtime().tm_hour >= 5:
            msg = 'morning'
        logger.info(f'Have a great {msg}!')
        exit()
