from __future__ import annotations

from functools import lru_cache
from inspect import isawaitable
from typing import TYPE_CHECKING, cast

from typing_extensions import get_args

from starlite.datastructures import Cookie, ResponseHeader
from starlite.dto import DTO
from starlite.enums import HttpMethod
from starlite.exceptions import ValidationException
from starlite.plugins.base import get_plugin_for_value
from starlite.status_codes import HTTP_200_OK, HTTP_201_CREATED, HTTP_204_NO_CONTENT
from starlite.utils import (
    annotation_is_iterable_of_type,
    is_async_callable,
    is_class_and_subclass,
)

if TYPE_CHECKING:
    from typing import Any, Sequence

    from starlite.app import Starlite
    from starlite.background_tasks import BackgroundTask, BackgroundTasks
    from starlite.connection import Request
    from starlite.plugins import SerializationPluginProtocol
    from starlite.response import Response
    from starlite.response_containers import ResponseContainer
    from starlite.types import (
        AfterRequestHookHandler,
        ASGIApp,
        AsyncAnyCallable,
        Method,
        ResponseType,
        TypeEncodersMap,
    )


def create_data_handler(
    after_request: AfterRequestHookHandler | None,
    background: BackgroundTask | BackgroundTasks | None,
    cookies: frozenset[Cookie],
    headers: frozenset[ResponseHeader],
    media_type: str,
    response_class: ResponseType,
    return_annotation: Any,
    status_code: int,
    type_encoders: TypeEncodersMap | None,
) -> AsyncAnyCallable:
    """Create a handler function for arbitrary data."""
    normalized_headers = [
        (name.lower().encode("latin-1"), value.encode("latin-1")) for name, value in normalize_headers(headers).items()
    ]
    cookie_headers = [cookie.to_encoded_header() for cookie in cookies if not cookie.documentation_only]
    raw_headers = [*normalized_headers, *cookie_headers]
    is_dto_annotation = is_class_and_subclass(return_annotation, DTO)
    is_dto_iterable_annotation = annotation_is_iterable_of_type(return_annotation, DTO)

    async def create_response(data: Any) -> "ASGIApp":
        response = response_class(
            background=background,
            content=data,
            media_type=media_type,
            status_code=status_code,
            type_encoders=type_encoders,
        )
        response.raw_headers = raw_headers

        if after_request:
            return await after_request(response)  # type: ignore

        return response

    async def handler(data: Any, plugins: list["SerializationPluginProtocol"], **kwargs: Any) -> "ASGIApp":
        if isawaitable(data):
            data = await data

        if is_dto_annotation and not isinstance(data, DTO):
            data = return_annotation(**data) if isinstance(data, dict) else return_annotation.from_model_instance(data)

        elif is_dto_iterable_annotation and data and not isinstance(data[0], DTO):  # pyright: ignore
            dto_type = cast("type[DTO]", get_args(return_annotation)[0])
            data = [
                dto_type(**datum) if isinstance(datum, dict) else dto_type.from_model_instance(datum) for datum in data
            ]

        elif plugins and not (is_dto_annotation or is_dto_iterable_annotation):
            data = await normalize_response_data(data=data, plugins=plugins)

        return await create_response(data=data)

    return handler


@lru_cache(1024)
def filter_cookies(local_cookies: frozenset[Cookie], layered_cookies: frozenset[Cookie]) -> list[Cookie]:
    """Given two sets of cookies, return a unique list of cookies, that are not marked as documentation_only."""
    return [cookie for cookie in {*local_cookies, *layered_cookies} if not cookie.documentation_only]


def create_generic_asgi_response_handler(
    after_request: AfterRequestHookHandler | None,
    cookies: frozenset[Cookie],
) -> AsyncAnyCallable:
    """Create a handler function for Responses."""

    async def handler(data: "ASGIApp", **kwargs: Any) -> "ASGIApp":
        if hasattr(data, "set_cookie"):
            for cookie in cookies:
                data.set_cookie(**cookie.dict)
        return await after_request(data) if after_request else data  # type: ignore

    return handler


@lru_cache(1024)
def normalize_headers(headers: frozenset[ResponseHeader]) -> dict[str, str]:
    """Given a dictionary of ResponseHeader, filter them and return a dictionary of values.

    Args:
        headers: A dictionary of :class:`ResponseHeader <starlite.datastructures.ResponseHeader>` values

    Returns:
        A string keyed dictionary of normalized values
    """
    return {
        header.name: cast("str", header.value)  # we know value to be a string at this point because we validate it
        # that it's not None when initializing a header with documentation_only=True
        for header in headers
        if not header.documentation_only
    }


async def normalize_response_data(data: Any, plugins: list["SerializationPluginProtocol"]) -> Any:
    """Normalize the response's data by awaiting any async values and resolving plugins.

    Args:
        data: An arbitrary value
        plugins: A list of :class:`plugins <starlite.plugins.base.SerializationPluginProtocol>`
    Returns:
        Value for the response body
    """

    plugin = get_plugin_for_value(value=data, plugins=plugins)
    if not plugin:
        return data

    if is_async_callable(plugin.to_dict):
        if isinstance(data, (list, tuple)):
            return [await plugin.to_dict(datum) for datum in data]
        return await plugin.to_dict(data)

    if isinstance(data, (list, tuple)):
        return [plugin.to_dict(datum) for datum in data]
    return plugin.to_dict(data)


def create_response_container_handler(
    after_request: AfterRequestHookHandler | None,
    cookies: frozenset[Cookie],
    headers: frozenset[ResponseHeader],
    media_type: str,
    status_code: int,
) -> AsyncAnyCallable:
    """Create a handler function for ResponseContainers."""
    normalized_headers = normalize_headers(headers)

    async def handler(data: ResponseContainer, app: "Starlite", request: "Request", **kwargs: Any) -> "ASGIApp":
        response = data.to_response(
            app=app,
            headers={**normalized_headers, **data.headers},
            status_code=status_code,
            media_type=data.media_type or media_type,
            request=request,
        )
        response.cookies = filter_cookies(frozenset(data.cookies), cookies)
        return await after_request(response) if after_request else response  # type: ignore

    return handler


def create_response_handler(
    after_request: AfterRequestHookHandler | None,
    cookies: frozenset[Cookie],
) -> AsyncAnyCallable:
    """Create a handler function for Starlite Responses."""

    async def handler(data: Response, **kwargs: Any) -> "ASGIApp":
        data.cookies = filter_cookies(frozenset(data.cookies), cookies)
        return await after_request(data) if after_request else data  # type: ignore

    return handler


def normalize_http_method(http_methods: HttpMethod | Method | Sequence[HttpMethod | Method]) -> set[Method]:
    """Normalize HTTP method(s) into a set of upper-case method names.

    Args:
        http_methods: A value for http method.

    Returns:
        A normalized set of http methods.
    """
    output: set[str] = set()

    if isinstance(http_methods, str):
        http_methods = [http_methods]  # pyright: ignore

    for method in http_methods:
        if isinstance(method, HttpMethod):
            method_name = method.value.upper()
        else:
            method_name = method.upper()
        if method_name not in HTTP_METHOD_NAMES:
            raise ValidationException(f"Invalid HTTP method: {method_name}")
        output.add(method_name)

    return cast("set[Method]", output)


def get_default_status_code(http_methods: set[Method]) -> int:
    """Return the default status code for a given set of HTTP methods.

    Args:
        http_methods: A set of method strings

    Returns:
        A status code
    """
    if HttpMethod.POST in http_methods:
        return HTTP_201_CREATED
    if HttpMethod.DELETE in http_methods:
        return HTTP_204_NO_CONTENT
    return HTTP_200_OK


HTTP_METHOD_NAMES = {m.value for m in HttpMethod}
