# coding: utf-8
"""\
Функции, помогающие в разборе конфигурационного файла
"""
import toml
from os.path import exists
from typing import Union, Any

from .error import Error
from .type_helpers import BaseTypeHelper
from ._lib import _UNDEFINED


class ConfigFileError(Error):
    """\
    Базовое исключение при обработке конфигурационного файла
    """


class ConfigSectionNotFound(ConfigFileError):
    """\
    Отсутствие нужного раздела в конфигурации
    """


class ConfigFileContent(object):
    """\
    Работа с содержимым файла конфигурации
    """
    def __init__(self, file_data, section_name=None):
        """\
        :param file_data: Разобранное в словарь содержимое файла конфигурации
        :param section_name: Имя секции в конфигурации
        """
        self.d = file_data
        self.section = section_name

    def _format_err_msg(self, msg: str) -> str:
        if self.section is not None:
            return f'Раздел конфигурации "{self.section}": {msg}'

        else:
            return msg

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        pass

    def get_value(self, name: str,
                  val_type: Union[type, BaseTypeHelper] = str,
                  default: Any = _UNDEFINED,
                  mandatory: bool = True):
        """\
        Получить из текущего раздела конфигурации нужный элемент

        :param name: имя параметра
        :param val_type: тип параметра
        :param default: значение параметра по умолчанию
        :param mandatory: является ли параметр обязательным. Если ``False``, то в случае отсутствия
                          параметра будет использовано значение None
        """
        if name not in self.d:
            if default is not _UNDEFINED:
                return default

            elif mandatory:
                raise ConfigFileError(self._format_err_msg(f'Параметр "{name}" в файле отсутствует'))

            else:
                return None

        else:
            _buf = self.d[name]
            try:
                return val_type(_buf)

            except (ValueError, TypeError) as e:
                raise ConfigFileError(self._format_err_msg(f'Не удалось привести значение "{_buf}" к типу '
                                                           f'{repr(val_type)}: {e}'))

    def _get_section_name(self, *names: str) -> str:
        """\
        Поучить имя текущего раздели или подраздела по заданному имени

        :type names: str
        :param names: Последовательные имена подразделов для получения доступа к глубокой иерархии
        :returns: Имя подраздела
        """
        res = self.section if self.section is not None else ''
        if not names:
            return res

        else:
            if res:
                res += '.'

            return res + '.'.join(names)

    def _get_part_by_path(self, *names: str):
        """\
        Получить конфигурационный элемент по заданному пути
        """
        if not names:
            raise ValueError(f'Не заданно имя запрашиваемого раздела')

        upper_path = names[:-1]
        last_path_el = names[-1]
        cur_level = self.d
        cur_section = self._get_section_name()

        for el in upper_path:
            cur_section = el if not cur_section else f'{cur_section}.{el}'

            if el in cur_level:
                cur_level = cur_level[el]

            else:
                raise ConfigSectionNotFound(f'Раздел "{cur_section}" не найден')

        cur_section = last_path_el if not cur_section else f'{cur_section}.{last_path_el}'
        if last_path_el in cur_level:
            return cur_level[last_path_el]

        else:
            raise ConfigSectionNotFound(f'Раздел "{cur_section}" не найден')


    def get_section(self, name: str, mandatory: bool = True):
        """\
        Получить раздел конфигурационного файла из текущего или корневого раздела

        :param name: имя раздела
        :param mandatory: является ли раздел обязательным или он может отсутствовать в конфигурации. Во втором
                          случае, будет создан пустой словарь и использован в качестве данных раздела.
        :returns: Экземпляр того-же класса, только для новой секции
        """
        section_name = self._get_section_name(name)
        try:
            section_val = self._get_part_by_path(name)

            if not isinstance(section_val, dict):
                raise ConfigFileError(f'Параметр конфигурации "{section_name}" не является разделом')

            else:
                return ConfigFileContent(file_data=section_val, section_name=f'{section_name}')

        except ConfigSectionNotFound as e:
            if mandatory:
                raise e

            else:
                return ConfigFileContent(file_data={}, section_name=f'{section_name}')

    def get_section_by_path(self, *names: str, mandatory: bool = True):
        """\
        Поучить секцию конфигурации по явно заданному пути

        :type names: str
        :param names: Имя секции на текущем уровне. Можно задавать несколько
             для прохода по всей иерархии необходимой иерархии одним вызовом.

        :type mandatory: bool
        :param mandatory: Является ли искомая секция обязательной?

        :returns: Экземпляр того-же класса, только для новой секции
        """
        section_name = self._get_section_name(*names)
        try:
            section_val = self._get_part_by_path(*names)

            if not isinstance(section_val, dict):
                raise ConfigFileError(f'Параметр конфигурации "{section_name}" не является разделом')

            else:
                return ConfigFileContent(file_data=section_val, section_name=f'{section_name}')

        except ConfigSectionNotFound as e:
            if mandatory:
                raise e

            else:
                return ConfigFileContent(file_data={}, section_name=f'{section_name}')

    def get_list_section(self, name: str, mandatory: bool = True):
        """\
        Для случая, когда раздел конфигурации задан массивом toml ([[${! SOME_SECTION !}]])

        :type name: str
        :param name: Имя раздела

        :type mandatory: bool
        :param mandatory: Данный раздел обязателен и не может отсутствовать

        :returns: Спискол из экземпляров данного класса, нацеленные на элементы массива хэш-таблиц файла toml
        """
        section_name = self._get_section_name(name)
        try:
            section_val = self._get_part_by_path(name)

            if isinstance(section_val, (list, tuple)):
                res = []
                for el in section_val:
                    if isinstance(el, dict):
                        res.append(ConfigFileContent(file_data=el, section_name=f'{section_name}'))

                    else:
                        raise ConfigFileError(f'Параметр конфигурации "{section_name}"'
                                              f' со значением "{el}"'
                                              f' не является разделом')
                return res

            elif isinstance(section_val, dict):
                return [
                    ConfigFileContent(file_data=section_val, section_name=f'{section_name}')
                ]

            else:
                raise ConfigFileError(f'Параметр конфигурации "{section_name}" не является разделом')

        except ConfigSectionNotFound as e:
            if mandatory:
                raise e

            else:
                return [
                    ConfigFileContent(file_data={}, section_name=f'{section_name}')
                ]


class ConfigFile(object):
    """\
    Работа с самим файлом конфигурации
    """
    def __init__(self, file_name: str):
        if not exists(file_name):
            raise ConfigFileError(f'Файл конфигурации "{file_name}" не существует')

        self.file_name = file_name

    def get(self):
        """\
        Производим разбор файла конфигурации и создаём корневой раздел конфигурации
        """
        try:
            return ConfigFileContent(toml.load(self.file_name))

        except OSError as e:
            raise ConfigFileError(f'Ошибка чтения файла "{self.file_name}": {e}')

        except toml.TomlDecodeError as e:
            raise ConfigFileError(f'Ошибка в разборе файла конфигурации: {e}')

        except ValueError as e:
            raise ConfigFileError(f'Ошибка разбора файла конфигурации: {ConfigFileError.fmt_error(e)}')

    def __exit__(self, exc_type, exc_val, exc_tb):
        pass

    def __enter__(self):
        return self.get()
