import asyncio
import json
from contextlib import suppress
from typing import Union, List, Tuple, Iterator, Optional

import aiofiles
import cdblib
import yaml

__all__ = ('Config',)


class Config:

    def __init__(self, filename: str, reload_interval: Optional[int] = None, loop=None) -> None:
        self._filename = filename
        self._reload_interval = reload_interval
        self._reload_task = None
        self._loop = loop if loop else asyncio.get_event_loop()

    @classmethod
    def read(cls, filename: str, reload_interval: Optional[int] = 30) -> "Config":
        """Read a cdb file and schedule periodic reload if needed"""
        _config = cls(filename, reload_interval)

        with open(_config._filename, 'rb') as f:
            _config.cdb = cdblib.Reader(f.read())

        if reload_interval:
            _config._reload_task = _config._loop.create_task(_config._schedule_reload())
        return _config

    def get(self, key: str) -> Union[str, dict]:
        return self._get(key)

    def __getitem__(self, key: str) -> Union[str, dict]:
        return self._get(key)

    def _get(self, key: str) -> Union[str, dict]:
        """Get value by key and convert it to corresponding type"""
        binary_value = self.cdb.get(key.encode())
        if binary_value is None:
            raise KeyError(f'Key `{key}` is not found in config')
        return self._cast_value(binary_value)

    def __contains__(self, key):
        """Return True if key exists in the config"""
        return key in self.cdb

    def items(self) -> List[Tuple[bytes, bytes]]:
        """Get config (key, value) pairs"""
        return self.cdb.items()

    def keys(self) -> List[bytes]:
        """Get list of config keys"""
        return self.cdb.keys()

    def values(self) -> List[bytes]:
        """Get list of config values"""
        return self.cdb.values()

    async def _schedule_reload(self):
        while True:
            async with aiofiles.open(self._filename, 'rb') as f:
                _config = await f.read()
            self.cdb = cdblib.Reader(_config)
            await asyncio.sleep(self._reload_interval)

    def fill_from_yaml(self, filename: str):
        """Read yaml file, convert parameters and save them to cdb file"""
        with open(filename) as f:
            conf = yaml.full_load(f.read())

        cdb_items = self._flatten_dict(conf)

        with open(self._filename, 'wb') as f:
            writer = cdblib.Writer(f)
            for k, v in cdb_items:
                writer.put(k.encode(), v.encode())
            writer.finalize()

    async def shutdown(self):
        if self._reload_task:
            self._reload_task.cancel()
            with suppress(asyncio.CancelledError):
                await self._reload_task

    @staticmethod
    def _cast_value(value: bytes) -> Union[str, dict]:
        # Decode bytes to unicode
        value = value.decode()
        prefix = value[:1]
        value = value[1:]

        if prefix == 's':
            return value
        elif prefix == 'j':
            return json.loads(value)
        else:
            raise ValueError

    def _flatten_dict(self, d: dict, path: str = '') -> Iterator[Tuple[str, str]]:
        """Return dict items as tuples: (xpath-like key, value)"""
        for key, value in d.items():
            _path = '/'.join((path, key))
            if isinstance(value, dict):
                yield from self._flatten_dict(value, _path)
            elif isinstance(value, list):
                yield _path, f'j{json.dumps(value)}'
            else:
                try:
                    json.loads(value)
                except (TypeError, json.decoder.JSONDecodeError):
                    yield _path, f's{value}'
                else:
                    yield _path, f'j{value}'

import asyncio
import json
from contextlib import suppress
from typing import Union, List, Tuple, Iterator, Optional

import aiofiles
import cdblib
import yaml

__all__ = ('Config',)


class Config:

    def __init__(self, filename: str, reload_interval: Optional[int] = None, loop=None) -> None:
        self._filename = filename
        self._reload_interval = reload_interval
        self._reload_task = None
        self._loop = loop if loop else asyncio.get_event_loop()

    @classmethod
    def read(cls, filename: str, reload_interval: Optional[int] = 30) -> "Config":
        """Read a cdb file and schedule periodic reload if needed"""
        _config = cls(filename, reload_interval)

        with open(_config._filename, 'rb') as f:
            _config.cdb = cdblib.Reader(f.read())

        if reload_interval:
            _config._reload_task = _config._loop.create_task(_config._schedule_reload())
        return _config

    def get(self, key: str) -> Union[str, dict]:
        return self._get(key)

    def __getitem__(self, key: str) -> Union[str, dict]:
        return self._get(key)

    def _get(self, key: str) -> Union[str, dict]:
        """Get value by key and convert it to corresponding type"""
        binary_value = self.cdb.get(key.encode())
        if binary_value is None:
            raise KeyError(f'Key `{key}` is not found in config')
        return self._cast_value(binary_value)

    def __contains__(self, key):
        """Return True if key exists in the config"""
        return key in self.cdb

    def items(self) -> List[Tuple[bytes, bytes]]:
        """Get config (key, value) pairs"""
        return self.cdb.items()

    def keys(self) -> List[bytes]:
        """Get list of config keys"""
        return self.cdb.keys()

    def values(self) -> List[bytes]:
        """Get list of config values"""
        return self.cdb.values()

    async def _schedule_reload(self):
        while True:
            async with aiofiles.open(self._filename, 'rb') as f:
                _config = await f.read()
            self.cdb = cdblib.Reader(_config)
            await asyncio.sleep(self._reload_interval)

    def fill_from_yaml(self, filename: str):
        """Read yaml file, convert parameters and save them to cdb file"""
        with open(filename) as f:
            conf = yaml.full_load(f.read())

        cdb_items = self._flatten_dict(conf)

        with open(self._filename, 'wb') as f:
            writer = cdblib.Writer(f)
            for k, v in cdb_items:
                writer.put(k.encode(), v.encode())
            writer.finalize()

    async def shutdown(self):
        if self._reload_task:
            self._reload_task.cancel()
            with suppress(asyncio.CancelledError):
                await self._reload_task

    @staticmethod
    def _cast_value(value: bytes) -> Union[str, dict]:
        # Decode bytes to unicode
        value = value.decode()
        prefix = value[:1]
        value = value[1:]

        if prefix == 's':
            return value
        elif prefix == 'j':
            return json.loads(value)
        else:
            raise ValueError

    def _flatten_dict(self, d: dict, path: str = '') -> Iterator[Tuple[str, str]]:
        """Return dict items as tuples: (xpath-like key, value)"""
        for key, value in d.items():
            _path = '/'.join((path, key))
            if isinstance(value, dict):
                yield from self._flatten_dict(value, _path)
            elif isinstance(value, list):
                yield _path, f'j{json.dumps(value)}'
            else:
                try:
                    json.loads(value)
                except (TypeError, json.decoder.JSONDecodeError):
                    yield _path, f's{value}'
                else:
                    yield _path, f'j{value}'

Read a cdb file and schedule periodic reload if needed Get value by key and convert it to corresponding type Return True if key exists in the config Get config (key, value) pairs Get list of config keys Get list of config values Read yaml file, convert parameters and save them to cdb file # Decode bytes to unicode Return dict items as tuples: (xpath-like key, value)