import asyncio
from collections.abc import AsyncGenerator, Callable, Mapping, Sequence
from datetime import timedelta
from json.decoder import JSONDecodeError
from typing import TYPE_CHECKING, ClassVar, TypeGuard, Union

from cross_web import HTTPException, QuartHTTPRequestAdapter
from quart import Request, Response, Websocket, request, websocket
from quart.ctx import has_websocket_context
from quart.views import View

from strawberry.http.async_base_view import (
    AsyncBaseHTTPView,
    AsyncWebSocketAdapter,
)
from strawberry.http.exceptions import (
    NonJsonMessageReceived,
    NonTextMessageReceived,
    WebSocketDisconnected,
)
from strawberry.http.ides import GraphQL_IDE
from strawberry.http.typevars import Context, RootValue
from strawberry.subscriptions import GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL

if TYPE_CHECKING:
    from quart.typing import ResponseReturnValue

    from strawberry.http import GraphQLHTTPResponse
    from strawberry.schema.base import BaseSchema


class QuartWebSocketAdapter(AsyncWebSocketAdapter):
    def __init__(
        self, view: AsyncBaseHTTPView, request: Websocket, response: Response
    ) -> None:
        super().__init__(view)
        self.ws = request

    async def iter_json(
        self, *, ignore_parsing_errors: bool = False
    ) -> AsyncGenerator[object, None]:
        try:
            while True:
                # Raises asyncio.CancelledError when the connection is closed.
                # https://quart.palletsprojects.com/en/latest/how_to_guides/websockets.html#detecting-disconnection
                message = await self.ws.receive()

                if not isinstance(message, str):
                    raise NonTextMessageReceived

                try:
                    yield self.view.decode_json(message)
                except JSONDecodeError as e:
                    if not ignore_parsing_errors:
                        raise NonJsonMessageReceived from e
        except asyncio.CancelledError:
            pass

    async def send_json(self, message: Mapping[str, object]) -> None:
        try:
            # Raises asyncio.CancelledError when the connection is closed.
            # https://quart.palletsprojects.com/en/latest/how_to_guides/websockets.html#detecting-disconnection
            await self.ws.send(self.view.encode_json(message))  # type:ignore
            # quart is misusing AnyStr, leading to type errors for unions, see https://github.com/pallets/quart/issues/451
        except asyncio.CancelledError as exc:
            raise WebSocketDisconnected from exc

    async def close(self, code: int, reason: str) -> None:
        await self.ws.close(code, reason=reason)


class GraphQLView(
    AsyncBaseHTTPView[
        Request, Response, Response, Websocket, Response, Context, RootValue
    ],
    View,
):
    methods: ClassVar[list[str]] = ["GET", "POST"]
    allow_queries_via_get: bool = True
    request_adapter_class = QuartHTTPRequestAdapter
    websocket_adapter_class = QuartWebSocketAdapter  # type: ignore

    def __init__(
        self,
        schema: "BaseSchema",
        graphql_ide: GraphQL_IDE | None = "graphiql",
        allow_queries_via_get: bool = True,
        keep_alive: bool = True,
        keep_alive_interval: float = 1,
        subscription_protocols: Sequence[str] = (
            GRAPHQL_TRANSPORT_WS_PROTOCOL,
            GRAPHQL_WS_PROTOCOL,
        ),
        connection_init_wait_timeout: timedelta = timedelta(minutes=1),
        multipart_uploads_enabled: bool = False,
        max_subscriptions_per_connection: int | None = 100,
    ) -> None:
        self.schema = schema
        self.allow_queries_via_get = allow_queries_via_get
        self.keep_alive = keep_alive
        self.keep_alive_interval = keep_alive_interval
        self.subscription_protocols = subscription_protocols
        self.connection_init_wait_timeout = connection_init_wait_timeout
        self.multipart_uploads_enabled = multipart_uploads_enabled
        self.max_subscriptions_per_connection = max_subscriptions_per_connection
        self.graphql_ide = graphql_ide

    async def render_graphql_ide(self, request: Request) -> Response:
        return Response(self.graphql_ide_html)

    def create_response(
        self,
        response_data: Union["GraphQLHTTPResponse", list["GraphQLHTTPResponse"]],
        sub_response: Response,
    ) -> Response:
        sub_response.set_data(self.encode_json(response_data))

        return sub_response

    async def get_context(
        self, request: Request | Websocket, response: Response
    ) -> Context:
        return {"request": request, "response": response}  # type: ignore

    async def get_root_value(self, request: Request | Websocket) -> RootValue | None:
        return None

    async def get_sub_response(self, request: Request) -> Response:
        return Response(status=200, content_type="application/json")

    async def dispatch_request(self, **kwargs: object) -> "ResponseReturnValue":
        try:
            return await self.run(
                request=websocket if has_websocket_context() else request
            )
        except HTTPException as e:
            return Response(
                response=e.reason,
                status=e.status_code,
                content_type="text/plain",
            )

    async def create_streaming_response(
        self,
        request: Request,
        stream: Callable[[], AsyncGenerator[str, None]],
        sub_response: Response,
        headers: dict[str, str],
    ) -> Response:
        return (
            stream(),
            sub_response.status_code,
            {  # type: ignore
                **sub_response.headers,
                **headers,
            },
        )

    def is_websocket_request(
        self, request: Request | Websocket
    ) -> TypeGuard[Websocket]:
        return has_websocket_context()

    async def pick_websocket_subprotocol(self, request: Websocket) -> str | None:
        protocols = request.requested_subprotocols
        intersection = set(protocols) & set(self.subscription_protocols)
        sorted_intersection = sorted(intersection, key=protocols.index)
        return next(iter(sorted_intersection), None)

    async def create_websocket_response(
        self, request: Websocket, subprotocol: str | None
    ) -> Response:
        await request.accept(subprotocol=subprotocol)
        return Response()


__all__ = ["GraphQLView"]
