from __future__ import annotations

import inspect
import traceback
from collections.abc import Awaitable, Callable, Mapping, Sequence
from contextlib import nullcontext
from typing import TYPE_CHECKING, Annotated, Any

from lilya import status
from lilya._internal._events import AsyncLifespan, handle_lifespan_events
from lilya._internal._middleware import wrap_middleware
from lilya._internal._path import get_route_path
from lilya._internal._permissions import wrap_permission
from lilya.compat import is_async_callable
from lilya.concurrency import run_in_threadpool
from lilya.conf import _monkay
from lilya.conf.global_settings import Settings
from lilya.contrib.documentation import Doc
from lilya.datastructures import URL, SendReceiveSniffer, URLPath
from lilya.dependencies import wrap_dependency
from lilya.enums import EventType, Match, ScopeType
from lilya.exceptions import ContinueRouting, HTTPException, ImproperlyConfigured
from lilya.lifecycle import get_hooks as _lifecycle_get_hooks
from lilya.middleware.base import DefineMiddleware
from lilya.permissions.base import DefinePermission
from lilya.requests import Request
from lilya.responses import PlainText, RedirectResponse, Response
from lilya.types import (
    ASGIApp,
    Dependencies,
    ExceptionHandler,
    Lifespan,
    Receive,
    Scope,
    Send,
)
from lilya.websockets import WebSocket, WebSocketClose

from .base import BasePath
from .mixins import RoutingMethodsMixin
from .types import (
    NoMatchFound,
    PassPartialMatches,
    PathHandler,
)

# TYPE_CHECKING guard for type annotations (avoids circular imports at runtime)
if TYPE_CHECKING:
    pass


class BaseRouter:
    """
    A Lilya base router object.
    """

    __slots__ = (
        "routes",
        "redirect_slashes",
        "default",
        "on_startup",
        "on_shutdown",
        "middleware",
        "permissions",
        "include_in_schema",
        "deprecated",
        "lifespan_context",
        "middleware_stack",
        "permission_started",
        "settings_module",
        "is_sub_router",
        "before_request",
        "after_request",
        "dependencies",
        "dependency_overrides",
        "wrapped_permissions",
        "_fast_path_len",
        "_fast_path_route",
    )

    def __init__(
        self,
        routes: Sequence[BasePath] | None = None,
        redirect_slashes: bool = True,
        default: ASGIApp | None = None,
        on_startup: Sequence[Callable[[], Any]] | None = None,
        on_shutdown: Sequence[Callable[[], Any]] | None = None,
        lifespan: Lifespan[Any] | None = None,
        *,
        middleware: Sequence[DefineMiddleware] | None = None,
        permissions: Sequence[DefinePermission] | None = None,
        dependencies: Dependencies | None = None,
        before_request: Sequence[Callable[..., Any]] | None = None,
        after_request: Sequence[Callable[..., Any]] | None = None,
        settings_module: Annotated[
            Settings | None,
            Doc(
                """
                Alternative settings parameter. This parameter is an alternative to
                `LILYA_SETTINGS_MODULE` way of loading your settings into a Lilya application.

                When the `settings_module` is provided, it will make sure it takes priority over
                any other settings provided for the instance.
                """
            ),
        ] = None,
        include_in_schema: bool = True,
        deprecated: bool = False,
        is_sub_router: bool = False,
    ) -> None:
        assert lifespan is None or (on_startup is None and on_shutdown is None), (
            "Use either 'lifespan' or 'on_startup'/'on_shutdown', not both."
        )

        if inspect.isasyncgenfunction(lifespan) or inspect.isgeneratorfunction(lifespan):
            raise ImproperlyConfigured(
                "async function generators are not allowed. Use @contextlib.asynccontextmanager instead."
            )

        self.on_startup = [] if on_startup is None else list(on_startup)
        self.on_shutdown = [] if on_shutdown is None else list(on_shutdown)

        # Inject global lifecycle hooks (from lilya.lifecycle)
        hooks = _lifecycle_get_hooks()

        # Global startup hooks run before app-defined ones
        self.on_startup = [*hooks["startup"], *self.on_startup]

        # Global shutdown hooks run after app-defined ones
        self.on_shutdown = [*self.on_shutdown, *hooks["shutdown"]]

        # Wrap dependencies
        _dependencies = dependencies if dependencies is not None else {}
        self.dependencies = {key: wrap_dependency(dep) for key, dep in _dependencies.items()}

        self.dependency_overrides: dict[str, Any] = {}

        self.lifespan_context = handle_lifespan_events(
            on_startup=on_startup, on_shutdown=on_shutdown, lifespan=lifespan
        )

        if self.lifespan_context is None:
            self.lifespan_context = AsyncLifespan(self)

        self.routes = [] if routes is None else list(routes)
        self.redirect_slashes = redirect_slashes
        self.default = self.handle_not_found if default is None else default
        self.include_in_schema = include_in_schema
        self.deprecated = deprecated

        if middleware is not None:
            self.middleware = [wrap_middleware(mid) for mid in middleware]
        else:
            self.middleware = middleware or []

        self.permissions = permissions if permissions is not None else []
        self.settings_module = settings_module
        self.middleware_stack = self.app
        self.permission_started = False
        self.is_sub_router = is_sub_router

        self.before_request = before_request if before_request is not None else []
        self.after_request = after_request if after_request is not None else []

        self.wrapped_permissions = [
            wrap_permission(permission) for permission in permissions or []
        ]

        self._apply_permissions(self.wrapped_permissions)
        self._apply_middleware(self.middleware)

        self._fast_path_len = -1
        self._fast_path_route: BasePath | None = None

    def _apply_middleware(self, middleware: Sequence[DefineMiddleware] | None) -> None:
        """
        Apply middleware to the app.

        Args:
            middleware (Union[Sequence[DefineMiddleware], None]): The middleware.

        Returns:
            None
        """
        if middleware is not None:
            for cls, args, options in reversed(middleware):
                self.middleware_stack = cls(app=self.middleware_stack, *args, **options)

    def _apply_permissions(self, permissions: Sequence[DefinePermission] | None) -> None:
        """
        Apply permissions to the app.

        Args:
            permissions (Union[Sequence[DefinePermission], None]): The permissions.

        Returns:
            None
        """
        if permissions is not None:
            for cls, args, options in reversed(self.permissions):
                self.middleware_stack = cls(app=self.middleware_stack, *args, **options)

    def path_for(self, name: str, /, **path_params: Any) -> URLPath:
        return self.url_path_for(name, **path_params)

    def url_path_for(self, name: str, /, **path_params: Any) -> URLPath:
        for route in self.routes:
            try:
                return route.url_path_for(name, **path_params)
            except NoMatchFound:
                ...
        raise NoMatchFound(name, path_params)

    async def startup(self) -> None:
        """
        Runs the the events on startup.
        """
        for handler in self.on_startup:
            if is_async_callable(handler):
                await handler()
            else:
                handler()

    async def shutdown(self) -> None:
        """
        Runs the the events on startup.
        """
        for handler in self.on_shutdown:
            if is_async_callable(handler):
                await handler()
            else:
                handler()

    def _compute_fast_path(self, routes: Sequence[BasePath]) -> BasePath | None:
        from .path import Path  # Late import to avoid circular dependency

        if self.is_sub_router or len(routes) != 1:
            return None

        route = routes[0]
        if not isinstance(route, Path):
            return None

        return route

    async def handle_route(self, route: BasePath, path_handler: PathHandler) -> None:
        """
        Handle a route match.

        Args:
            route: The matched route.
            path_handler (PathHandler): The PathHandler.
        """
        base_scope = path_handler.scope
        child = path_handler.child_scope or {}

        if "path_params" in child:
            base_scope.setdefault("path_params", {}).update(child["path_params"])
        # expose the matched route + template
        base_scope["route"] = route
        base_scope["route_path_template"] = getattr(route, "path", None)

        if self.dependency_overrides:
            base_scope["dependency_overrides"] = self.dependency_overrides

        new_scope = dict(base_scope)
        new_scope.update(child)
        await route.handle_dispatch(new_scope, path_handler.receive, path_handler.send)

    async def handle_partial(self, route: BasePath, path_handler: PathHandler) -> None:
        """
        Handle a partial route match.

        Args:
            route: The matched route.
            path_handler (PathHandler): The PathHandler.
        """
        new_scope = dict(path_handler.scope)
        new_scope.update(path_handler.child_scope)
        await route.handle_dispatch(new_scope, path_handler.receive, path_handler.send)

    async def handle_default(self, scope: Scope, receive: Receive, send: Send) -> None:
        """
        Handle the default behavior.

        Args:
            scope (Scope): The ASGI scope.
            receive (Receive): The ASGI receive channel.
            send (Send): The ASGI send channel.
        """
        await self.default(scope, receive, send)

    async def lifespan(self, scope: Scope, receive: Receive, send: Send) -> None:
        """
        Handle ASGI lifespan messages, managing application startup and shutdown events.

        Args:
            scope (Scope): The ASGI scope.
            receive (Receive): The receive channel.
            send (Send): The send channel.
        """
        completed_startup = False

        async def startup_complete() -> None:
            await send({"type": "lifespan.startup.complete"})

        async def shutdown_complete() -> None:
            await send({"type": "lifespan.shutdown.complete"})

        async def lifespan_error(type: str, message: str) -> None:
            await send({"type": type, "message": message})

        try:
            app: Any = scope.get("app")
            await receive()

            # If it's a bound method (needs invoking to get the actual callable, example? From settings)
            if inspect.ismethod(self.lifespan_context):
                lifespan = self.lifespan_context()
                if lifespan is not None:
                    self.lifespan_context = lifespan

            assert self.lifespan_context is not None
            async with self.lifespan_context(app) as state:
                if state is not None:
                    if "state" not in scope:
                        raise RuntimeError(
                            'The server does not support "state" in the lifespan scope.'
                        )
                    scope["state"].update(state)

                await startup_complete()
                completed_startup = True
                await receive()

        except BaseException:
            exc_text = traceback.format_exc()
            if completed_startup:
                await lifespan_error("lifespan.shutdown.failed", exc_text)
            else:
                await lifespan_error("lifespan.startup.failed", exc_text)
            raise

        else:
            await shutdown_complete()

    async def app(self, scope: Scope, receive: Receive, send: Send) -> None:
        """
        Handle ASGI messages, managing different scopes and routing.

        Args:
            scope (Scope): The ASGI scope.
            receive (Receive): The ASGI receive channel.
            send (Send): The ASGI send channel.
        """
        assert scope["type"] in (
            ScopeType.HTTP,
            ScopeType.WEBSOCKET,
            ScopeType.LIFESPAN,
        )

        if "router" not in scope:
            scope["router"] = self

        if scope["type"] == ScopeType.HTTP:
            routes = self.routes
            if self._fast_path_len != len(routes) or (
                len(routes) == 1 and routes[0] is not self._fast_path_route
            ):
                self._fast_path_route = self._compute_fast_path(routes)
                self._fast_path_len = len(routes)

            fast_route = self._fast_path_route
            if fast_route is not None:
                match, child_scope = fast_route.search(scope)
                had_match = match != Match.NONE

                if had_match:
                    try:
                        if match == Match.FULL:
                            base_scope = scope
                            if "path_params" in child_scope:
                                base_scope.setdefault("path_params", {}).update(
                                    child_scope["path_params"]
                                )
                            base_scope["route"] = fast_route
                            base_scope["route_path_template"] = getattr(fast_route, "path", None)

                            if self.dependency_overrides:
                                base_scope["dependency_overrides"] = self.dependency_overrides

                            new_scope = dict(base_scope)
                            new_scope.update(child_scope)
                        else:
                            new_scope = dict(scope)
                            new_scope.update(child_scope)

                        await fast_route.handle_dispatch(new_scope, receive, send)
                        return
                    except ContinueRouting:
                        pass

                if not had_match and self.redirect_slashes:
                    route_path = get_route_path(scope)
                    if route_path != "/":
                        redirect_scope = dict(scope)
                        if route_path.endswith("/"):
                            redirect_scope["path"] = redirect_scope["path"].rstrip("/")
                        else:
                            redirect_scope["path"] = redirect_scope["path"] + "/"

                        match, _child = fast_route.search(redirect_scope)
                        if match != Match.NONE:
                            redirect_url = URL.build_from_scope(scope=redirect_scope)
                            response = RedirectResponse(url=str(redirect_url))
                            await response(scope, receive, send)
                            return

                await self.handle_default(scope, receive, send)
                return

        sniffer = SendReceiveSniffer(receive, send)

        partial_matches: list[tuple[BaseRouter, BasePath, PathHandler]] = []
        had_match = False

        for route in self.routes:
            # we cannot continue when the sniffer detects a sent
            if sniffer.sent:
                return
            match, child_scope = route.search(scope)
            if match == Match.NONE:
                continue
            had_match = True
            path_handler = PathHandler(child_scope=child_scope, scope=scope, sniffer=sniffer)
            try:
                if match == Match.FULL:
                    await self.handle_route(route, path_handler=path_handler)
                    if sniffer.sent:
                        return  # type: ignore
                    else:
                        sniffer.repeat_message = True
                elif match == Match.PARTIAL:
                    partial_matches.append((self, route, path_handler))
            except PassPartialMatches as exc:
                # collect the partial matches from the sub-router
                partial_matches.extend(exc.partial_matches)
            except ContinueRouting:
                assert not sniffer.sent, "Cannot continue routing when an answer was already sent."
                sniffer.repeat_message = True
        # check if redirect would be possible, when no match was found
        if not had_match and self.redirect_slashes:
            route_path = get_route_path(scope)
            if scope["type"] == ScopeType.HTTP and route_path != "/":
                redirect_scope = dict(scope)
                if route_path.endswith("/"):
                    redirect_scope["path"] = redirect_scope["path"].rstrip("/")
                else:
                    redirect_scope["path"] = redirect_scope["path"] + "/"

                for route in self.routes:
                    match, child_scope = route.search(redirect_scope)
                    if match != Match.NONE:
                        redirect_url = URL.build_from_scope(scope=redirect_scope)
                        response = RedirectResponse(url=str(redirect_url))
                        await response(scope, receive, send)
                        return

        if self.is_sub_router:
            # now pass the partial matches to the upper router
            raise PassPartialMatches(partial_matches=partial_matches)
        # when in the main router, check partial matches (method does not match)
        # use for this the handler method of the sub-router to keep their modifications
        for router, route, path_handler in partial_matches:
            try:
                await router.handle_partial(route, path_handler)
                # should propagate back to this sniffer
                if sniffer.sent:
                    return
                else:
                    # rearm sniffer of the path handler (sub router)
                    path_handler.sniffer.repeat_message = True
                    sniffer.repeat_message = True
            except ContinueRouting:
                assert not sniffer.sent, "Cannot continue routing when an answer was already sent."
                path_handler.sniffer.repeat_message = True
                sniffer.repeat_message = True

        await self.handle_default(scope, receive, send)

    async def handle_not_found(self, scope: Scope, receive: Receive, send: Send) -> None:
        """
        Handles the case when no match is found.

        Args:
            scope (Scope): The request scope.
            receive (Receive): The receive channel.
            send (Send): The send channel.

        Returns:
            None
        """
        if "app" in scope:
            raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)

        if scope["type"] == ScopeType.HTTP:
            response = PlainText("Not Found", status_code=status.HTTP_404_NOT_FOUND)
            await response(scope, receive, send)

        elif scope["type"] == ScopeType.WEBSOCKET:
            websocket_close = WebSocketClose()
            await websocket_close(scope, receive, send)

    def include(
        self,
        path: str,
        app: ASGIApp,
        name: str | None = None,
        middleware: Sequence[DefineMiddleware] | None = None,
        permissions: Sequence[DefinePermission] | None = None,
        exception_handlers: Mapping[Any, ExceptionHandler] | None = None,
        dependencies: Dependencies | None = None,
        namespace: str | None = None,
        pattern: str | None = None,
        include_in_schema: bool = True,
        before_request: Sequence[Callable[..., Any]] | None = None,
        after_request: Sequence[Callable[..., Any]] | None = None,
        redirect_slashes: bool = True,
    ) -> None:
        """
        Adds an Include application into the routes.
        """
        from lilya.routing import Include  # Late import to avoid circular dependency

        route = Include(
            path,
            app=app,
            name=name,
            middleware=middleware,
            permissions=permissions,
            exception_handlers=exception_handlers,
            dependencies=dependencies,
            namespace=namespace,
            pattern=pattern,
            include_in_schema=include_in_schema,
            before_request=before_request,
            after_request=after_request,
            redirect_slashes=redirect_slashes,
        )
        self.routes.append(route)

    def host(self, host: str, app: ASGIApp, name: str | None = None) -> None:
        """
        Adds a Host application into the routes.
        """
        from .host import Host  # Late import to avoid circular dependency

        route = Host(host, app=app, name=name)
        self.routes.append(route)

    def add_route(
        self,
        path: str,
        handler: Callable[[Request], Awaitable[Response] | Response] | Any,
        methods: list[str] | None = None,
        name: str | None = None,
        middleware: Sequence[DefineMiddleware] | None = None,
        permissions: Sequence[DefinePermission] | None = None,
        exception_handlers: Mapping[Any, ExceptionHandler] | None = None,
        dependencies: Dependencies | None = None,
        include_in_schema: bool = True,
        before_request: Sequence[Callable[..., Any]] | None = None,
        after_request: Sequence[Callable[..., Any]] | None = None,
    ) -> None:
        """
        Manually creates a `Path`` from a given handler.
        """
        from .path import Path  # Late import to avoid circular dependency

        route = Path(
            path,
            handler=handler,
            methods=methods,
            middleware=middleware,
            permissions=permissions,
            exception_handlers=exception_handlers,
            dependencies=dependencies,
            name=name,
            include_in_schema=include_in_schema,
            before_request=before_request,
            after_request=after_request,
        )
        self.routes.append(route)

    def add_websocket_route(
        self,
        path: str,
        handler: Callable[[WebSocket], Awaitable[None]],
        name: str | None = None,
        middleware: Sequence[DefineMiddleware] | None = None,
        permissions: Sequence[DefinePermission] | None = None,
        exception_handlers: Mapping[Any, ExceptionHandler] | None = None,
        dependencies: Dependencies | None = None,
        before_request: Sequence[Callable[..., Any]] | None = None,
        after_request: Sequence[Callable[..., Any]] | None = None,
    ) -> None:
        """
        Manually creates a `WebSocketPath` from a given handler.
        """
        from .websocket import WebSocketPath  # Late import to avoid circular dependency

        route = WebSocketPath(
            path,
            handler=handler,
            middleware=middleware,
            permissions=permissions,
            name=name,
            exception_handlers=exception_handlers,
            dependencies=dependencies,
            before_request=before_request,
            after_request=after_request,
        )
        self.routes.append(route)

    def add_event_handler(
        self, event_type: str, func: Callable[[], Any]
    ) -> None:  # pragma: no cover
        assert event_type in (
            EventType.ON_STARTUP,
            EventType.ON_SHUTDOWN,
            EventType.STARTUP,
            EventType.SHUTDOWN,
        )

        if event_type in (EventType.ON_STARTUP, EventType.STARTUP):
            self.on_startup.append(func)
        else:
            self.on_shutdown.append(func)

    def on_event(self, event_type: str) -> Callable:
        def wrapper(func: Callable) -> Callable:
            self.add_event_handler(event_type, func)
            return func

        return wrapper

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        ctxmanager = (
            nullcontext()
            if self.settings_module is None
            else _monkay.with_settings(self.settings_module)
        )
        with ctxmanager:
            if scope["type"] == ScopeType.LIFESPAN:
                await self.lifespan(scope, receive, send)
                return

            for before_request in self.before_request:
                if inspect.isclass(before_request):
                    before_request = before_request()

                if is_async_callable(before_request):
                    await before_request(scope, receive, send)
                else:
                    await run_in_threadpool(before_request, scope, receive, send)
            await self.middleware_stack(scope, receive, send)

            for after_request in self.after_request:
                if inspect.isclass(after_request):
                    after_request = after_request()

                if is_async_callable(after_request):
                    await after_request(scope, receive, send)
                else:
                    await run_in_threadpool(after_request, scope, receive, send)


class Router(RoutingMethodsMixin, BaseRouter):
    """
    Contains all the needed base routing system and adds the ability to use decorators.
    """

    def route(
        self,
        path: str,
        methods: list[str],
        name: str | None = None,
        middleware: Sequence[DefineMiddleware] | None = None,
        permissions: Sequence[DefinePermission] | None = None,
        exception_handlers: Mapping[Any, ExceptionHandler] | None = None,
        dependencies: Dependencies | None = None,
        include_in_schema: bool = True,
        before_request: Sequence[Callable[..., Any]] | None = None,
        after_request: Sequence[Callable[..., Any]] | None = None,
    ) -> Callable[..., Any]:
        """
        Decorator for defining a generic route.

        Args:
            path (str): The URL path pattern for the route.
            methods (list[str] | None, optional): The HTTP methods allowed for the route. Defaults to None.
            name (str | None, optional): The name of the route. Defaults to None.
            middleware (Sequence[DefineMiddleware] | None, optional): The middleware functions to apply to the route. Defaults to None.
            permissions (Sequence[DefinePermission] | None, optional): The permissions required for the route. Defaults to None.
            exception_handlers (Mapping[Any, ExceptionHandler] | None, optional): The exception handlers for the route. Defaults to None.
            dependencies (Dependencies | None, optional): Dependencies to inject into the route handler. Defaults to None.
            include_in_schema (bool, optional): Whether to include the route in the API schema. Defaults to True.
            before_request (Sequence[Callable[..., Any]] | None, optional): Functions to run before the request is processed. Defaults to None.
            after_request (Sequence[Callable[..., Any]] | None, optional): Functions to run after the request is processed. Defaults to None.

        Returns:
            Callable[..., Any]: The decorated function.
        """

        def wrapper(func: Callable) -> Callable:
            self.add_route(
                path=path,
                handler=func,
                methods=methods,
                name=name,
                middleware=middleware,
                permissions=permissions,
                exception_handlers=exception_handlers,
                dependencies=dependencies,
                include_in_schema=include_in_schema,
                before_request=before_request,
                after_request=after_request,
            )
            func._lilya_dependencies = dependencies or {}  # type: ignore[attr-defined]
            return func

        return wrapper

    def websocket(
        self,
        path: str,
        name: str | None = None,
        middleware: Sequence[DefineMiddleware] | None = None,
        permissions: Sequence[DefinePermission] | None = None,
        exception_handlers: Mapping[Any, ExceptionHandler] | None = None,
        dependencies: Dependencies | None = None,
        before_request: Sequence[Callable[..., Any]] | None = None,
        after_request: Sequence[Callable[..., Any]] | None = None,
    ) -> Callable[..., Any]:
        """
        Decorator for defining a WebSocket route.

        Args:
            path (str): The URL path for the WebSocket route.
            name (str, optional): The name of the route. Defaults to None.
            middleware (Sequence[DefineMiddleware], optional): The middleware to apply to the route. Defaults to None.
            permissions (Sequence[DefinePermission], optional): The permissions required for the route. Defaults to None.
            exception_handlers (Mapping[Any, ExceptionHandler], optional): The exception handlers for the route. Defaults to None.
            dependencies (Dependencies | None, optional): Dependencies to inject into the route handler. Defaults to None.
            before_request (Sequence[Callable[..., Any]] | None, optional): Functions to run before the request is processed. Defaults to None.
            after_request (Sequence[Callable[..., Any]] | None, optional): Functions to run after the request is processed. Defaults to None.

        Returns:
            Callable[..., Any]: The decorated function.

        """

        def wrapper(func: Callable) -> Callable:
            self.add_websocket_route(
                path=path,
                handler=func,
                name=name,
                middleware=middleware,
                permissions=permissions,
                exception_handlers=exception_handlers,
                dependencies=dependencies,
                before_request=before_request,
                after_request=after_request,
            )
            func._lilya_dependencies = dependencies or {}  # type: ignore[attr-defined]
            return func

        return wrapper
