'''Provides utilities required for configuring modules, packages and app'''

import datetime
import logging
import os
import re
from typing import TYPE_CHECKING, Any, MutableMapping, MutableSequence, cast

import requests
import sentry_sdk
from bson import ObjectId
from croniter import croniter
from passlib.hash import pbkdf2_sha512
from redis import asyncio as aioredis  # type: ignore

from nawah.classes import (Attr, Default, Diff, Extn, Func, Module, SysDoc,
                           UserSetting)
from nawah.config import Config
from nawah.enums import AttrType, Event
from nawah.exceptions import (ConfigException, InvalidAttrTypeArgException,
                              InvalidAttrTypeException)

from ._attr_type import generate_attr_val
from ._call import call
from ._validate._type import validate_type
from ._validate._user_setting import validate_user_setting

if TYPE_CHECKING:
    from nawah.types import NawahDoc

logger = logging.getLogger('nawah')


def extract_attr(*, attrs: MutableMapping[str, 'Attr'], path: str) -> 'Attr':
    '''Extracts attr using its 'path' from 'attrs'. Attr path considers different Attr Types. For
    instance, to get child attr of Attr Type LIST, UNION, use index of child attr, to get 'key',
    'val' attrs of Attr Type KV_DICT use the arg name, to get child attr of Attr Type TYPED_DICT
    use key from 'dict' arg'''

    scope: MutableMapping[str, 'Attr'] | MutableSequence['Attr'] = attrs
    path_part: int | str
    path_parts = path.split('.')
    _iter = enumerate(path_parts)
    for _, path_part in _iter:
        if re.match('^[0-9]+$', cast(str, path_part)):
            path_part = int(path_part)
            path_part_check = path_part in range(len(scope))
        else:
            path_part_check = path_part in scope

        if not path_part_check:
            raise ValueError(
                f'Attr \'{path_part}\' does not exist in scope \'{scope}\''
            )

        scope_attr = cast(MutableMapping[str, 'Attr'], scope)[cast(str, path_part)]

        while scope_attr.type in [AttrType.LIST, AttrType.UNION]:
            try:
                _, path_part = next(_iter)
            except StopIteration:
                return scope_attr

            arg = 'list' if scope_attr.type == AttrType.LIST else 'union'
            scope_attr = scope_attr.args[arg][int(path_part)]

        while scope_attr.type == AttrType.KV_DICT:
            try:
                _, path_part = next(_iter)
            except StopIteration:
                return scope_attr

            scope_attr = scope_attr.args[path_part]

        while scope_attr.type == AttrType.TYPED_DICT:
            try:
                _, path_part = next(_iter)
            except StopIteration:
                return scope_attr

            scope_attr = scope_attr.args['dict'][path_part]


        match scope_attr.type:
            case AttrType.LIST:
                scope = scope_attr.args['list']
            case AttrType.UNION:
                scope = scope_attr.args['union']
            case AttrType.KV_DICT:
                scope = scope_attr.args['val']
            case AttrType.TYPED_DICT:
                scope = scope_attr.args['dict']

    return scope_attr


def config_module(*, module_name: str, module: Module):
    '''Validates args for 'module' and updates values for 'attrs' of 'module' per sets from
    'defaults', 'extns', 'counters' '''

    # pylint: disable=import-outside-toplevel

    import nawah.base as Base

    # Set name of module in Module object for reverse reference
    module.name = module_name

    logger.debug('Attempting to configure module \'%s\'', module_name)

    if not module.funcs:
        raise ConfigException(
            f'Module \'{module_name}\' doesn\'t define any function'
        )

    for func_name, func in module.funcs.items():
        func.name = func_name
        func.module = module
        # Check value type
        if not isinstance(func, Func):
            raise ConfigException(
                f'Invalid Nawah Function for \'{module_name}\'.\'{func_name}\' of type '
                '\'{type(func)}\''
            )
        # If Func has callable defined skip
        if func.callable:
            if not callable(func.callable):
                raise ConfigException(
                    f'Value \'callable\' for Nawah Function \'{func_name}\' of \'{module_name}\' '
                    'is not callable'
                )
            continue

        if func_name.startswith('create_file'):
            func.callable = Base.create_file

        elif func_name.startswith('delete_file'):
            func.callable = Base.delete_file

        elif func_name.startswith('delete_lock'):
            func.callable = Base.delete_lock

        elif func_name.startswith('obtain_lock'):
            func.callable = Base.obtain_lock

        elif func_name.startswith('read_diff'):
            func.callable = Base.read_diff

        elif func_name.startswith('retrieve_file'):
            func.callable = Base.retrieve_file

        elif func_name.startswith('read'):
            func.callable = Base.read

        elif func_name.startswith('create'):
            func.callable = Base.create

        elif func_name.startswith('update'):
            func.callable = Base.update

        elif func_name.startswith('delete'):
            func.callable = Base.delete

        else:
            raise ConfigException(
                f'Can\'t set callable for Nawah Function \'{func_name}\' of'
                f' \'{module_name}\''
            )

    # Check attrs for any invalid type
    for attr in module.attrs:
        try:
            logger.debug(
                'Attempting to validate Attr Type for \'%s\'.\'%s\'',
                module_name,
                attr,
            )
            validate_type(attr_type=module.attrs[attr])
        except InvalidAttrTypeException as e:
            raise ConfigException(
                f'Invalid Attr Type for \'{module_name}\'.\'{attr}\'. Original '
                'validation error: {str(e)}',
            ) from e
        except InvalidAttrTypeArgException as e:
            raise ConfigException(
                f'Invalid Attr Type Arg for \'{module_name}\'.\'{attr}\'. Original '
                'validation error: {str(e)}'
            ) from e

    # Check default for invalid types, update default value
    for attr_name, default in module.defaults.items():
        if not isinstance(default, Default):
            raise ConfigException(
                    f'Invalid Default Instruction for \'{module_name}\'.\'{attr_name}\', of type '
                    '\'{type(default)}\''
            )

        logger.debug(
            'Updating default value for attr \'%s\' to: \'%s\'',
            attr_name,
            module.defaults[attr_name],
        )
        extract_attr(attrs=module.attrs, path=attr_name).default = module.defaults[attr_name]

    # Update extn value
    for attr_name, extn in module.extns.items():
        if not isinstance(extn, Extn):
            raise ConfigException(
                f'Invalid Extension Instruction for \'{module_name}\'.\'{attr_name}\', of type '
                '\'{type(extn)}\''
            )

        logger.debug(
            'Updating \'extn\' for attr \'%s\' to: \'%s\'',
            attr_name,
            module.extns[attr_name],
        )
        extract_attr(attrs=module.attrs, path=attr_name).extn = module.extns[attr_name]

    # Check extns for invalid extended attrs
    for attr_name, extn in module.extns.items():
        if not isinstance(extn, Extn):
            raise ConfigException(
                f'Invalid extns attr \'{attr_name}\' of module \'{module_name}\''
            )

    # Check valid type, value for diff
    if module.diff is not None and not isinstance(module.diff, Diff):
        raise ConfigException(f'Invalid Diff Instruction for module \'{module_name}\'')

    if isinstance(module.diff, Attr):
        module.diff = cast(Attr, module.diff)
        if module.diff.type != AttrType.TYPE:
            raise ConfigException(
                f'Invalid Attr Type for diff of module \'{module_name}\'. Only Attr Type TYPE '
                'is allowed'
            )
        logger.debug(
            'Attempting to validate Attr Type diff of module \'%s\'', module_name
        )
        try:
            validate_type(attr_type=module.diff)
        except InvalidAttrTypeException as e:
            raise ConfigException(
                f'Invalid Attr Type for diff of module \'{module_name}\'. Original validation error: {str(e)}'
            ) from e
        except InvalidAttrTypeArgException as e:
            raise ConfigException(
                f'Invalid Attr Type Arg for diff of module \'{module_name}\'. Original validation error: {str(e)}'
            ) from e

    # [TODO] Re-implement
    # Check valid types, values for create_draft, update_draft
    # for attr in ['create_draft', 'update_draft']:
    #     if type(getattr(self, attr)) not in [bool, Attr]:
    #         raise ConfigException(f'Invalid {attr} for module \'{module_name}\'') from e
    #     if type(getattr(self, attr)) == Attr:
    #         if getattr(self, attr).type != AttrType.TYPE:
    #             raise ConfigException(
    #                 f'Invalid Attr Type for {attr} of module \'{module_name}\'. Only Attr Type TYPE is allowed'
    #             ) from e
    #         logger.debug(
    #             f'Attempting to validate Attr Type {attr} of module \'{module_name}\'.'
    #         )
    #         try:
    #             validate_type(attr_type=getattr(self, attr))
    #         except InvalidAttrTypeException as e:
    #             raise ConfigException(
    #                 f'Invalid Attr Type for {attr} of module \'{module_name}\'. Original validation error: {str(e)}'
    #             ) from e
    #         except InvalidAttrTypeArgException as e:
    #             raise ConfigException(
    #                 f'Invalid Attr Type Arg for {attr} of module \'{module_name}\'. Original validation error: {str(e)}'
    #             ) from e

    logger.debug('Configured module %s', module_name)


async def config_app():

    # [TODO] Add validator for user_attrs, user_doc_settings
    # Check app packages
    if len(Config.sys.dependencies) > 2:
        logger.debug(
            'Found app \'dependencies\'. Attempting to validate all loaded packages are'
            'matching \'dependencies\' value'
        )

        Config.sys.packages['core'] = Config.sys.dependencies['core']
        Config.sys.packages[Config.sys.default_package] = Config.sys.dependencies[
            Config.sys.default_package
        ]

        missing_packages = [
            package
            for package in Config.sys.packages
            if package not in Config.sys.dependencies
        ]
        if missing_packages:
            raise ConfigException(
                'At least one package is missing that is required by app. Missing package[s]:'
                f'\'{", ".join(missing_packages)}\'',
            )

        extra_packages = [
            package
            for package in Config.sys.dependencies
            if package not in Config.sys.packages
        ]
        if extra_packages:
            raise ConfigException(
                'At least one extra package is present in \'packages\' folder that is not'
                f'required by app. Extra package[s]: \'{", ".join(extra_packages)}\''
            )

        # Check for version mismatch
        for package, version in Config.sys.packages.items():
            # Skip core and default_packages
            if package in ['core', Config.sys.default_package]:
                continue
            if version != Config.sys.dependencies[package]:
                raise ConfigException(
                    f'Package \'{package}\' version \'{Config.sys.dependencies[package]}\' is'
                    f'added to app but not matching required version \'{version}\''
                )

    # Check API version
    nawah_level = '.'.join(Config.sys.version.split('.')[0:2])
    for package, api_level in {
        name: config['package'].api_level
        for name, config in Config.sys.packages.items()
    }.items():
        if api_level != nawah_level:
            raise ConfigException(
                f'Nawah framework is on API-level \'{nawah_level}\', but the app package'
                f'\'{package}\' requires API-level \'{api_level}\''
            )
    try:
        versions = (
            (
                requests.get(
                    'https://raw.githubusercontent.com/masaar/nawah_versions/master/versions.txt'
                ).content
            )
            .decode('utf-8')
            .split('\n')
        )
        version_detected = ''
        for version in versions:
            if version.startswith(f'{nawah_level}.'):
                if version_detected and int(version.split('.')[-1]) < int(
                    version_detected.split('.')[-1]
                ):
                    continue
                version_detected = version
        if version_detected and version_detected != Config.sys.version:
            logger.warning(
                f'Your app is using Nawah version \'{Config.sys.version}\' while newer version \'{version_detected}\' of the API-level is available. Please, update'
            )
    except:
        logger.warning(
            'An error occurred while attempting to check for latest update to Nawah. Please, check for updates on your own'
        )

    # Check for jobs
    if Config.jobs:
        # Check jobs schedule validity
        Config.sys.jobs_base = datetime.datetime.utcnow()
        for job_name in Config.jobs:
            job = Config.jobs[job_name]
            if not croniter.is_valid(job.schedule):
                raise ConfigException(
                    f'Job with schedule \'{job_name}\' schedule is invalid'
                )

            job._cron_schedule = croniter(job.schedule, Config.sys.jobs_base)
            job._next_time = datetime.datetime.fromtimestamp(
                job._cron_schedule.get_next(), datetime.timezone.utc
            ).isoformat()[:16]

    # Check for presence of user_auth_attrs
    if not Config.user_attrs:
        raise ConfigException('No \'user_attrs\' are provided')

    # Check for Env Vars
    attrs_defaults = {
        'data_server': 'mongodb://localhost',
        'data_name': 'nawah_data',
        'data_ssl': False,
        'emulate_test': False,
    }
    for attr_name in attrs_defaults:
        attr_val = getattr(Config, attr_name)
        if type(attr_val) == str and attr_val.startswith('$__env.'):
            logger.debug(f'Detected Env Variable for config attr \'{attr_name}\'')
            if not os.getenv(attr_val[7:]):
                logger.warning(
                    f'Couldn\'t read Env Variable for config attr \'{attr_name}\'. Defaulting to \'{attrs_defaults[attr_name]}\''
                )
                setattr(Config, attr_name, attrs_defaults[attr_name])
            else:
                # Set data_ssl to True rather than string Env Variable value
                if attr_name == 'ssl':
                    attr_val = True
                else:
                    attr_val = os.getenv(attr_val[7:])
                logger.warning(
                    f'Setting Env Variable for config attr \'{attr_name}\' to \'{attr_val}\''
                )
                setattr(Config, attr_name, attr_val)

    from nawah import data as Data

    # Create default env dict
    anon_user = _compile_anon_user()
    anon_session = _compile_anon_session()
    anon_session['user'] = anon_user
    Config.sys.conn = Data.create_conn()
    Config.sys.env = {
        'conn': Config.sys.conn,
        'REMOTE_ADDR': '127.0.0.1',
        'HTTP_USER_AGENT': 'Nawah',
        'client_app': '__sys',
        'session': anon_session,
    }

    # Check cache_server Config Attr
    if Config.cache_server:
        logger.debug('Attempting to create connection with \'cache_server\'')
        Config.sys.cache = aioredis.Redis(
            host=Config.cache_server,
            db=Config.cache_db,
            username=Config.cache_username,
            password=Config.cache_password,
        ).json()

    # Check error_reporting_server Config Attr
    if Config.error_reporting_server:
        logger.debug('Attempting to create connection with \'error_reporting_server\'')
        sentry_sdk.init(  # pylint: disable=abstract-class-instantiated
            Config.error_reporting_server,
            traces_sample_rate=1.0,
            with_locals=True,
            release=f'{Config.sys.name}:{Config.sys.version}',
            environment=Config.env,
        )

    # Check test mode
    if Config.test:
        logger.debug('Test mode detected')
        logger.setLevel(logging.DEBUG)
        __location__ = os.path.realpath(os.path.join('.'))
        if not os.path.exists(os.path.join(__location__, 'tests')):
            os.makedirs(os.path.join(__location__, 'tests'))
        for module_name, module in Config.modules.items():
            module_collection = module.collection
            logger.debug(
                f'Updating collection name \'{module_collection}\' of module {module_name}'
            )
            module_collection = module.collection = f'test_{module_collection}'

            logger.debug(f'Flushing test collection \'{module_collection}\'')
            await Data.drop(
                env=Config.sys.env,
                collection_name=module_collection,
            )

    # Test user_settings
    logger.debug('Testing user_settings')
    if Config.user_settings:
        for user_setting in Config.user_settings:
            logger.debug('Testing %s', user_setting)
            if not isinstance(Config.user_settings[user_setting], UserSetting):
                raise ConfigException(
                    'Invalid Config Attr \'user_settings\' with key \'{user_setting}\' of type \'{type(Config.user_settings[user_setting])}\' with required type \'USER_SETTING\''
                )

            # Validate USER_SETTING
            validate_user_setting(user_setting=Config.user_settings[user_setting])

    # Checking users collection
    # [TODO] Updated sequence to handle users
    logger.debug('Testing users collection')
    user_results = await call(
        'user/read',
        # [TODO] Check behaviour with removed Event.ON
        skip_events=[Event.PERM],
        env=Config.sys.env,
        query=[{'_id': 'f00000000000000000000010'}],
    )
    if not user_results['args']['count']:
        logger.debug('ADMIN user not found, creating it')
        # Prepare base ADMIN user doc
        admin_create_doc = {
            '_id': ObjectId('f00000000000000000000010'),
            'name': {Config.locale: '__ADMIN'},
            'groups': [],
            'privileges': {'*': ['*']},
            'locale': Config.locale,
        }
        # Update ADMIN user doc with admin_doc Config Attr
        admin_create_doc.update(Config.admin_doc)

        for auth_attr in Config.user_attrs:
            admin_create_doc[f'{auth_attr}_hash'] = pbkdf2_sha512.using(
                rounds=100000
            ).hash(
                f'{auth_attr}{admin_create_doc[auth_attr]}{Config.admin_password}'
                f'{Config.anon_token}'.encode('utf-8')
            )
        admin_results = await call(
            'user/create',
            skip_events=[Event.PERM],
            env=Config.sys.env,
            doc=admin_create_doc,
        )
        logger.debug('ADMIN user creation results: %s', admin_results)
        if admin_results['status'] != 200:
            raise ConfigException('Config step failed')
    elif not Config.force_admin_check:
        logger.warning(
            'ADMIN user found, skipping check due to force_admin_check Config Attr'
        )
    else:
        logger.warning(
            'ADMIN user found, checking it due to force_admin_check Config Attr'
        )
        admin_doc = user_results['args']['docs'][0]
        admin_doc_update = {}
        for attr_name, attr_val in Config.admin_doc.items():
            if (
                attr_name not in admin_doc
                or not admin_doc[attr_name]
                or attr_val != admin_doc[attr_name]
            ):
                if (
                    isinstance(attr_val, dict)
                    and Config.locale in attr_val
                    and isinstance(admin_doc[attr_name], dict)
                    and (
                        (
                            Config.locale in admin_doc[attr_name]
                            and attr_val[Config.locale]
                            == admin_doc[attr_name][Config.locale]
                        )
                        or (Config.locale not in admin_doc[attr_name])
                    )
                ):
                    continue
                logger.debug(
                    'Detected change in \'admin_doc.%s\' Config Attr', attr_name
                )
                admin_doc_update[attr_name] = attr_val
        for auth_attr in Config.user_attrs:
            auth_attr_hash = pbkdf2_sha512.using(rounds=100000).hash(
                f'{auth_attr}{admin_doc[auth_attr]}{Config.admin_password}'
                f'{Config.anon_token}'.encode('utf-8')
            )
            if (
                f'{auth_attr}_hash' not in admin_doc
                or auth_attr_hash != admin_doc[f'{auth_attr}_hash']
            ):
                logger.debug(f'Detected change in \'admin_password\' Config Attr')
                admin_doc_update[f'{auth_attr}_hash'] = auth_attr_hash
        if len(admin_doc_update.keys()):
            logger.debug(
                f'Attempting to update ADMIN user with doc: \'{admin_doc_update}\''
            )
            admin_results = await call(
                'user/update',
                # [TODO] Check behaviour with removed Event.ON
                # [TODO] Check behaviour with removed Event.PRE
                skip_events=[Event.PERM],
                env=Config.sys.env,
                query=[{'_id': ObjectId('f00000000000000000000010')}],
                doc=admin_doc_update,
            )
            logger.debug(f'ADMIN user update results: {admin_results}')
            if admin_results['status'] != 200:
                raise ConfigException('Config step failed')
        else:
            logger.debug('ADMIN user is up-to-date')

    Config.sys.docs[ObjectId('f00000000000000000000010')] = SysDoc(module='user')

    # Test if ANON user exists
    user_results = await call(
        'user/read',
        # [TODO] Check behaviour with removed Event.ON
        skip_events=[Event.PERM],
        env=Config.sys.env,
        query=[{'_id': 'f00000000000000000000011'}],
    )
    if not user_results['args']['count']:
        logger.debug('ANON user not found, creating it')
        anon_results = await call(
            'user/create',
            # [TODO] Check behaviour with removed Event.ON
            # [TODO] Check behaviour with removed Event.PRE
            skip_events=[Event.PERM],
            env=Config.sys.env,
            doc=_compile_anon_user(),
        )
        logger.debug(f'ANON user creation results: {anon_results}')
        if anon_results['status'] != 200:
            raise ConfigException('Config step failed')
    else:
        logger.debug('ANON user found, checking it')
        anon_doc = _compile_anon_user()
        anon_doc_update = {}
        for attr in Config.user_attrs:
            if attr not in anon_doc or not anon_doc[attr]:
                logger.debug(f'Detected change in \'anon_doc.{attr}\' Config Attr')
                anon_doc_update[attr] = generate_attr_val(attr_type=Config.user_attrs[attr])
        for module in Config.anon_privileges:
            if module not in anon_doc or set(anon_doc[module]) != set(
                Config.anon_privileges[module]
            ):
                logger.debug(f'Detected change in \'anon_privileges\' Config Attr')
                anon_doc_update[f'privileges.{module}'] = Config.anon_privileges[module]
        for auth_attr in Config.user_attrs:
            if (
                f'{auth_attr}_hash' not in anon_doc
                or anon_doc[f'{auth_attr}_hash'] != Config.anon_token
            ):
                logger.debug(f'Detected change in \'anon_token\' Config Attr')
                anon_doc_update[attr] = Config.anon_token
            anon_doc_update[f'{auth_attr}_hash'] = Config.anon_token
        if len(anon_doc_update.keys()):
            logger.debug(
                f'Attempting to update ANON user with doc: \'{anon_doc_update}\''
            )
            anon_results = await call(
                'user/update',
                # [TODO] Check behaviour with removed Event.ON
                # [TODO] Check behaviour with removed Event.PRE
                skip_events=[Event.PERM],
                env=Config.sys.env,
                query=[{'_id': ObjectId('f00000000000000000000011')}],
                doc=anon_doc_update,
            )
            logger.debug(f'ANON user update results: {anon_results}')
            if anon_results['status'] != 200:
                raise ConfigException('Config step failed')
        else:
            logger.debug('ANON user is up-to-date')

    Config.sys.docs[ObjectId('f00000000000000000000011')] = SysDoc(module='user')

    logger.debug('Testing sessions collection')
    # Test if ANON session exists
    session_results = await call(
        'session/read',
        # [TODO] Check behaviour with removed Event.ON
        skip_events=[Event.PERM],
        env=Config.sys.env,
        query=[{'_id': 'f00000000000000000000012'}],
    )
    if not session_results['args']['count']:
        logger.debug('ANON session not found, creating it')
        anon_results = await call(
            'session/create',
            # [TODO] Check behaviour with removed Event.ON
            # [TODO] Check behaviour with removed Event.PRE
            skip_events=[Event.PERM],
            env=Config.sys.env,
            doc=_compile_anon_session(),
        )
        logger.debug(f'ANON session creation results: {anon_results}')
        if anon_results['status'] != 200:
            raise ConfigException('Config step failed')
    Config.sys.docs[ObjectId('f00000000000000000000012')] = SysDoc(module='session')

    logger.debug('Testing groups collection')
    # Test if DEFAULT group exists
    group_results = await call(
        'group/read',
        # [TODO] Check behaviour with removed Event.ON
        skip_events=[Event.PERM],
        env=Config.sys.env,
        query=[{'_id': 'f00000000000000000000013'}],
    )
    if not group_results['args']['count']:
        logger.debug('DEFAULT group not found, creating it')
        group_create_doc = {
            '_id': ObjectId('f00000000000000000000013'),
            'user': ObjectId('f00000000000000000000010'),
            'name': {locale: '__DEFAULT' for locale in Config.locales},
            'bio': {locale: '__DEFAULT' for locale in Config.locales},
            'privileges': Config.default_privileges,
        }
        group_results = await call(
            'group/create',
            # [TODO] Check behaviour with removed Event.ON
            # [TODO] Check behaviour with removed Event.PRE
            skip_events=[Event.PERM],
            env=Config.sys.env,
            doc=group_create_doc,
        )
        logger.debug('DEFAULT group creation results: %s', group_results)
        if group_results['status'] != 200:
            raise ConfigException('Config step failed')
    else:
        logger.debug('DEFAULT group found, checking it')
        default_doc = group_results['args']['docs'][0]
        default_doc_update: MutableMapping[str, Any] = {}
        for module in Config.default_privileges:
            if module not in default_doc['privileges'].keys() or set(
                default_doc['privileges'][module]
            ) != set(Config.default_privileges[module]):
                logger.debug('Detected change in \'default_privileges\' Config Attr')
                default_doc_update[f'privileges.{module}'] = Config.default_privileges[
                    module
                ]
        if len(default_doc_update.keys()):
            logger.debug(
                'Attempting to update DEFAULT group with doc: \'%s\'',
                default_doc_update,
            )
            default_results = await call(
                'group/update',
                # [TODO] Check behaviour with removed Event.ON
                # [TODO] Check behaviour with removed Event.PRE
                skip_events=[Event.PERM],
                env=Config.sys.env,
                query=[{'_id': ObjectId('f00000000000000000000013')}],
                doc=default_doc_update,
            )
            logger.debug(f'DEFAULT group update results: {default_results}')
            if anon_results['status'] != 200:
                raise ConfigException('Config step failed')
        else:
            logger.debug('DEFAULT group is up-to-date')

    Config.sys.docs[ObjectId('f00000000000000000000013')] = SysDoc(module='group')

    # Test app-specific groups
    logger.debug('Testing app-specific groups collection')
    for group in Config.groups:
        group_results = await call(
            'group/read',
            # [TODO] Check behaviour with removed Event.ON
            skip_events=[Event.PERM],
            env=Config.sys.env,
            query=[{'_id': group['_id']}],
        )
        if not group_results['args']['count']:
            logger.debug(
                f'App-specific group with name \'{group["name"]}\' not found, creating it'
            )
            group_results = await call(
                'group/create',
                # [TODO] Check behaviour with removed Event.ON
                # [TODO] Check behaviour with removed Event.PRE
                skip_events=[Event.PERM],
                env=Config.sys.env,
                doc=group,
            )
            logger.debug(
                f'App-specific group with name {group["name"]} creation results: {group_results}'
            )
            if group_results['status'] != 200:
                raise ConfigException('Config step failed')
        else:
            logger.debug(
                f'App-specific group with name \'{group["name"]}\' found, checking it'
            )
            group_doc = group_results['args']['docs'][0]
            group_doc_update = {}
            if 'privileges' in group:
                for module in group['privileges']:
                    if module not in group_doc['privileges'].keys() or set(
                        group_doc['privileges'][module]
                    ) != set(group['privileges'][module]):
                        logger.debug(
                            f'Detected change in \'privileges\' Doc Arg for group with name \'{group["name"]}\''
                        )
                        group_doc_update[f'privileges.{module}'] = group['privileges'][
                            module
                        ]
            if len(group_doc_update.keys()):
                logger.debug(
                    'Attempting to update group with name \'%s\' with doc: \'%s\'',
                    group['name'],
                    group_doc_update,
                )
                group_results = await call(
                    'group/update',
                    # [TODO] Check behaviour with removed Event.ON
                    # [TODO] Check behaviour with removed Event.PRE
                    skip_events=[Event.PERM],
                    env=Config.sys.env,
                    query=[{'_id': group['_id']}],
                    doc=group_doc_update,
                )
                logger.debug(
                    'Group with name \'%s\' update results: %s',
                    group['name'],
                    group_results,
                )
                if group_results['status'] != 200:
                    raise ConfigException('Config step failed')
            else:
                logger.debug('Group with name \'%s\' is up-to-date', group['name'])

        Config.sys.docs[ObjectId(group['_id'])] = SysDoc(module='group')

    # Test app-specific data indexes
    logger.debug('Testing data indexes')
    for index in Config.data_indexes:
        logger.debug('Attempting to create data index: %s', index)
        try:
            Config.sys.conn[Config.data_name][index['collection']].create_index(
                index['index']
            )
        except Exception as e:
            logger.error('Failed to create data index: %s, with error: %s', index, e)
            logger.error('Evaluate error and take action manually')

    logger.debug(
        'Creating \'var\', \'type\', \'user\' data indexes for settings collections'
    )
    Config.sys.conn[Config.data_name]['settings'].create_index([('var', 1)])
    Config.sys.conn[Config.data_name]['settings'].create_index([('type', 1)])
    Config.sys.conn[Config.data_name]['settings'].create_index([('user', 1)])
    logger.debug(
        'Creating \'user\', \'event\', \'subevent\' data indexes for analytics collections'
    )
    Config.sys.conn[Config.data_name]['analytics'].create_index([('user', 1)])
    Config.sys.conn[Config.data_name]['analytics'].create_index([('event', 1)])
    Config.sys.conn[Config.data_name]['analytics'].create_index([('subevent', 1)])
    logger.debug('Creating \'__deleted\' data indexes for all collections')
    for module in Config.modules:
        if Config.modules[module].collection:
            logger.debug(
                'Attempting to create \'__deleted\' data index for collection: %s',
                Config.modules[module].collection,
            )
            Config.sys.conn[Config.data_name][
                Config.modules[module].collection
            ].create_index([('__deleted', 1)])

    # Test app-specific docs
    logger.debug('Testing docs')
    for doc in Config.docs:
        if not isinstance(doc, SysDoc):
            raise ConfigException('Invalid Config Attr \'docs\'')

        doc_results = await call(
            f'{doc.module}/read',
            # [TODO] Check behaviour with removed Event.ON
            # [TODO] Check behaviour with removed Event.PRE
            skip_events=[Event.PERM, Event.ATTRS],
            env=Config.sys.env,
            query=[{doc.key: doc.key_value}],  # type: ignore
        )
        if not doc_results['args']['count']:
            skip_events = [Event.PERM]
            if doc.skip_args is True:
                skip_events.append(Event.ATTRS)
            doc.doc = cast('NawahDoc', doc.doc)
            doc_results = await call(
                f'{doc.module}/create',
                skip_events=skip_events,
                env=Config.sys.env,
                doc=doc.doc,
            )
            logger.debug(
                'App-specific doc with %s \'%s\' of module \'%s\' creation results: %s',
                doc.key,
                doc.key_value,
                doc.module,
                doc_results,
            )
            if doc_results['status'] != 200:
                raise ConfigException('Config step failed')
        Config.sys.docs[ObjectId(doc_results['args']['docs'][0]['_id'])] = SysDoc(
            module=doc.module
        )

    # Check for emulate_test mode
    if Config.emulate_test:
        Config.test = True


def _compile_anon_user():
    anon_doc = {
        '_id': ObjectId('f00000000000000000000011'),
        'name': {Config.locale: '__ANON'},
        'groups': [],
        'privileges': Config.anon_privileges,
        'locale': Config.locale,
    }
    for attr_name, attr_val in Config.user_attrs.items():
        anon_doc[attr_name] = generate_attr_val(attr_type=attr_val)
    for auth_attr in Config.user_attrs:
        anon_doc[f'{auth_attr}_hash'] = Config.anon_token
    return anon_doc


def _compile_anon_session():
    session_doc = {
        '_id': ObjectId('f00000000000000000000012'),
        'user': ObjectId('f00000000000000000000011'),
        'host_add': '127.0.0.1',
        'user_agent': Config.anon_token,
        'timestamp': '1970-01-01T00:00:00',
        'expiry': '1970-01-01T00:00:00',
        'token': Config.anon_token,
        'token_hash': Config.anon_token,
    }
    return session_doc
