"""Helper to track the current http request."""

from collections.abc import Awaitable, Callable
from contextvars import ContextVar
from http import HTTPStatus
import inspect
import logging
from typing import Any, Final

from aiohttp import web
from aiohttp.typedefs import LooseHeaders
from aiohttp.web import AppKey, Request
from aiohttp.web_exceptions import (
    HTTPBadRequest,
    HTTPInternalServerError,
    HTTPUnauthorized,
)
from aiohttp.web_urldispatcher import AbstractResource, AbstractRoute
import voluptuous as vol

from homeassistant import exceptions
from homeassistant.const import CONTENT_TYPE_JSON
from homeassistant.core import Context, HomeAssistant, is_callback
from homeassistant.util.json import JSON_ENCODE_EXCEPTIONS, format_unserializable_data

from .json import find_paths_unserializable_data, json_bytes, json_dumps

_LOGGER = logging.getLogger(__name__)

# Responses smaller than this fit within a single network packet, so
# compressing them wastes event-loop CPU without reducing round-trips.
MIN_COMPRESSED_RESPONSE_SIZE: Final = 1024


type AllowCorsType = Callable[[AbstractRoute | AbstractResource], None]
KEY_AUTHENTICATED: Final = "ha_authenticated"
KEY_ALLOW_ALL_CORS = AppKey[AllowCorsType]("allow_all_cors")
KEY_ALLOW_CONFIGURED_CORS = AppKey[AllowCorsType]("allow_configured_cors")
KEY_HASS: AppKey[HomeAssistant] = AppKey("hass")

current_request: ContextVar[Request | None] = ContextVar(
    "current_request", default=None
)


def request_handler_factory(
    hass: HomeAssistant, view: HomeAssistantView, handler: Callable
) -> Callable[[web.Request], Awaitable[web.StreamResponse]]:
    """Wrap the handler classes."""
    is_coroutinefunction = inspect.iscoroutinefunction(handler)
    assert is_coroutinefunction or is_callback(handler), (
        "Handler should be a coroutine or a callback."
    )

    async def handle(request: web.Request) -> web.StreamResponse:
        """Handle incoming request."""
        if hass.is_stopping:
            return web.Response(status=HTTPStatus.SERVICE_UNAVAILABLE)

        authenticated = request.get(KEY_AUTHENTICATED, False)

        if view.requires_auth and not authenticated:
            # Import here to avoid circular dependency with network.py
            from .network import NoURLAvailableError, get_url  # noqa: PLC0415

            # Get the current request header to include as resource metadata
            # endpoint for RFC9728.
            try:
                url_prefix = get_url(hass, require_current_request=True)
            except NoURLAvailableError:
                # Omit header to avoid leaking configured URLs
                raise HTTPUnauthorized from None
            raise HTTPUnauthorized(
                headers={
                    "WWW-Authenticate": (
                        f'Bearer resource_metadata="{url_prefix}'
                        '/.well-known/oauth-protected-resource"'
                    )
                }
            )

        if _LOGGER.isEnabledFor(logging.DEBUG):
            _LOGGER.debug(
                "Serving %s to %s (auth: %s)",
                request.path,
                request.remote,
                authenticated,
            )

        try:
            if is_coroutinefunction:
                result = await handler(request, **request.match_info)
            else:
                result = handler(request, **request.match_info)
        except vol.Invalid as err:
            raise HTTPBadRequest from err
        except exceptions.ServiceNotFound as err:
            raise HTTPInternalServerError from err
        except exceptions.Unauthorized as err:
            raise HTTPUnauthorized from err

        if isinstance(result, web.StreamResponse):
            # The method handler returned a ready-made Response, how nice of it
            return result

        status_code = HTTPStatus.OK
        if isinstance(result, tuple):
            result, status_code = result

        if isinstance(result, bytes):
            return web.Response(body=result, status=status_code)

        if isinstance(result, str):
            return web.Response(text=result, status=status_code)

        if result is None:
            return web.Response(body=b"", status=status_code)

        raise TypeError(
            f"Result should be None, string, bytes or StreamResponse. Got: {result}"
        )

    return handle


class HomeAssistantView:
    """Base view for all views."""

    url: str | None = None
    extra_urls: list[str] = []
    # Views inheriting from this class can override this
    requires_auth = True
    cors_allowed = False

    @staticmethod
    def context(request: web.Request) -> Context:
        """Generate a context from a request."""
        if (user := request.get("hass_user")) is None:
            return Context()

        return Context(user_id=user.id)

    @staticmethod
    def json(
        result: Any,
        status_code: HTTPStatus | int = HTTPStatus.OK,
        headers: LooseHeaders | None = None,
    ) -> web.Response:
        """Return a JSON response."""
        try:
            msg = json_bytes(result)
        except JSON_ENCODE_EXCEPTIONS as err:
            _LOGGER.error(
                "Unable to serialize to JSON. Bad data found at %s",
                format_unserializable_data(
                    find_paths_unserializable_data(result, dump=json_dumps)
                ),
            )
            raise HTTPInternalServerError from err
        response = web.Response(
            body=msg,
            content_type=CONTENT_TYPE_JSON,
            status=int(status_code),
            headers=headers,
            zlib_executor_size=32768,
        )
        if len(msg) > MIN_COMPRESSED_RESPONSE_SIZE:
            response.enable_compression()
        return response

    def json_message(
        self,
        message: str,
        status_code: HTTPStatus | int = HTTPStatus.OK,
        message_code: str | None = None,
        headers: LooseHeaders | None = None,
    ) -> web.Response:
        """Return a JSON message response."""
        data = {"message": message}
        if message_code is not None:
            data["code"] = message_code
        return self.json(data, status_code, headers=headers)

    def register(
        self, hass: HomeAssistant, app: web.Application, router: web.UrlDispatcher
    ) -> None:
        """Register the view with a router."""
        assert self.url is not None, "No url set for view"
        urls = [self.url, *self.extra_urls]
        routes: list[AbstractRoute] = []

        for method in ("get", "post", "delete", "put", "patch", "head", "options"):
            if not (handler := getattr(self, method, None)):
                continue

            handler = request_handler_factory(hass, self, handler)

            routes.extend(router.add_route(method, url, handler) for url in urls)

        # Use `get` because CORS middleware is not be loaded in emulated_hue
        if self.cors_allowed:
            allow_cors = app.get(KEY_ALLOW_ALL_CORS)
        else:
            allow_cors = app.get(KEY_ALLOW_CONFIGURED_CORS)

        if allow_cors:
            for route in routes:
                allow_cors(route)
