from __future__ import annotations

import inspect
from collections.abc import Callable, Coroutine, Generator
from functools import cached_property
from typing import Any, ClassVar, TypeVar, cast

from lilya import status
from lilya._internal._responses import BaseHandler
from lilya.conf import settings
from lilya.enums import Event, HTTPMethod, ScopeType, SignatureDefault
from lilya.exceptions import HTTPException, ImproperlyConfigured
from lilya.requests import Request
from lilya.responses import PlainText, Response
from lilya.serializers import serializer
from lilya.types import Message, Receive, Scope, Send
from lilya.websockets import WebSocket

C = TypeVar("C", bound="Controller")
CW = TypeVar("CW", bound="WebSocketController")


class BaseController(BaseHandler):
    __exclude_from_openapi__: bool = False
    permissions: list[Callable[..., Coroutine[Any, Any, bool]]] = []
    middleware: list[Callable[..., Coroutine[Any, Any, None]]] = []
    exception_handlers: dict[int, Callable[[Request, Exception], Response]] = {}
    dependencies: dict[str, Any] = {}
    before_request: list[Callable[..., Coroutine[Any, Any, None]]] = []
    after_request: list[Callable[..., Coroutine[Any, Any, None]]] = []

    __is_controller__: bool = True

    def handle_signature(self) -> None:
        """
        Validates the return annotation of a handler
        if `enforce_return_annotation` is set to True.
        """
        if not settings.enforce_return_annotation:
            return None

        if self.signature.return_annotation is inspect._empty:
            raise ImproperlyConfigured(
                "A return value of a route handler function should be type annotated. "
                "If your function doesn't return a value or returns None, annotate it as returning 'NoReturn' or 'None' respectively."
            )


class Controller(BaseController):
    """
    Object oriented controller allowing the
    declaration of the http verbs as views.
    """

    __scope__: Scope | None = None

    signature: inspect.Signature | None = None

    @classmethod
    def with_init(cls: type[C], *init_args: Any, **init_kwargs: Any) -> type[Controller]:
        """
        Class method that creates a dynamic, singleton factory wrapper around the current
        `Controller` class (`cls`).

        This allows the original controller to be instantiated with fixed, **pre-defined arguments**
        (`init_args`, `init_kwargs`) on every incoming ASGI request.

        The primary instance of the factory wrapper (`_Factory`) is managed as a **singleton**
        to ensure minimal overhead when the framework router invokes the handler repeatedly.

        Args:
            cls: The original `Controller` class being wrapped.
            *init_args: Positional arguments to be passed to the wrapped controller's
                        `__init__` method upon request execution.
            **init_kwargs: Keyword arguments to be passed to the wrapped controller's
                           `__init__` method upon request execution.

        Returns:
            The dynamically created, callable factory class (`_Factory`).
        """
        parent: type[C] = cls
        name: str = f"{cls.__name__}WithInit"

        class InitFactory(Controller):
            """
            The dynamic, singleton factory wrapper class.

            This factory intercepts the ASGI call, instantiates the original controller (`parent`)
            with the baked-in arguments, and delegates the ASGI lifecycle.
            """

            __is_controller__: ClassVar[bool] = True
            __factory_base__: ClassVar[type[Controller]] = parent
            __init_args__: ClassVar[tuple[Any, ...]] = init_args
            __init_kwargs__: ClassVar[dict[str, Any]] = init_kwargs
            _singleton: ClassVar[InitFactory | None] = None

            # Router calls `self.app()` → class() every request.
            # Make that return the SAME instance (stateless wrapper).
            def __new__(cls_, *args: Any, **kwargs: Any) -> Any:
                """
                Enforces a singleton pattern for the factory wrapper instance itself (`_Factory`).
                """
                if cls_._singleton is None:
                    cls_._singleton = super().__new__(cls_)
                return cls_._singleton

            def __init__(self, *args: Any, **kwargs: Any) -> None:
                """The factory instance itself requires no custom initialization logic."""
                ...

            async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
                """
                The ASGI entry point. Instantiates the original controller with baked-in arguments
                and processes the request.
                """
                # 1. Instantiate the original controller (C) with fixed arguments
                instance: Controller = self.__factory_base__(
                    *self.__init_args__, **self.__init_kwargs__
                )

                # 2. Delegate the ASGI call to the newly created instance
                await instance(scope, receive, send)

        # Adjust the metadata of the dynamically created class
        InitFactory.__name__ = name
        InitFactory.__qualname__ = name
        InitFactory.__doc__ = getattr(cls, "__doc__", None)
        return InitFactory

    @cached_property
    def __allowed_methods__(self) -> list[str]:
        return [
            method
            for method in HTTPMethod.to_list()
            if getattr(self, method.lower(), None) is not None
        ]

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        assert scope["type"] == ScopeType.HTTP, (
            f"{self.__class__.__name__} classes must be in the http scope."
        )

        await self.handle_dispatch(scope=scope, receive=receive, send=send)

    async def handle_dispatch(self, scope: Scope, receive: Receive, send: Send) -> None:
        request = Request(scope=scope, receive=receive, send=send)
        name = (
            HTTPMethod.GET.lower()
            if request.method == HTTPMethod.HEAD and not hasattr(self, "head")
            else request.method.lower()
        )
        handler: Callable[[], Coroutine[Any, Any, Response]] = getattr(
            self, name, self.handle_not_allowed
        )
        self.signature = inspect.signature(handler)
        self.__scope__ = scope

        func_params: dict[str, Any] = await self._extract_params_from_request(
            request=request, signature=self.signature
        )

        # Assign query params automatically.
        request_information = self.extract_request_params_information(
            request=request, signature=self.signature
        )
        func_params.update(**request_information)

        if self.signature.parameters:
            if SignatureDefault.REQUEST in self.signature.parameters:
                func_params.update({"request": request})
                response = await self._execute_function(handler, **func_params)
            else:
                response = await self._execute_function(handler, **func_params)
        else:
            response = await self._execute_function(handler, **func_params)

        await self._handle_response_content(response, scope, receive, send)

    async def handle_not_allowed(self) -> Response:
        headers = {"Allow": ", ".join(self.__allowed_methods__)}
        if "app" in self.__scope__:
            raise HTTPException(status_code=status.HTTP_405_METHOD_NOT_ALLOWED)
        return PlainText(
            "Method Not Allowed", status_code=status.HTTP_405_METHOD_NOT_ALLOWED, headers=headers
        )


class WebSocketController(BaseController):
    """
    Object oriented controller allowing the
    declaration of the http verbs as views.
    """

    encoding: str | None = None

    def __init__(self, scope: Scope, receive: Receive, send: Send) -> None:
        assert scope["type"] == ScopeType.WEBSOCKET, (
            f"{self.__class__.__name__} classes must be in the websocket scope."
        )
        self.scope = scope
        self.receive = receive
        self.send = send

    def __await__(self) -> Generator[Any, None, None]:
        return self.handle_dispatch(
            scope=self.scope, receive=self.receive, send=self.send
        ).__await__()

    @classmethod
    def with_init(cls: type[CW], *init_args: Any, **init_kwargs: Any) -> type[WebSocketController]:
        """
        Class method that creates a dynamic, singleton factory wrapper around the current
        `WebSocketController` class (`cls`).

        This allows the controller to be instantiated with fixed, pre-defined arguments
        (`init_args`, `init_kwargs`) every time the factory is called by the framework.

        The primary instance of the factory wrapper itself (`_Factory`) is managed as a
        singleton via `__new__`.

        Args:
            cls: The original `WebSocketController` class being wrapped.
            *init_args: Positional arguments to be passed to the wrapped controller's
                        `__init__` method upon activation.
            **init_kwargs: Keyword arguments to be passed to the wrapped controller's
                           `__init__` method upon activation.

        Returns:
            The dynamically created, callable factory class (`_Factory`).
        """
        parent: type[CW] = cls
        name: str = f"{cls.__name__}WithInit"

        class InitFactory(WebSocketController):
            """
            The dynamic, singleton factory wrapper class.

            This class intercepts the ASGI call, instantiates the original controller (`parent`)
            with the baked-in `init_args/kwargs`, and delegates the ASGI lifecycle to the
            newly created instance.
            """

            __is_controller__: ClassVar[bool] = True
            __factory_base__: ClassVar[type[WebSocketController]] = parent
            __init_args__: ClassVar[tuple[Any, ...]] = init_args
            __init_kwargs__: ClassVar[dict[str, Any]] = init_kwargs
            _singleton: ClassVar[InitFactory | None] = None

            def __new__(cls_, *args: Any, **kwargs: Any) -> Any:
                """
                Enforces a singleton pattern for the factory wrapper instance itself.
                """
                if cls_._singleton is None:
                    cls_._singleton = super().__new__(cls_)
                return cls_._singleton

            def __init__(self, *args: Any, **kwargs: Any) -> None:
                """
                The factory instance itself requires no initialization logic.
                """
                if len(args) >= 3:
                    self._w_scope = args[0]
                    self._w_receive = args[1]
                    self._w_send = args[2]

            def __await__(self) -> Generator[Any, None, None]:
                # When awaited directly (common in test client), delegate to real instance
                scope = getattr(self, "_w_scope", None)
                receive = getattr(self, "_w_receive", None)
                send = getattr(self, "_w_send", None)
                if scope is None or receive is None or send is None:
                    raise RuntimeError("WebSocket factory awaited without scope/receive/send")

                instance: WebSocketController = self.__factory_base__(
                    scope, receive, send, *self.__init_args__, **self.__init_kwargs__
                )
                return instance.__await__()

            async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
                """
                The ASGI entry point. Instantiates the original controller and delegates.

                This method passes the ASGI connection components (`scope`, `receive`, `send`)
                along with the baked-in initialization arguments (`__init_args__`, `__init_kwargs__`)
                to the original controller class.
                """
                # 1. Instantiate the original controller (`parent`) with all arguments
                instance: WebSocketController = self.__factory_base__(
                    scope, receive, send, *self.__init_args__, **self.__init_kwargs__
                )
                # 2. Delegate the ASGI lifecycle (Controller.__call__ or __await__)
                await instance

        # Adjust the metadata of the dynamically created class
        InitFactory.__name__ = name
        InitFactory.__qualname__ = name
        InitFactory.__doc__ = getattr(cls, "__doc__", None)

        return InitFactory

    async def handle_dispatch(self, scope: Scope, receive: Receive, send: Send) -> None:
        """
        Handle WebSocket connection, message reception, and disconnection.

        Args:
            scope (Scope): ASGI scope.
            receive (Receive): ASGI receive channel.
            send (Send): ASGI send channel.
        """
        websocket = WebSocket(scope=scope, receive=receive, send=send)
        await self.on_connect(websocket)

        close_code = status.WS_1000_NORMAL_CLOSURE
        try:
            while True:
                message = await websocket.receive()
                if message["type"] == Event.WEBSOCKET_RECEIVE:
                    data = await self.decode(websocket, message)
                    await self.on_receive(websocket, data)
                elif message["type"] == Event.WEBSOCKET_DISCONNECT:
                    close_code = int(message.get("code") or close_code)
                    break
        except Exception as e:
            close_code = status.WS_1011_INTERNAL_ERROR
            raise e
        finally:
            await self.on_disconnect(websocket, close_code)

    async def decode(self, websocket: WebSocket, message: Message) -> Any:
        """
        Decode WebSocket messages based on the specified encoding.

        Args:
            websocket (WebSocket): WebSocket instance.
            message (Message): WebSocket message.

        Returns:
            Any: Decoded message data.
        """
        if self.encoding == "text":
            return await self.decode_text_message(websocket, message)
        elif self.encoding == "bytes":
            return await self.decode_bytes_message(websocket, message)
        elif self.encoding == "json":
            return await self.decode_json_message(message)

        assert self.encoding is None, f"Unsupported 'encoding' attribute {self.encoding}"
        return message["text"] if message.get("text") else message["bytes"]

    async def decode_text_message(self, websocket: WebSocket, message: Message) -> str:
        """
        Decode text WebSocket message.

        Args:
            websocket (WebSocket): WebSocket instance.
            message (Message): WebSocket message.

        Returns:
            str: Decoded text message.
        """
        if "text" not in message:
            await self.close_websocket(
                websocket, status.WS_1003_UNSUPPORTED_DATA, "Expected text messages, but got bytes"
            )
        return cast(str, message["text"])

    async def decode_bytes_message(self, websocket: WebSocket, message: Message) -> bytes:
        """
        Decode bytes WebSocket message.

        Args:
            websocket (WebSocket): WebSocket instance.
            message (Message): WebSocket message.

        Returns:
            bytes: Decoded bytes message.
        """
        if "bytes" not in message:
            await self.close_websocket(
                websocket, status.WS_1003_UNSUPPORTED_DATA, "Expected bytes messages, but got text"
            )
        return cast(bytes, message["bytes"])

    async def decode_json_message(self, message: Message) -> Any:
        """
        Decode JSON WebSocket message.

        Args:
            message (Message): WebSocket message.

        Returns:
            Any: Decoded JSON message data.
        """
        text = (
            message["text"]
            if message.get("text") is not None
            else message["bytes"].decode("utf-8")
        )
        try:
            return serializer.loads(text)
        except Exception:
            raise RuntimeError("Malformed JSON data received.") from None

    async def on_connect(self, websocket: WebSocket) -> None:
        """
        Handle WebSocket connection.

        Args:
            websocket (WebSocket): WebSocket instance.
        """
        await websocket.accept()

    async def on_receive(self, websocket: WebSocket, data: Any) -> None:
        """
        Handle WebSocket message reception.

        Args:
            websocket (WebSocket): WebSocket instance.
            data (Any): Decoded message data.
        """
        ...

    async def on_disconnect(self, websocket: WebSocket, close_code: int) -> None:
        """
        Handle WebSocket disconnection.

        Args:
            websocket (WebSocket): WebSocket instance.
            close_code (int): WebSocket close code.
        """
        ...

    async def close_websocket(self, websocket: WebSocket, code: int, reason: str) -> None:
        """
        Close WebSocket connection with the specified code and reason.

        Args:
            websocket (WebSocket): WebSocket instance.
            code (int): WebSocket close code.
            reason (str): Reason for closing the WebSocket connection.
        """
        await websocket.close(code=code)
        raise RuntimeError(reason)
