import typing as t

from sqlalchemy import delete, insert, select, update

from ash_dal.constants import PAGINATOR_FIRST_PAGE_INDEX
from ash_dal.dao.mixin import BaseDAOMixin
from ash_dal.database import Database
from ash_dal.typing import Entity
from ash_dal.utils import Paginator, SyncCursorPaginator
from ash_dal.utils.paginator import PaginatorPage
from ash_dal.utils.paginator.interface import PaginatorFactoryProtocol


class BaseDAO(BaseDAOMixin[Entity]):
    __paginator_factory__: PaginatorFactoryProtocol = Paginator

    def __init__(self, database: Database) -> None:
        self._db = database

    @property
    def db(self) -> Database:
        """
        Property returns an instance of :class:`Database` class
        :return: DB instance
        """
        return self._db

    def get_by_pk(self, pk: t.Any) -> Entity | None:
        """
        Using this method you can fetch an entity by its primary key
        :param pk: the record's primary key value
        :return: Entity instance or None if the record is not found
        """
        with self.db.session as session:
            db_item = session.get(self.__model__, pk, options=self.__default_load_options__)
            if not db_item:
                return None
            return self._convert_db_item_in_entity(db_item=db_item)

    def all(self) -> tuple[Entity, ...]:
        """
        Using this method you can fetch all entities from the database
        :return: a tuple with entities
        """
        with self.db.session as session:
            db_items = session.scalars(select(self.__model__).options(*self.__default_load_options__))
            if self.__default_load_options__:
                db_items = db_items.unique().all()
            return self._get_entities_from_db_items(db_items=db_items)

    def stream(
        self,
        specification: dict[str, t.Any] | None = None,
        batch_size: int | None = None,
    ) -> t.Iterator[Entity]:
        """
        Streams entities from the database one by one.
        :param specification: Can be used to filter entities.
        :param batch_size: Numeric value that controls how many rows are fetched per network roundtrip.
        :return: an iterator with entities.
        """
        query = select(self.__model__)
        if specification:
            query = query.filter_by(**specification)

        effective_batch_size = batch_size or self.__default_page_size__
        with self.db.session as session:
            if not self.__default_load_options__:
                # Without eager loaders we can use true cursor streaming end-to-end.
                stream_stmt = query.execution_options(
                    stream_results=True,
                    yield_per=effective_batch_size,
                )
                db_items = session.scalars(stream_stmt)
                for db_item in db_items:
                    yield self._convert_db_item_in_entity(db_item=db_item)
                return

            # joinedload/subqueryload on collections are incompatible with yield_per.
            # To keep memory bounded, stream PKs with keyset pagination and hydrate one PK batch at a time.
            cursor_paginator = SyncCursorPaginator(
                session=session,
                query=query,
                model=self.__model__,
                page_size=effective_batch_size,
                load_options=self.__default_load_options__,
            )
            for page in cursor_paginator.paginate():
                for db_item in page.items:
                    yield self._convert_db_item_in_entity(db_item=db_item)

    def get_page(
        self,
        page_index: int = PAGINATOR_FIRST_PAGE_INDEX,
        page_size: int | None = None,
        specification: dict[str, t.Any] | None = None,
    ) -> PaginatorPage[Entity]:
        """
        Fetch a page with entities by page index. If the index is out of range, an empty page will be returned.
        :param specification: Can be used to filter the entities you want to receive.
        :param page_index: Numeric value. Index starts from 0
        :param page_size: Numeric value. Defines size of the page that will be returned
        :return: An instance of :class:`PaginatorPage` that includes entities.
        """
        query = select(self.__model__).options(*self.__default_load_options__)
        if specification:
            query = query.filter_by(**specification)

        with self.db.session as session:
            paginator = self.__paginator_factory__(
                session=session,
                query=query,
                page_size=page_size or self.__default_page_size__,
            )
            page = paginator.get_page(page_index=page_index)
            entities = self._get_entities_from_db_items(db_items=page)
            return PaginatorPage(index=page_index, items=entities, pages_count=paginator.size)

    def paginate(
        self,
        specification: dict[str, t.Any] | None = None,
        page_size: int | None = None,
    ) -> t.Iterator[PaginatorPage[Entity]]:
        """
        An iterator that returns pages with entities.
        :param specification: Can be used to filter the entities you want to receive.
        :param page_size: Numeric value. Defines size of pages that will be returned
        :return: :class:`t.Iterator` that returns :class:`PaginatorPage` with entities
        """
        with self.db.session as session:
            query = select(self.__model__).options(*self.__default_load_options__)
            if specification:
                query = query.filter_by(**specification)
            paginator = self.__paginator_factory__(
                session=session,
                query=query,
                page_size=page_size or self.__default_page_size__,
            )
            for page_index, page in enumerate(paginator.paginate()):
                entities = self._get_entities_from_db_items(db_items=page)
                yield PaginatorPage(index=page_index, items=entities, pages_count=paginator.size)

    def filter(self, specification: dict[str, t.Any]) -> tuple[Entity, ...]:
        """
        Fetch entities from database by specification.
        :param specification: a dict that will be used for filtering
        :return: a tuple with entities
        """
        with self.db.session as session:
            db_items = session.scalars(
                select(self.__model__).filter_by(**specification).options(*self.__default_load_options__),
            )
            if self.__default_load_options__:
                db_items = db_items.unique().all()
            return self._get_entities_from_db_items(db_items=db_items)

    def create(self, data: dict[str, t.Any]) -> Entity:
        """
        Create an entity in database
        :param data: a dict that represents entity to be created.
        :return: a created entity instance
        """
        with self.db.session as session:
            result = session.execute(insert(self.__model__).values(**data))
            session.commit()
            if (pks := result.inserted_primary_key) is None:  # pyright: ignore [reportUnknownMemberType, reportAttributeAccessIssue, reportUnknownVariableType]
                msg = "Inserted primary key is not found"
                raise ValueError(msg)
            pk_dict: dict[str, t.Any] = pks._asdict()  # pyright: ignore [reportUnknownVariableType, reportUnknownMemberType]
            response_data = {**data, **pk_dict}
            return self._dict_to_entity(dict_=response_data)

    def bulk_create(self, data: t.Sequence[dict[str, t.Any]]) -> None:
        """
        Create multiple entities within one query
        :param data: a sequence with dicts that represent entities to be created
        """
        with self.db.session as session:
            session.execute(insert(self.__model__), data)
            session.commit()

    def update(self, specification: dict[str, t.Any], update_data: dict[str, t.Any]) -> bool:
        """
        Patch record(s)
        :param specification: record(s) for updating are chosen based on this specification. If an empy dict is passed,
        a :class:`ValueError` exception will be raised for safety reasons.
        :param update_data: a dict with new values to be written for the chosen record(s)
        :return: a :class:`bool` value that shows either the entity(ies) are updated or not.
        """
        if not specification:
            msg = "Specification should be passed"
            raise ValueError(msg)
        with self.db.session as session:
            result = session.execute(update(self.__model__).filter_by(**specification).values(update_data))
            session.commit()
            return bool(result.rowcount)  # pyright: ignore [reportUnknownMemberType, reportUnknownArgumentType, reportAttributeAccessIssue]

    def delete(self, specification: dict[str, t.Any]) -> bool:
        """
        Remove record(s).
        :param specification: record(s) for removing are chosen based on this specification. If an empy dict is passed,
        a :class:`ValueError` exception will be raised for safety reasons.
        :return: a :class:`bool` value that shows either the entity(ies) are removed or not.
        """
        if not specification:
            msg = "Specification should be passed"
            raise ValueError(msg)
        with self.db.session as session:
            result = session.execute(delete(self.__model__).filter_by(**specification))
            session.commit()
            return bool(result.rowcount)  # pyright: ignore [reportUnknownMemberType, reportUnknownArgumentType, reportAttributeAccessIssue]
