from abc import abstractmethod
from typing import Type, Union, Any

from api_graphql.abc.query_abc import QueryABC
from api_graphql.field.mutation_field_builder import MutationFieldBuilder
from core.database.abc.data_access_object_abc import DataAccessObjectABC
from core.database.abc.db_join_model_abc import DbJoinModelABC
from core.typing import T
from service.permission.permissions_enum import Permissions


class MutationABC(QueryABC):
    __abstract__ = True

    @abstractmethod
    def __init__(self, name: str = __name__):
        QueryABC.__init__(self, f"{name}Mutation")

    def add_mutation_type(
        self,
        name: str,
        mutation_name: str,
        require_any_permission=None,
        require_any=None,
        public: bool = False,
    ):
        """
        Add mutation type (sub mutation) to the mutation object
        :param str name: GraphQL mutation name
        :param str mutation_name: Internal (class) mutation name without "Mutation" suffix
        :param list[Permissions] require_any_permission: List of permissions required to access the field
        :param tuple[list[Permissions], list[callable]] require_any: List of permissions and resolvers required to access the field
        :param bool public: Define if the field can resolve without authentication
        :return:
        """
        from api_graphql.definition import QUERIES

        field = (
            MutationFieldBuilder(name)
            .with_resolver(
                lambda *args, **kwargs: [
                    x for x in QUERIES if x.name == f"{mutation_name}Mutation"
                ][0]
            )
            .with_public(public)
        )

        if require_any_permission is not None:
            field.with_require_any_permission(require_any_permission)

        if require_any is not None:
            field.with_require_any(*require_any)

        self.field(field)

    @staticmethod
    async def _resolve_assignments(
        foreign_objs: list[int],
        resolved_obj: T,
        reference_key_own: Union[str, property],
        reference_key_foreign: Union[str, property],
        source_dao: DataAccessObjectABC[T],
        join_dao: DataAccessObjectABC[T],
        join_type: Type[DbJoinModelABC],
        foreign_dao: DataAccessObjectABC[T],
    ):
        if foreign_objs is None:
            return

        reference_key_own_attr = reference_key_own
        if isinstance(reference_key_own, property):
            reference_key_own_attr = reference_key_own.fget.__name__

        reference_key_foreign_attr = reference_key_foreign
        if isinstance(reference_key_foreign, property):
            reference_key_foreign_attr = reference_key_foreign.fget.__name__

        foreign_list = await join_dao.find_by(
            [{reference_key_own: resolved_obj.id}, {"deleted": False}]
        )

        to_delete = (
            foreign_list
            if len(foreign_objs) == 0
            else await join_dao.find_by(
                [
                    {reference_key_own: resolved_obj.id},
                    {reference_key_foreign: {"notIn": foreign_objs}},
                ]
            )
        )
        foreign_ids = [getattr(x, reference_key_foreign_attr) for x in foreign_list]
        deleted_foreign_ids = [
            getattr(x, reference_key_foreign_attr)
            for x in await join_dao.find_by(
                [{reference_key_own: resolved_obj.id}, {"deleted": True}]
            )
        ]

        to_create = [
            join_type(0, resolved_obj.id, x)
            for x in foreign_objs
            if x not in foreign_ids and x not in deleted_foreign_ids
        ]
        to_restore = [
            await join_dao.get_single_by(
                [
                    {reference_key_own: resolved_obj.id},
                    {reference_key_foreign: x},
                ]
            )
            for x in foreign_objs
            if x not in foreign_ids and x in deleted_foreign_ids
        ]

        if len(to_delete) > 0:
            await join_dao.delete_many(to_delete)

        if len(to_create) > 0:
            await join_dao.create_many(to_create)

        if len(to_restore) > 0:
            await join_dao.restore_many(to_restore)

        foreign_changes = [*to_delete, *to_create, *to_restore]
        if len(foreign_changes) > 0:
            await source_dao.touch(resolved_obj)
            await foreign_dao.touch_many_by_id(
                [getattr(x, reference_key_foreign_attr) for x in foreign_changes]
            )
