import pathlib
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, cast

from axipy._internal._decorator import _deprecated_by
from axipy._internal._shadow_instance_factory import _shadow_manager
from axipy.da.data_object import DataObject, QueryTable, Table
from axipy.da.schema import Schema
from axipy.da.sqldialect import TypeSqlDialect

if TYPE_CHECKING:
    from axipy.cpp_core_core import ShadowIO

__all__: List[str] = []


class _IOClass:
    @property
    def _shadow(self) -> "ShadowIO":
        return _shadow_manager.shadow_io

    def loaded_providers(self) -> Dict[str, str]:
        """
        Возвращает список всех загруженных провайдеров данных.

        Returns:
            Провайдеры в виде пар ``(Идентификатор : Описание)``.

        ::

            print(provider_manager.loaded_providers())
        """
        return self._shadow.providersInfo()

    def openfile(self, filepath: str, dataobject: str = "", provider: str = "") -> DataObject:
        """
        Открывает данные из файла.

        Args:
            filepath: Путь к открываемому файлу.
            dataobject: Имя открываемого объекта данных.
            provider: Провайдер, с помощью которого будет произведено открытие.
                Если не указан, выбирается наиболее подходящий.
                Перечень можно получить с помощью :meth:`loaded_providers`.

        Пример::

            table = provider_manager.openfile(
                '../path/to/datadir/example.gpkg', dataobject='tablename', provider='SqliteDataProvider')
        """
        result = self._shadow.openfile(filepath, dataobject, provider)
        # Guaranteed not None
        return cast(DataObject, DataObject._wrap(result))

    def open(self, definition: dict) -> DataObject:
        """
        Открывает данные по описанию.

        Формат описания объектов данных индивидуален для каждого провайдера данных,
        однако многие элементы используются для всех провайдеров данных.

        Args:
            definition: Описание объекта данных.

        Пример::

            # Пример открытия GPKG файла::
            definition = { 'src': '../path/to/datadir/example.gpkg',
                           'dataobject': 'tablename',
                           'provider': 'SqliteDataProvider' }
            table = provider_manager.open(definition)

        Пример открытия таблицы базы данных::

            definition = {"src": "localhost",
                          "db": "sample",
                          "user": "postgres",
                          "password": "postgres",
                          "dataobject": "public.world",
                          "provider": "PgDataProvider"}
            table = provider_manager.open(definition)
        """
        result = self._shadow.open(definition)
        # Guaranteed not None
        return cast(DataObject, DataObject._wrap(result))

    def create(self, definition: dict) -> DataObject:
        """
        Создает данные из описания.

        Возможные параметры:
            - ``src`` - Строка, определяющая местоположение источника данных.
             Это может быть либо путь к файлу с расширением TAB,
             либо пустая строка (для таблицы, размещаемой в памяти).
            - ``schema`` - Схема таблицы. Задается массивом объектов, содержащих атрибуты.

        Args:
            definition: Описание объекта данных.

        Пример::

            definition = {
                'src': '../path/to/datadir/edit/table.tab',
                'schema': attr.schema(
                    attr.string('field1'),
                    attr.integer('field2'),
                ),
            }
            table = provider_manager.create(definition)
        """
        if "src" in definition and pathlib.Path(definition["src"]).is_file():
            override = definition["override"] if "override" in definition else False
            if not override:
                raise FileExistsError(f"File '{definition['src']}' exist. Remove it.")

        copy_definition = definition.copy()
        if "schema" in copy_definition:
            if isinstance(copy_definition["schema"], Schema):
                copy_definition["schema"] = copy_definition["schema"]._to_dict()
        result = self._shadow.create(copy_definition)
        # Guaranteed not None
        return cast(DataObject, DataObject._wrap(result))

    def createfile(self, filepath: str, schema: Schema) -> DataObject:
        """
        Создает таблицу.

        :meth:`create` выполняет ту же функцию, но в более обобщенном виде.

        Args:
            filepath: Путь к создаваемой таблице.
            schema: Схема таблицы.
        """
        return self.create({"src": filepath, "schema": schema})

    def read_contents(self, definition: dict) -> List[str]:
        """
        Читает содержимое источника данных.

        Обычно используется для источников, способных содержать несколько
        объектов данных.

        Args:
            definition: Описание источника данных.

        Returns:
            Имена объектов данных.

        Пример::

            import axipy
            # Запросим перечень таблиц
            contents = axipy.provider_manager.read_contents('../path/to/datadir/example.gpkg')
            print(contents)
            >>> ['world', 'worldcap']
            # Откроем таблицу по имени
            world = provider_manager.openfile('../path/to/datadir/example.gpkg',  dataobject='world')
        """
        # backwards compatibility check for string
        if isinstance(definition, str):
            fix_definition = {"src": definition}
        else:
            fix_definition = definition
        return self._shadow.read_contents(fix_definition)

    def _ensure_sql_dialect(self, dialect: Optional[str]) -> str:
        if dialect is not None and dialect in tuple(elem.value for elem in TypeSqlDialect):
            return dialect
        return self.sql_dialect

    def query(self, query_text: str, *tables: Table, dialect: Optional[str] = None) -> Optional[QueryTable]:
        """
        Выполняет SQL-запрос к перечисленным таблицам.

        Args:
            query_text: Текст запроса.
            *tables: Список таблиц, к которым выполняется запрос.
            dialect: Диалект, который используется при выполнении запроса. Допустимые значения `axioma` и `sqlite`.
              Значение по умолчанию установлено как :attr:`sql_dialect`.

        Returns:
            Таблица, если результатом запроса является таблица.

        Raises:
            RuntimeError: При возникновении ошибки.

        Пример::

                query_text = "SELECT * FROM world, caps WHERE world.capital = caps.capital"
                joined = provider_manager.query(query_text, world, caps)
        """
        d = self._ensure_sql_dialect(dialect)
        result = self._shadow.query(query_text, [t._shadow for t in tables], d)
        return DataObject._wrap(result)

    def check_query(self, query_text: str, *tables: Table, dialect: Optional[str] = None) -> Tuple[bool, str]:
        d = self._ensure_sql_dialect(dialect)
        return self._shadow.checkQuery(query_text, [t._shadow for t in tables], d)

    @staticmethod
    def _query_result(query_table: Optional[QueryTable]) -> Tuple[Any, ...]:
        if query_table is None:
            return tuple()
        feature = next(query_table.items())
        return tuple(v for v in feature)

    def _query_aggregate(self, query_text: str, *tables: Table) -> tuple:
        """
        Выполняет SQL-запрос и возвращает значения первой записи.

        Удобен при чтении результата агрегирующих запросов.

        Args:
            query_text: Текст запроса.
            *tables: Список таблиц, к которым выполняется запрос.

        Returns:
            Кортеж значений.

        Пример::

            avg, max, sum = provider_manager.query_aggregate(
                f'SELECT AVG(myvalue), MAX(myvalue), SUM(myvalue) FROM {table.name}', table)
        """
        query_table = self.query(query_text, *tables)
        return self._query_result(query_table)

    @property
    def sql_dialect(self) -> str:  # TypeSqlDialect:
        """
        Тип используемого диалекта по умолчанию для выполнения SQL-предложений.

        Returns:
          Строковое значение, определяющее его тип. Возможные значения: `axioma` или `sqlite`.
        """
        return self._shadow.currentSqlDialect()


_opener_instance = _IOClass()


def _apply_deprecated() -> None:
    # Using getattr to hide deprecated objects from axipy namespace on IDE inspections
    getattr(__all__, "extend")(("IOClass", "opener_instance"))

    class IOClass(_IOClass):
        """
        Warning:
        .. deprecated:: 3.0.0
            Используйте :class:`axipy.ProviderManager` и его готовый экземпляр :attr:`axipy.provider_manager`.
        """

        @_deprecated_by()
        def loaded_providers(self) -> dict:
            """
            Warning:
            .. deprecated:: 3.0.0
            """
            return super().loaded_providers()

        @_deprecated_by()
        def openfile(self, filepath: str, dataobject: str = "", provider: str = "") -> DataObject:
            """
            Warning:
            .. deprecated:: 3.0.0
            """
            return super().openfile(filepath, dataobject, provider)

        @_deprecated_by()
        def open(self, definition: dict) -> DataObject:
            """
            Warning:
            .. deprecated:: 3.0.0
            """
            return super().open(definition)

        @_deprecated_by()
        def create(self, definition: dict) -> DataObject:
            """
            Warning:
            .. deprecated:: 3.0.0
            """
            return super().create(definition)

        @_deprecated_by()
        def createfile(self, filepath: str, schema: Schema) -> DataObject:
            """
            Warning:
            .. deprecated:: 3.0.0
            """
            return super().createfile(filepath, schema)

        @_deprecated_by()
        def read_contents(self, definition: dict) -> List[str]:
            """
            Warning:
            .. deprecated:: 3.0.0
            """
            return super().read_contents(definition)

        @_deprecated_by()
        def query(self, query_text: str, *tables: Table, dialect: Optional[str] = None) -> Optional[Table]:
            """
            Warning:
            .. deprecated:: 3.0.0
            """
            return super().query(query_text, *tables, dialect=dialect)

        @_deprecated_by()
        def check_query(self, query_text: str, *tables: Table, dialect: Optional[str] = None) -> Tuple[bool, str]:
            """
            Warning:
            .. deprecated:: 3.0.0
            """
            return super().check_query(query_text, *tables, dialect=dialect)

        @property
        @_deprecated_by()
        def sql_dialect(self) -> str:
            """
            Warning:
            .. deprecated:: 3.0.0
            """
            return super().sql_dialect

    opener_instance = IOClass()

    globals().update(IOClass=IOClass, opener_instance=opener_instance)


_apply_deprecated()
