from typing import Iterable
from typing import Optional
from typing import Type
from typing import Union

from drakaina import ENV_APP
from drakaina.config import rpc_config
from drakaina.exceptions import BadRequestError
from drakaina.middlewares.base import BaseMiddleware
from drakaina.rpc_protocols import BaseRPCProtocol
from drakaina.rpc_protocols import JsonRPCv2
from drakaina.typing_ import WSGIApplication
from drakaina.typing_ import WSGIEnvironment
from drakaina.typing_ import WSGIInputStream
from drakaina.typing_ import WSGIResponse
from drakaina.typing_ import WSGIStartResponse

ALLOWED_METHODS = ("OPTIONS", "GET", "POST")


class WSGIHandler:
    """Implementation of WSGI protocol.

    :param route:
    :type route: str
    :param handler:
    :type handler: BaseRPCProtocol
    :param middlewares:
    :type middlewares: Iterable[Type[BaseMiddleware]]
    :param provide_smd:
    :type provide_smd: bool
    :param provide_openrpc:
    :type provide_openrpc: bool
    :param provide_openapi:
    :type provide_openapi: bool

    """

    __slots__ = (
        "environ",
        "start_response",
        "handler",
        "route",
        "provide_smd",
        "provide_openrpc",
        "provide_openapi",
        "_rpc_content_type",
        "_allowed_methods",
        "_middlewares_chain",
    )

    environ: WSGIEnvironment
    start_response: WSGIStartResponse
    _middlewares_chain: Union[WSGIApplication, BaseMiddleware]

    def __init__(
        self,
        route: Optional[str] = None,
        handler: Optional[BaseRPCProtocol] = None,
        middlewares: Optional[Iterable[Type[BaseMiddleware]]] = None,
        provide_smd: Optional[Union[bool, str]] = False,
        provide_openrpc: Optional[Union[bool, str]] = False,
        provide_openapi: Optional[Union[bool, str]] = False,
    ):
        self.handler = handler or JsonRPCv2()
        self._rpc_content_type = self.handler.content_type

        self.route = route
        if isinstance(self.route, str) and not self.route.startswith("/"):
            self.route = "/" + self.route

        self.provide_smd = provide_smd
        self.provide_openrpc = provide_openrpc
        self.provide_openapi = provide_openapi

        if provide_smd or provide_openrpc or provide_openapi:
            self._allowed_methods = ", ".join(ALLOWED_METHODS)
        else:
            self._allowed_methods = ", ".join(
                [m for m in ALLOWED_METHODS if m != "GET"],
            )

        # Build middleware stack
        # todo: add ExceptionMiddleware
        self._middlewares_chain = self._wsgi_app
        kw = {"is_async": False}
        for mw in reversed(middlewares or []):
            if issubclass(mw, BaseMiddleware):
                self._middlewares_chain = mw(self._middlewares_chain, **kw)
            self._middlewares_chain = mw(self._middlewares_chain)

    def __call__(
        self,
        environ: WSGIEnvironment,
        start_response: WSGIStartResponse,
    ) -> WSGIResponse:
        environ[ENV_APP] = self
        return self._middlewares_chain(environ, start_response)  # noqa

    def _wsgi_app(
        self,
        environ: WSGIEnvironment,
        start_response: WSGIStartResponse,
    ) -> WSGIResponse:
        self.environ = environ
        self.start_response = start_response

        method = environ["REQUEST_METHOD"]
        if method in ALLOWED_METHODS:
            if self._validate_path(environ["PATH_INFO"]):
                return getattr(self, method.lower())()

            return self._not_found()

        return self._method_not_allowed()

    def get(self) -> WSGIResponse:
        # todo: what if provide_scheme args is string with url?
        # todo: handle with parameters and routing
        if self.provide_smd:
            response_body = self.handler.smd_scheme()
            response_headers = [
                ("Content-Type", "application/json"),
                ("Content-Length", str(len(response_body))),
            ]
        elif self.provide_openrpc:
            response_body = self.handler.openrpc_scheme()
            response_headers = [
                ("Content-Type", "application/json"),
                ("Content-Length", str(len(response_body))),
            ]
        elif self.provide_openapi:
            response_body = self.handler.openapi_scheme()
            response_headers = [
                ("Content-Type", "application/json"),
                ("Content-Length", str(len(response_body))),
            ]
        else:
            return self._method_not_allowed()

        self.start_response("200 OK", response_headers)
        return (response_body,)

    def post(self) -> WSGIResponse:
        wsgi_input: WSGIInputStream = self.environ["wsgi.input"]

        content_type = self.environ.get("CONTENT_TYPE")
        content_length = int(self.environ.get("CONTENT_LENGTH"))
        if (
            not (content_type and content_length)
            or content_type != self._rpc_content_type
            or content_length > rpc_config.MAX_CONTENT_SIZE
        ):
            if content_type != self._rpc_content_type:
                response_status = "415 Unsupported Media Type"
            else:
                response_status = "400 Bad Request"
            # Return RPC error
            # todo: raise?
            response_body = self.handler.get_raw_error(BadRequestError())
        else:
            response_status = "200 OK"
            response_body = self.handler.handle_raw_request(
                wsgi_input.read(content_length),
                request=self.environ,
            )

        response_headers = [
            ("Content-Type", self._rpc_content_type),
            ("Content-Length", str(len(response_body))),
        ]
        self.start_response(response_status, response_headers)

        yield response_body

    def options(self) -> WSGIResponse:
        response_headers = [
            ("Allow", self._allowed_methods),
            ("Content-Length", "0"),
        ]
        self.start_response("200 OK", response_headers)
        yield b""

    def _not_found(self) -> WSGIResponse:
        response_headers = []
        self.start_response("404 Not Found", response_headers)
        yield b""

    def _method_not_allowed(self) -> WSGIResponse:
        response_headers = [("Allow", self._allowed_methods)]
        self.start_response("405 Method Not Allowed", response_headers)
        yield b""

    def _validate_path(self, path_info: str) -> bool:
        # todo: validate get urls (provide_*)
        # todo: implement re.compile
        return (
            self.route is not None
            and isinstance(path_info, str)
            and path_info.startswith(self.route)
        )
