from __future__ import annotations

from ast import NodeVisitor, Return, parse
from collections.abc import Iterable
from contextlib import suppress
from inspect import getsource, signature
from textwrap import dedent
from typing import (
    Any,
    Callable,
    cast,
)

from sanic_routing.route import Route

from sanic.base.meta import SanicMeta
from sanic.constants import HTTP_METHODS
from sanic.errorpages import RESPONSE_MAPPING
from sanic.mixins.base import BaseMixin
from sanic.models.futures import FutureRoute, FutureStatic
from sanic.models.handler_types import RouteHandler
from sanic.types import HashableDict


RouteWrapper = Callable[
    [RouteHandler], RouteHandler | tuple[Route, RouteHandler]
]


class RouteMixin(BaseMixin, metaclass=SanicMeta):
    def __init__(self, *args, **kwargs) -> None:
        self._future_routes: set[FutureRoute] = set()
        self._future_statics: set[FutureStatic] = set()

    def _apply_route(self, route: FutureRoute) -> list[Route]:
        raise NotImplementedError  # noqa

    def route(
        self,
        uri: str,
        methods: Iterable[str] | None = None,
        host: str | list[str] | None = None,
        strict_slashes: bool | None = None,
        stream: bool = False,
        version: int | str | float | None = None,
        name: str | None = None,
        ignore_body: bool = False,
        apply: bool = True,
        subprotocols: list[str] | None = None,
        websocket: bool = False,
        unquote: bool = False,
        static: bool = False,
        version_prefix: str = "/v",
        error_format: str | None = None,
        **ctx_kwargs: Any,
    ) -> RouteWrapper:
        """Decorate a function to be registered as a route.

        Args:
            uri (str): Path of the URL.
            methods (Optional[Iterable[str]]): List or tuple of
                methods allowed.
            host (Optional[Union[str, List[str]]]): The host, if required.
            strict_slashes (Optional[bool]): Whether to apply strict slashes
                to the route.
            stream (bool): Whether to allow the request to stream its body.
            version (Optional[Union[int, str, float]]): Route specific
                versioning.
            name (Optional[str]): User-defined route name for url_for.
            ignore_body (bool): Whether the handler should ignore request
                body (e.g. `GET` requests).
            apply (bool): Apply middleware to the route.
            subprotocols (Optional[List[str]]): List of subprotocols.
            websocket (bool): Enable WebSocket support.
            unquote (bool): Unquote special characters in the URL path.
            static (bool): Enable static route.
            version_prefix (str): URL path that should be before the version
                 value; default: `"/v"`.
            error_format (Optional[str]): Error format for the route.
            ctx_kwargs (Any): Keyword arguments that begin with a `ctx_*`
                prefix will be appended to the route context (`route.ctx`).

        Returns:
            RouteWrapper: Tuple of routes, decorated function.

        Examples:
            Using the method to define a GET endpoint:

            ```python
            @app.route("/hello")
            async def hello(request: Request):
                return text("Hello, World!")
            ```

            Adding context kwargs to the route:

            ```python
            @app.route("/greet", ctx_name="World")
            async def greet(request: Request):
                name = request.route.ctx.name
                return text(f"Hello, {name}!")
            ```
        """

        # Fix case where the user did not prefix the URL with a /
        # and will probably get confused as to why it's not working
        if not uri.startswith("/") and (uri or hasattr(self, "router")):
            uri = "/" + uri

        if strict_slashes is None:
            strict_slashes = self.strict_slashes

        if not methods and not websocket:
            methods = frozenset({"GET"})

        route_context = self._build_route_context(ctx_kwargs)

        def decorator(handler):
            nonlocal uri
            nonlocal methods
            nonlocal host
            nonlocal strict_slashes
            nonlocal stream
            nonlocal version
            nonlocal name
            nonlocal ignore_body
            nonlocal subprotocols
            nonlocal websocket
            nonlocal static
            nonlocal version_prefix
            nonlocal error_format

            if isinstance(handler, tuple):
                # if a handler fn is already wrapped in a route, the handler
                # variable will be a tuple of (existing routes, handler fn)
                _, handler = handler

            name = self.generate_name(name, handler)

            if isinstance(host, str):
                host = frozenset([host])
            elif host and not isinstance(host, frozenset):
                try:
                    host = frozenset(host)
                except TypeError:
                    raise ValueError(
                        "Expected either string or Iterable of host strings, "
                        "not %s" % host
                    )
            if isinstance(subprotocols, list):
                # Ordered subprotocols, maintain order
                subprotocols = tuple(subprotocols)
            elif isinstance(subprotocols, set):
                # subprotocol is unordered, keep it unordered
                subprotocols = frozenset(subprotocols)

            if not error_format or error_format == "auto":
                error_format = self._determine_error_format(handler)

            route = FutureRoute(
                handler,
                uri,
                None if websocket else frozenset([x.upper() for x in methods]),
                host,
                strict_slashes,
                stream,
                version,
                name,
                ignore_body,
                websocket,
                subprotocols,
                unquote,
                static,
                version_prefix,
                error_format,
                route_context,
            )
            overwrite = getattr(self, "_allow_route_overwrite", False)
            if overwrite:
                self._future_routes = set(
                    filter(lambda x: x.uri != uri, self._future_routes)
                )
            self._future_routes.add(route)

            args = list(signature(handler).parameters.keys())
            if websocket and len(args) < 2:
                handler_name = handler.__name__

                raise ValueError(
                    f"Required parameter `request` and/or `ws` missing "
                    f"in the {handler_name}() route?"
                )
            elif not args:
                handler_name = handler.__name__

                raise ValueError(
                    f"Required parameter `request` missing "
                    f"in the {handler_name}() route?"
                )

            if not websocket and stream:
                handler.is_stream = stream

            if apply:
                self._apply_route(route, overwrite=overwrite)

            if static:
                return route, handler
            return handler

        return decorator

    def add_route(
        self,
        handler: RouteHandler,
        uri: str,
        methods: Iterable[str] = frozenset({"GET"}),
        host: str | list[str] | None = None,
        strict_slashes: bool | None = None,
        version: int | str | float | None = None,
        name: str | None = None,
        stream: bool = False,
        version_prefix: str = "/v",
        error_format: str | None = None,
        unquote: bool = False,
        **ctx_kwargs: Any,
    ) -> RouteHandler:
        """A helper method to register class-based view or functions as a handler to the application url routes.

        Args:
            handler (RouteHandler): Function or class-based view used as a route handler.
            uri (str): Path of the URL.
            methods (Iterable[str]): List or tuple of methods allowed; these are overridden if using an HTTPMethodView.
            host (Optional[Union[str, List[str]]]): Hostname or hostnames to match for this route.
            strict_slashes (Optional[bool]): If set, a route's slashes will be strict. E.g. `/foo` will not match `/foo/`.
            version (Optional[Union[int, str, float]]): Version of the API for this route.
            name (Optional[str]): User-defined route name for `url_for`.
            stream (bool): Boolean specifying if the handler is a stream handler.
            version_prefix (str): URL path that should be before the version value; default: ``/v``.
            error_format (Optional[str]): Custom error format string.
            unquote (bool): Boolean specifying if the handler requires unquoting.
            ctx_kwargs (Any): Keyword arguments that begin with a `ctx_*` prefix will be appended to the route context (``route.ctx``). See below for examples.

        Returns:
            RouteHandler: The route handler.

        Examples:
            ```python
            from sanic import Sanic, text

            app = Sanic("test")

            async def handler(request):
                return text("OK")

            app.add_route(handler, "/test", methods=["GET", "POST"])
            ```

            You can use `ctx_kwargs` to add custom context to the route. This
            can often be useful when wanting to add metadata to a route that
            can be used by other parts of the application (like middleware).

            ```python
            from sanic import Sanic, text

            app = Sanic("test")

            async def handler(request):
                return text("OK")

            async def custom_middleware(request):
                if request.route.ctx.monitor:
                    do_some_monitoring()

            app.add_route(handler, "/test", methods=["GET", "POST"], ctx_monitor=True)
            app.register_middleware(custom_middleware)
        """  # noqa: E501
        # Handle HTTPMethodView differently
        if hasattr(handler, "view_class"):
            methods = set()

            for method in HTTP_METHODS:
                view_class = getattr(handler, "view_class")
                _handler = getattr(view_class, method.lower(), None)
                if _handler:
                    methods.add(method)
                    if hasattr(_handler, "is_stream"):
                        stream = True

        if strict_slashes is None:
            strict_slashes = self.strict_slashes

        self.route(
            uri=uri,
            methods=methods,
            host=host,
            strict_slashes=strict_slashes,
            stream=stream,
            version=version,
            name=name,
            version_prefix=version_prefix,
            error_format=error_format,
            unquote=unquote,
            **ctx_kwargs,
        )(handler)
        return handler

    # Shorthand method decorators
    def get(
        self,
        uri: str,
        host: str | list[str] | None = None,
        strict_slashes: bool | None = None,
        version: int | str | float | None = None,
        name: str | None = None,
        ignore_body: bool = True,
        version_prefix: str = "/v",
        error_format: str | None = None,
        **ctx_kwargs: Any,
    ) -> RouteHandler:
        """Decorate a function handler to create a route definition using the **GET** HTTP method.

        Args:
            uri (str): URL to be tagged to GET method of HTTP.
            host (Optional[Union[str, List[str]]]): Host IP or FQDN for
                the service to use.
            strict_slashes (Optional[bool]): Instruct Sanic to check if the
                request URLs need to terminate with a `/`.
            version (Optional[Union[int, str, float]]): API Version.
            name (Optional[str]): Unique name that can be used to identify
                the route.
            ignore_body (bool): Whether the handler should ignore request
                body. This means the body of the request, if sent, will not
                be consumed. In that instance, you will see a warning in
                the logs. Defaults to `True`, meaning do not consume the body.
            version_prefix (str): URL path that should be before the version
                value. Defaults to `"/v"`.
            error_format (Optional[str]): Custom error format string.
            **ctx_kwargs (Any): Keyword arguments that begin with a
                `ctx_* prefix` will be appended to the route
                context (`route.ctx`).

        Returns:
            RouteHandler: Object decorated with route method.
        """  # noqa: E501
        return cast(
            RouteHandler,
            self.route(
                uri,
                methods=frozenset({"GET"}),
                host=host,
                strict_slashes=strict_slashes,
                version=version,
                name=name,
                ignore_body=ignore_body,
                version_prefix=version_prefix,
                error_format=error_format,
                **ctx_kwargs,
            ),
        )

    def post(
        self,
        uri: str,
        host: str | list[str] | None = None,
        strict_slashes: bool | None = None,
        stream: bool = False,
        version: int | str | float | None = None,
        name: str | None = None,
        version_prefix: str = "/v",
        error_format: str | None = None,
        **ctx_kwargs: Any,
    ) -> RouteHandler:
        """Decorate a function handler to create a route definition using the **POST** HTTP method.

        Args:
            uri (str): URL to be tagged to POST method of HTTP.
            host (Optional[Union[str, List[str]]]): Host IP or FQDN for
                the service to use.
            strict_slashes (Optional[bool]): Instruct Sanic to check if the
                request URLs need to terminate with a `/`.
            stream (bool): Whether or not to stream the request body.
                Defaults to `False`.
            version (Optional[Union[int, str, float]]): API Version.
            name (Optional[str]): Unique name that can be used to identify
                the route.
            version_prefix (str): URL path that should be before the version
                value. Defaults to `"/v"`.
            error_format (Optional[str]): Custom error format string.
            **ctx_kwargs (Any): Keyword arguments that begin with a
                `ctx_*` prefix will be appended to the route
                context (`route.ctx`).

        Returns:
            RouteHandler: Object decorated with route method.
        """  # noqa: E501
        return cast(
            RouteHandler,
            self.route(
                uri,
                methods=frozenset({"POST"}),
                host=host,
                strict_slashes=strict_slashes,
                stream=stream,
                version=version,
                name=name,
                version_prefix=version_prefix,
                error_format=error_format,
                **ctx_kwargs,
            ),
        )

    def put(
        self,
        uri: str,
        host: str | list[str] | None = None,
        strict_slashes: bool | None = None,
        stream: bool = False,
        version: int | str | float | None = None,
        name: str | None = None,
        version_prefix: str = "/v",
        error_format: str | None = None,
        **ctx_kwargs: Any,
    ) -> RouteHandler:
        """Decorate a function handler to create a route definition using the **PUT** HTTP method.

        Args:
            uri (str): URL to be tagged to PUT method of HTTP.
            host (Optional[Union[str, List[str]]]): Host IP or FQDN for
                the service to use.
            strict_slashes (Optional[bool]): Instruct Sanic to check if the
                request URLs need to terminate with a `/`.
            stream (bool): Whether or not to stream the request body.
                Defaults to `False`.
            version (Optional[Union[int, str, float]]): API Version.
            name (Optional[str]): Unique name that can be used to identify
                the route.
            version_prefix (str): URL path that should be before the version
                value. Defaults to `"/v"`.
            error_format (Optional[str]): Custom error format string.
            **ctx_kwargs (Any): Keyword arguments that begin with a
                `ctx_*` prefix will be appended to the route
                context (`route.ctx`).

        Returns:
            RouteHandler: Object decorated with route method.
        """  # noqa: E501
        return cast(
            RouteHandler,
            self.route(
                uri,
                methods=frozenset({"PUT"}),
                host=host,
                strict_slashes=strict_slashes,
                stream=stream,
                version=version,
                name=name,
                version_prefix=version_prefix,
                error_format=error_format,
                **ctx_kwargs,
            ),
        )

    def head(
        self,
        uri: str,
        host: str | list[str] | None = None,
        strict_slashes: bool | None = None,
        version: int | str | float | None = None,
        name: str | None = None,
        ignore_body: bool = True,
        version_prefix: str = "/v",
        error_format: str | None = None,
        **ctx_kwargs: Any,
    ) -> RouteHandler:
        """Decorate a function handler to create a route definition using the **HEAD** HTTP method.

        Args:
            uri (str): URL to be tagged to HEAD method of HTTP.
            host (Optional[Union[str, List[str]]]): Host IP or FQDN for
                the service to use.
            strict_slashes (Optional[bool]): Instruct Sanic to check if the
                request URLs need to terminate with a `/`.
            version (Optional[Union[int, str, float]]): API Version.
            name (Optional[str]): Unique name that can be used to identify
                the route.
            ignore_body (bool): Whether the handler should ignore request
                body. This means the body of the request, if sent, will not
                be consumed. In that instance, you will see a warning in
                the logs. Defaults to `True`, meaning do not consume the body.
            version_prefix (str): URL path that should be before the version
                value. Defaults to `"/v"`.
            error_format (Optional[str]): Custom error format string.
            **ctx_kwargs (Any): Keyword arguments that begin with a
                `ctx_*` prefix will be appended to the route
                context (`route.ctx`).

        Returns:
            RouteHandler: Object decorated with route method.
        """  # noqa: E501
        return cast(
            RouteHandler,
            self.route(
                uri,
                methods=frozenset({"HEAD"}),
                host=host,
                strict_slashes=strict_slashes,
                version=version,
                name=name,
                ignore_body=ignore_body,
                version_prefix=version_prefix,
                error_format=error_format,
                **ctx_kwargs,
            ),
        )

    def options(
        self,
        uri: str,
        host: str | list[str] | None = None,
        strict_slashes: bool | None = None,
        version: int | str | float | None = None,
        name: str | None = None,
        ignore_body: bool = True,
        version_prefix: str = "/v",
        error_format: str | None = None,
        **ctx_kwargs: Any,
    ) -> RouteHandler:
        """Decorate a function handler to create a route definition using the **OPTIONS** HTTP method.

        Args:
            uri (str): URL to be tagged to OPTIONS method of HTTP.
            host (Optional[Union[str, List[str]]]): Host IP or FQDN for
                the service to use.
            strict_slashes (Optional[bool]): Instruct Sanic to check if the
                request URLs need to terminate with a `/`.
            version (Optional[Union[int, str, float]]): API Version.
            name (Optional[str]): Unique name that can be used to identify
                the route.
            ignore_body (bool): Whether the handler should ignore request
                body. This means the body of the request, if sent, will not
                be consumed. In that instance, you will see a warning in
                the logs. Defaults to `True`, meaning do not consume the body.
            version_prefix (str): URL path that should be before the version
                value. Defaults to `"/v"`.
            error_format (Optional[str]): Custom error format string.
            **ctx_kwargs (Any): Keyword arguments that begin with a
                `ctx_*` prefix will be appended to the route
                context (`route.ctx`).

        Returns:
            RouteHandler: Object decorated with route method.
        """  # noqa: E501
        return cast(
            RouteHandler,
            self.route(
                uri,
                methods=frozenset({"OPTIONS"}),
                host=host,
                strict_slashes=strict_slashes,
                version=version,
                name=name,
                ignore_body=ignore_body,
                version_prefix=version_prefix,
                error_format=error_format,
                **ctx_kwargs,
            ),
        )

    def patch(
        self,
        uri: str,
        host: str | list[str] | None = None,
        strict_slashes: bool | None = None,
        stream=False,
        version: int | str | float | None = None,
        name: str | None = None,
        version_prefix: str = "/v",
        error_format: str | None = None,
        **ctx_kwargs: Any,
    ) -> RouteHandler:
        """Decorate a function handler to create a route definition using the **PATCH** HTTP method.

        Args:
            uri (str): URL to be tagged to PATCH method of HTTP.
            host (Optional[Union[str, List[str]]]): Host IP or FQDN for
                the service to use.
            strict_slashes (Optional[bool]): Instruct Sanic to check if the
                request URLs need to terminate with a `/`.
            stream (bool): Set to `True` if full request streaming is needed,
                `False` otherwise. Defaults to `False`.
            version (Optional[Union[int, str, float]]): API Version.
            name (Optional[str]): Unique name that can be used to identify
                the route.
            version_prefix (str): URL path that should be before the version
                value. Defaults to `"/v"`.
            error_format (Optional[str]): Custom error format string.
            **ctx_kwargs (Any): Keyword arguments that begin with a
                `ctx_*` prefix will be appended to the route
                context (`route.ctx`).

        Returns:
            RouteHandler: Object decorated with route method.
        """  # noqa: E501
        return cast(
            RouteHandler,
            self.route(
                uri,
                methods=frozenset({"PATCH"}),
                host=host,
                strict_slashes=strict_slashes,
                stream=stream,
                version=version,
                name=name,
                version_prefix=version_prefix,
                error_format=error_format,
                **ctx_kwargs,
            ),
        )

    def delete(
        self,
        uri: str,
        host: str | list[str] | None = None,
        strict_slashes: bool | None = None,
        version: int | str | float | None = None,
        name: str | None = None,
        ignore_body: bool = False,
        version_prefix: str = "/v",
        error_format: str | None = None,
        **ctx_kwargs: Any,
    ) -> RouteHandler:
        """Decorate a function handler to create a route definition using the **DELETE** HTTP method.

        Args:
            uri (str): URL to be tagged to the DELETE method of HTTP.
            host (Optional[Union[str, List[str]]]): Host IP or FQDN for the
                service to use.
            strict_slashes (Optional[bool]): Instruct Sanic to check if the
                request URLs need to terminate with a */*.
            version (Optional[Union[int, str, float]]): API Version.
            name (Optional[str]): Unique name that can be used to identify
                the Route.
            ignore_body (bool): Whether or not to ignore the body in the
                request. Defaults to `False`.
            version_prefix (str): URL path that should be before the version
                value. Defaults to `"/v"`.
            error_format (Optional[str]): Custom error format string.
            **ctx_kwargs (Any): Keyword arguments that begin with a `ctx_*`
                prefix will be appended to the route context (`route.ctx`).

        Returns:
            RouteHandler: Object decorated with route method.
        """  # noqa: E501
        return cast(
            RouteHandler,
            self.route(
                uri,
                methods=frozenset({"DELETE"}),
                host=host,
                strict_slashes=strict_slashes,
                version=version,
                name=name,
                ignore_body=ignore_body,
                version_prefix=version_prefix,
                error_format=error_format,
                **ctx_kwargs,
            ),
        )

    def websocket(
        self,
        uri: str,
        host: str | list[str] | None = None,
        strict_slashes: bool | None = None,
        subprotocols: list[str] | None = None,
        version: int | str | float | None = None,
        name: str | None = None,
        apply: bool = True,
        version_prefix: str = "/v",
        error_format: str | None = None,
        **ctx_kwargs: Any,
    ):
        """Decorate a function to be registered as a websocket route.

        Args:
            uri (str): Path of the URL.
            host (Optional[Union[str, List[str]]]): Host IP or FQDN details.
            strict_slashes (Optional[bool]): If the API endpoint needs to
                terminate with a `"/"` or not.
            subprotocols (Optional[List[str]]): Optional list of str with
                supported subprotocols.
            version (Optional[Union[int, str, float]]): WebSocket
                protocol version.
            name (Optional[str]): A unique name assigned to the URL so that
                it can be used with url_for.
            apply (bool): If set to False, it doesn't apply the route to the
                app. Default is `True`.
            version_prefix (str): URL path that should be before the version
                value. Defaults to `"/v"`.
            error_format (Optional[str]): Custom error format string.
            **ctx_kwargs (Any): Keyword arguments that begin with
                a `ctx_* prefix` will be appended to the route
                context (`route.ctx`).

        Returns:
            tuple: Tuple of routes, decorated function.
        """
        return self.route(
            uri=uri,
            host=host,
            methods=None,
            strict_slashes=strict_slashes,
            version=version,
            name=name,
            apply=apply,
            subprotocols=subprotocols,
            websocket=True,
            version_prefix=version_prefix,
            error_format=error_format,
            **ctx_kwargs,
        )

    def add_websocket_route(
        self,
        handler,
        uri: str,
        host: str | list[str] | None = None,
        strict_slashes: bool | None = None,
        subprotocols=None,
        version: int | str | float | None = None,
        name: str | None = None,
        version_prefix: str = "/v",
        error_format: str | None = None,
        **ctx_kwargs: Any,
    ):
        """A helper method to register a function as a websocket route.

        Args:
            handler (Callable): A callable function or instance of a class
                that can handle the websocket request.
            uri (str): URL path that will be mapped to the websocket handler.
            host (Optional[Union[str, List[str]]]): Host IP or FQDN details.
            strict_slashes (Optional[bool]): If the API endpoint needs to
                terminate with a `"/"` or not.
            subprotocols (Optional[List[str]]): Subprotocols to be used with
                websocket handshake.
            version (Optional[Union[int, str, float]]): Versioning information.
            name (Optional[str]): A unique name assigned to the URL.
            version_prefix (str): URL path before the version value.
                Defaults to `"/v"`.
            error_format (Optional[str]): Format for error handling.
            **ctx_kwargs (Any): Keyword arguments beginning with `ctx_*`
                prefix will be appended to the route context (`route.ctx`).

        Returns:
            Callable: Object passed as the handler.
        """
        return self.websocket(
            uri=uri,
            host=host,
            strict_slashes=strict_slashes,
            subprotocols=subprotocols,
            version=version,
            name=name,
            version_prefix=version_prefix,
            error_format=error_format,
            **ctx_kwargs,
        )(handler)

    def _determine_error_format(self, handler) -> str:
        with suppress(OSError, TypeError):
            src = dedent(getsource(handler))
            tree = parse(src)
            http_response_types = self._get_response_types(tree)

            if len(http_response_types) == 1:
                return next(iter(http_response_types))

        return ""

    def _get_response_types(self, node):
        types = set()

        class HttpResponseVisitor(NodeVisitor):
            def visit_Return(self, node: Return) -> Any:
                nonlocal types

                with suppress(AttributeError):
                    checks = [node.value.func.id]  # type: ignore
                    if node.value.keywords:  # type: ignore
                        checks += [
                            k.value
                            for k in node.value.keywords  # type: ignore
                            if k.arg == "content_type"
                        ]

                    for check in checks:
                        if check in RESPONSE_MAPPING:
                            types.add(RESPONSE_MAPPING[check])

        HttpResponseVisitor().visit(node)

        return types

    def _build_route_context(self, raw: dict[str, Any]) -> HashableDict:
        ctx_kwargs = {
            key.replace("ctx_", ""): raw.pop(key)
            for key in {**raw}.keys()
            if key.startswith("ctx_")
        }
        if raw:
            unexpected_arguments = ", ".join(raw.keys())
            raise TypeError(
                f"Unexpected keyword arguments: {unexpected_arguments}"
            )
        return HashableDict(ctx_kwargs)
