from abc import abstractmethod
from asyncio import iscoroutinefunction
from enum import Enum
from types import NoneType
from typing import Callable, Type, get_args, Any, Union

from ariadne import ObjectType, SubscriptionType
from graphql import GraphQLResolveInfo
from starlette.requests import Request
from typing_extensions import deprecated

from api.middleware.request import get_request
from api.route import Route
from api_graphql.abc.collection_filter_abc import CollectionFilterABC
from api_graphql.abc.input_abc import InputABC
from api_graphql.abc.sort_abc import Sort
from api_graphql.field.collection_field import CollectionField
from api_graphql.field.collection_field_builder import CollectionFieldBuilder
from api_graphql.field.dao_field import DaoField
from api_graphql.field.dao_field_builder import DaoFieldBuilder
from api_graphql.field.mutation_field import MutationField
from api_graphql.field.mutation_field_builder import MutationFieldBuilder
from api_graphql.field.resolver_field import ResolverField
from api_graphql.field.resolver_field_builder import ResolverFieldBuilder
from api_graphql.field.subscription_field import SubscriptionField
from api_graphql.service.collection_result import CollectionResult
from api_graphql.service.exceptions import (
    UnauthorizedException,
    PermissionDeniedException,
    AccessDenied,
)
from api_graphql.service.query_context import QueryContext
from api_graphql.typing import TRequireAnyPermissions, TRequireAnyResolvers
from core.logger import APILogger
from core.string import first_to_lower
from service.permission.permissions_enum import Permissions

logger = APILogger(__name__)


class QueryABC(ObjectType):
    __abstract__ = True

    @abstractmethod
    def __init__(self, name: str = __name__):
        ObjectType.__init__(self, name)
        self._subscriptions: dict[str, SubscriptionType] = {}

    @staticmethod
    async def _authorize(request: Request):
        if not await Route.is_authorized(request):
            raise UnauthorizedException()

    @staticmethod
    async def _require_any_permission(permissions: list[Permissions]):
        if permissions is None or len(permissions) == 0:
            return

        authenticated = await Route.get_authenticated_user_or_api_key()

        if all([await authenticated.has_permission(x) for x in permissions]):
            return

        raise PermissionDeniedException([x.value for x in permissions])

    @classmethod
    async def _require_any(
        cls,
        data: Any,
        permissions: TRequireAnyPermissions,
        resolvers: TRequireAnyResolvers,
        *args,
        **kwargs,
    ):
        if len(permissions) > 0:
            user = await Route.get_authenticated_user_or_api_key_or_default()
            if user is not None and all(
                [await user.has_permission(x) for x in permissions]
            ):
                return

        resolver_results = []
        for x in resolvers:
            user = await Route.get_authenticated_user_or_api_key_or_default()
            user_permissions = []
            if user is not None:
                user_permissions = await user.permissions

            if iscoroutinefunction(x):
                resolver_results.append(
                    await x(QueryContext(data, user, user_permissions, *args, **kwargs))
                )
            else:
                resolver_results.append(
                    x(QueryContext(data, user, user_permissions, *args, **kwargs))
                )

        if not any(resolver_results):
            raise AccessDenied()

    def field(
        self,
        builder: Union[
            DaoFieldBuilder,
            CollectionFieldBuilder,
            ResolverFieldBuilder,
            MutationFieldBuilder,
        ],
    ):
        """
        Add a field to the query
        :param FieldBuilder builder: Field builder
        :return:
        """
        field = builder.build()

        resolver = lambda *args, **kwargs: []

        async def dao_wrapper(*args, **kwargs):
            filters = []
            sorts = []
            take = None
            skip = None

            if field.default_filter:
                filters.append(await field.default_filter(*args, **kwargs))

            if field.filter_type and "filter" in kwargs:
                in_filters = kwargs["filter"]
                if not isinstance(in_filters, list):
                    in_filters = [in_filters]

                for f in in_filters:
                    filters.append(field.filter_type(f).to_attribute_filter())

            if field.sort_type and "sort" in kwargs:
                sorts = kwargs["sort"]

            if "take" in kwargs:
                take = kwargs["take"]

            if "skip" in kwargs:
                skip = kwargs["skip"]

            collection = await field.dao.find_by(filters, sorts, take, skip)
            if field.direct_result:
                return collection

            res = CollectionResult(
                await field.dao.count(filters), len(collection), collection
            )
            return res

        async def collection_wrapper(*args, **kwargs):
            if "filter" in kwargs:
                kwargs["filters"] = kwargs["filter"]
                del kwargs["filter"]

            if field.filter_type and "filters" in kwargs:
                filters = []
                for f in kwargs["filters"]:
                    filters.append(field.filter_type(f))

                kwargs["filters"] = filters
            else:
                kwargs["filters"] = None

            if field.sort_type and "sort" in kwargs:
                sorts = []
                for s in kwargs["sort"]:
                    sorts.append(field.sort_type(get_args(field.sort_type)[0], s))
                kwargs["sort"] = sorts
            else:
                kwargs["sort"] = None

            if iscoroutinefunction(field.resolver):
                collection = await field.resolver(*args)
            else:
                collection = field.resolver(*args)

            return self._resolve_collection(
                collection,
                *args,
                **kwargs,
            )

        async def resolver_wrapper(*args, **kwargs):
            result = (
                await field.resolver(*args, **kwargs)
                if iscoroutinefunction(field.resolver)
                else field.resolver(*args, **kwargs)
            )
            return result

        if isinstance(field, DaoField):
            resolver = dao_wrapper

        elif isinstance(field, CollectionField):
            resolver = collection_wrapper

        elif isinstance(field, ResolverField):
            resolver = resolver_wrapper

        elif isinstance(field, MutationField):

            async def input_wrapper(
                mutation: QueryABC, info: GraphQLResolveInfo, **kwargs
            ):
                if field.input_type is None:
                    return await resolver_wrapper(mutation, info, **kwargs)

                logger.debug(
                    f"{field.name}: {field.input_type.__name__} {kwargs[field.input_key]}"
                )
                input_obj = field.input_type(kwargs[field.input_key])
                del kwargs[field.input_key]

                return await resolver_wrapper(input_obj, mutation, info, **kwargs)

            resolver = input_wrapper

        elif isinstance(field, SubscriptionField):

            async def sub_wrapper(sub: QueryABC, info: GraphQLResolveInfo, **kwargs):
                return await resolver_wrapper(sub, info, **kwargs)

            resolver = sub_wrapper

        else:
            raise ValueError(f"Unknown field type: {field.name}")

        async def wrapper(*args, **kwargs):
            if not field.public:
                await self._authorize(get_request())

            if (
                field.require_any is None
                and not field.public
                and field.require_any_permission
            ):
                await self._require_any_permission(field.require_any_permission)

            if isinstance(field, MutationField):
                if field.require_any is not None:
                    await self._require_any(
                        None,
                        *field.require_any,
                        *args,
                        **kwargs,
                    )

                result = await resolver(*args, **kwargs)
                return result

            result = await resolver(*args, **kwargs)

            if field.require_any is not None:
                await self._require_any(
                    result,
                    *field.require_any,
                    *args,
                    **kwargs,
                )

            return result

        self.set_field(field.name, wrapper)

    @deprecated("Use field(FieldBuilder()) instead")
    def mutation(
        self,
        name: str,
        f: Callable,
        input_type: Type[InputABC] = None,
        input_key: str = "input",
        require_any_permission: list[Permissions] = None,
        public: bool = False,
    ):
        """
        Adds a mutation to the query
        :param str name: Name of the graphql field
        :param Callable f: Mutation function
        :param Type[InputABC] input_type: Type of the input
        :param str input_key: Key of the input in the arguments
        :param list[Permissions] require_any_permission: List of permissions required to access the field
        :param bool public: Define if the field can resolve without authentication
        :return:
        """

        self.field(
            MutationFieldBuilder(name)
            .with_resolver(f)
            .with_change_broadcast(
                f"{first_to_lower(self.name.replace("Mutation", ""))}Change"
            )
            .with_input(input_type, input_key)
            .with_require_any_permission(require_any_permission)
            .with_public(public)
        )

    @classmethod
    def _resolve_collection(
        cls,
        collection: list,
        *_,
        filters: list[CollectionFilterABC] = None,
        sort: list[Sort] = None,
        skip: int = None,
        take: int = None,
    ) -> CollectionResult:
        total_count = len(collection)

        if filters is not None and len(filters) > 0:
            for f in filters:
                collection = list(filter(lambda x: f.filter(x), collection))

            total_count = len(collection)

        if sort is not None:

            def f_sort(x: object, k: str):
                attr = getattr(x, k)
                if isinstance(attr, Enum):
                    return attr.value

                if isinstance(attr, NoneType):
                    return 0

                return attr

            for s in reversed(
                sort
            ):  # Apply sorting in reverse order to make first primary "orderBy" and other secondary "thenBy"
                attrs = [a for a in dir(s) if not a.startswith("_")]
                for k in attrs:
                    collection = sorted(
                        collection,
                        key=lambda x: f_sort(x, k),
                        reverse=getattr(s, k) == "DESC",
                    )

        if skip is not None:
            collection = collection[skip:]

        if take is not None:
            collection = collection[:take]

        return CollectionResult(total_count, len(collection), collection)
