"""
Caprail Middleware base class.

Middleware wraps request handling in a pipeline, identical in concept
to Laravel's middleware. Implement ``handle()`` and call ``next(request)``
to pass control down the pipeline.
"""
from __future__ import annotations

from abc import ABC, abstractmethod
from collections.abc import Awaitable, Callable
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from caprail.http.request import Request
    from caprail.http.response import Response

NextCallable = Callable[["Request"], Awaitable["Response"]]


class Middleware(ABC):
    """Base class for all Caprail middleware."""

    @abstractmethod
    async def handle(self, request: "Request", next: NextCallable) -> "Response":
        """
        Handle the incoming request.

        Call ``await next(request)`` to pass the request to the next
        middleware or the route handler.
        """


class Pipeline:
    """
    Executes a stack of middleware around a final handler.

    Usage::

        response = await Pipeline(middleware_list).send(request).then(handler)
    """

    def __init__(self, middleware: list[Middleware]) -> None:
        self._middleware = middleware
        self._request: "Request | None" = None

    def send(self, request: "Request") -> "Pipeline":
        self._request = request
        return self

    async def then(self, handler: Callable[["Request"], Awaitable["Response"]]) -> "Response":
        assert self._request is not None
        stack = self._build_stack(handler)
        return await stack(self._request)

    def _build_stack(
        self, handler: Callable[["Request"], Awaitable["Response"]]
    ) -> Callable[["Request"], Awaitable["Response"]]:
        pipeline = handler
        for mw in reversed(self._middleware):
            pipeline = self._wrap(mw, pipeline)
        return pipeline

    @staticmethod
    def _wrap(
        mw: Middleware, next_handler: Callable[["Request"], Awaitable["Response"]]
    ) -> Callable[["Request"], Awaitable["Response"]]:
        async def call(request: "Request") -> "Response":
            return await mw.handle(request, next_handler)
        return call
