"""Bounded HTTP request handling for the public licensing authority."""

from __future__ import annotations

import json
from typing import Awaitable, Callable, Mapping, MutableMapping

from .errors import ServerErrorCode


AsgiReceive = Callable[[], Awaitable[MutableMapping[str, object]]]
AsgiSend = Callable[[MutableMapping[str, object]], Awaitable[None]]
BOUNDED_REQUEST_BODY_STATE_KEY = "licensing_bounded_request_body"


class RequestBodyLimitMiddleware:
    def __init__(self, app: Callable, maximum_bytes: int) -> None:
        if not callable(app):
            raise TypeError("app must be callable")
        if isinstance(maximum_bytes, bool) or not isinstance(maximum_bytes, int):
            raise TypeError("maximum_bytes must be an integer")
        if maximum_bytes < 1:
            raise ValueError("maximum_bytes must be at least one")
        self._app = app
        self._maximum_bytes = maximum_bytes

    async def __call__(
        self,
        scope: Mapping[str, object],
        receive: AsgiReceive,
        send: AsgiSend,
    ) -> None:
        if not isinstance(scope, Mapping):
            raise TypeError("scope must be a mapping")
        if not callable(receive):
            raise TypeError("receive must be callable")
        if not callable(send):
            raise TypeError("send must be callable")
        if scope.get("type") != "http":
            await self._app(scope, receive, send)
            return

        content_length, has_content_length = self._content_length(scope)
        if content_length is None:
            await self._send_error(send, 400, "Content-Length header is invalid")
            return
        if content_length > self._maximum_bytes:
            await self._send_error(send, 413, "request body is too large")
            return

        chunks: list[bytes] = []
        total = 0
        while True:
            message = await receive()
            message_type = message.get("type")
            if message_type == "http.disconnect":
                await self._app(scope, self._replay_disconnect, send)
                return
            if message_type != "http.request":
                raise RuntimeError("unexpected ASGI message while reading request body")
            chunk = message.get("body", b"")
            if not isinstance(chunk, bytes):
                raise TypeError("ASGI request body must be bytes")
            total += len(chunk)
            if total > self._maximum_bytes:
                await self._send_error(send, 413, "request body is too large")
                return
            chunks.append(chunk)
            if not message.get("more_body", False):
                break
        if has_content_length and total != content_length:
            await self._send_error(send, 400, "Content-Length does not match request body")
            return

        body = b"".join(chunks)
        replayed = False

        async def replay() -> MutableMapping[str, object]:
            nonlocal replayed
            if replayed:
                return {"type": "http.disconnect"}
            replayed = True
            return {"type": "http.request", "body": body, "more_body": False}

        child_scope = dict(scope)
        raw_state = scope.get("state", {})
        if not isinstance(raw_state, Mapping):
            raise TypeError("ASGI state must be a mapping")
        state = dict(raw_state)
        state[BOUNDED_REQUEST_BODY_STATE_KEY] = body
        child_scope["state"] = state
        await self._app(child_scope, replay, send)

    def _content_length(self, scope: Mapping[str, object]) -> tuple[int | None, bool]:
        headers = scope.get("headers", ())
        if not isinstance(headers, (list, tuple)):
            raise TypeError("ASGI headers must be a list or tuple")
        values: list[int] = []
        for item in headers:
            if not isinstance(item, (list, tuple)) or len(item) != 2:
                raise TypeError("ASGI header entries must be name/value pairs")
            name, value = item
            if not isinstance(name, bytes) or not isinstance(value, bytes):
                raise TypeError("ASGI header names and values must be bytes")
            if name.lower() != b"content-length":
                continue
            try:
                parsed = int(value.decode("ascii"), 10)
            except (UnicodeError, ValueError):
                return None, True
            if parsed < 0:
                return None, True
            values.append(parsed)
        if values and any(value != values[0] for value in values[1:]):
            return None, True
        return (values[0], True) if values else (0, False)

    @staticmethod
    async def _replay_disconnect() -> MutableMapping[str, object]:
        return {"type": "http.disconnect"}

    @staticmethod
    async def _send_error(send: AsgiSend, status_code: int, message: str) -> None:
        body = json.dumps(
            {
                "code": ServerErrorCode.INVALID_REQUEST.value,
                "message": message,
                "retryable": False,
                "existingResourceId": None,
            },
            ensure_ascii=True,
            separators=(",", ":"),
        ).encode("utf-8")
        await send(
            {
                "type": "http.response.start",
                "status": status_code,
                "headers": (
                    (b"content-type", b"application/json"),
                    (b"content-length", str(len(body)).encode("ascii")),
                ),
            }
        )
        await send({"type": "http.response.body", "body": body})


__all__ = ["BOUNDED_REQUEST_BODY_STATE_KEY", "RequestBodyLimitMiddleware"]
