from abc import abstractmethod
from typing import List, Optional, Type, cast

from axipy.da.data_object import Table

from .data_provider import DataProvider
from .source import Destination, ExportParameters, Schema, Source

__all__: List[str] = [
    "DatabaseDataProvider",
]


class DatabaseDataProvider(DataProvider):
    """Базовый класс для провайдеров БД."""

    DEFAULT_PORT: int = 0

    @property
    @abstractmethod
    def _get_source_class(self) -> Optional[Type[Source]]:
        return None

    @property
    @abstractmethod
    def _get_destination_class(self) -> Optional[Type[Destination]]:
        return None

    def get_source(
        self,
        host: str,
        db_name: str,
        user: str,
        password: str,
        port: int = DEFAULT_PORT,
        dataobject: Optional[str] = None,
        sql: Optional[str] = None,
        prj: Optional[str] = None,
        alias: Optional[str] = None,
        unique: Optional[str] = None,
        editable_unique: Optional[bool] = False,
    ) -> Source:
        """
        Создает описательную структуру для источника данных. Она в дальнейшем может быть
        использована при открытии данных :meth:`ProviderManager.open`.

        В качестве таблицы можно указать либо ее наименование `dataobject` либо текст запроса `sql`.

        Args:
            host: Адрес сервера.
            db_name: Имя базы данных.
            user: Имя пользователя.
            password: Пароль.
            port: Порт.
            dataobject: Имя таблицы.
            sql: SQL-запрос. Если указан, то он имеет более высокий приоритет по отношению к значению `dataobject`.
            prj: Строка Системы Координат.
            alias: Псевдоним для открываемой таблицы.
            unique: Поле с уникальным значением, которое будет использоваться как идентификатор.
            editable_unique: Установить возможность изменения значения поля `unique`.
        """
        if not self._get_source_class:
            raise NotImplementedError

        pars = {
            "host": host,
            "port": port,
            "db": db_name,
            "user": user,
            "password": password,
        }
        if dataobject is not None:
            pars["dataobject"] = dataobject
        elif sql is not None:
            pars["sql"] = sql
        if unique is not None:
            pars["unique"] = unique
            if editable_unique:
                pars["editableUnique"] = True

        return self._get_source_class(Source._provider(self.id), Source._alias(alias), Source._prj(prj), pars)

    def open(
        self,
        host: str,
        db_name: str,
        user: str,
        password: str,
        port: int = DEFAULT_PORT,
        dataobject: Optional[str] = None,
        sql: Optional[str] = None,
        prj: Optional[str] = None,
        alias: Optional[str] = None,
        unique: Optional[str] = None,
        editable_unique: Optional[bool] = False,
    ) -> Table:
        """
        Открывает объект данных.

        В качестве таблицы можно указать либо ее наименование `dataobject` либо текст запроса `sql`.

        Args:
            host: Адрес сервера.
            db_name: Имя базы данных.
            user: Имя пользователя.
            password: Пароль.
            port: Порт.
            dataobject: Имя таблицы.
            sql: SQL-запрос. Если указан, то он имеет более высокий приоритет по отношению к значению `dataobject`.
            prj: Строка Системы Координат.
            alias: Псевдоним для открываемой таблицы.
            unique: Поле с уникальным значением, которое будет использоваться как идентификатор.
            editable_unique: Установить возможность изменения значения поля `unique`.
        """
        return cast(
            Table,
            self.get_source(
                host, db_name, user, password, port, dataobject, sql, prj, alias, unique, editable_unique
            ).open(),
        )

    def get_destination(
        self,
        schema: Schema,
        dataobject: str,
        db_name: str,
        host: str,
        user: str,
        password: str,
        port: int = DEFAULT_PORT,
        export_params: Optional[ExportParameters] = None,
    ) -> Destination:
        """
        Создает назначение объекта данных.

            Args:
                schema: Схема таблицы.
                dataobject: Имя таблицы.
                db_name: Имя базы данных.
                host: Адрес сервера.
                user: Имя пользователя.
                password: Пароль.
                port: Порт.
                export_params: Дополнительные параметры экспорта.

        .. seealso::
            :ref:`export_db_index`
        """
        if not self._get_destination_class:
            raise NotImplementedError

        return self._get_destination_class(
            schema,
            Source._provider(self.id),
            {
                "host": host,
                "port": port,
                "db": db_name,
                "user": user,
                "password": password,
                "dataobject": dataobject,
                "exportParams": (export_params.to_dict() if export_params is not None else None),
            },
        )

    def create_open(
        self,
        schema: Schema,
        dataobject: str,
        db_name: str,
        host: str,
        user: str,
        password: str,
        port: int = DEFAULT_PORT,
        export_params: Optional[ExportParameters] = None,
    ) -> Table:
        """
        Создает и открывает объект данных.

        Args:
            schema: Схема таблицы.
            dataobject: Имя таблицы.
            db_name: Имя базы данных.
            host: Адрес сервера.
            user: Имя пользователя.
            password: Пароль.
            port: Порт.
            export_params: Дополнительные параметры экспорта.
        """
        return cast(
            Table,
            self.get_destination(schema, dataobject, db_name, host, user, password, port, export_params).create_open(),
        )
