from __future__ import annotations

import inspect
import sys
import uuid
from abc import ABC, abstractmethod
from contextlib import asynccontextmanager
from pathlib import Path
from typing import (
    TYPE_CHECKING,
    Any,
    Final,
    Generic,
    ParamSpec,
    TypeVar,
    cast,
    overload,
)

from typing_extensions import Unpack, override

from jobify._internal.common.constants import PATCH_FUNC_NAME
from jobify._internal.common.datastructures import State
from jobify._internal.exceptions import RouteAlreadyRegisteredError

if TYPE_CHECKING:
    from collections.abc import (
        AsyncIterator,
        Callable,
        Coroutine,
        Iterator,
        Sequence,
    )

    from jobify._internal.common.types import (
        ExceptionHandlers,
        Lifespan,
        MappingExceptionHandlers,
    )
    from jobify._internal.configuration import RouteOptions
    from jobify._internal.middleware.base import (
        BaseMiddleware,
        BaseOuterMiddleware,
    )
    from jobify._internal.scheduler.job import Job
    from jobify._internal.scheduler.scheduler import ScheduleBuilder


T_co = TypeVar("T_co", covariant=True)
Return_co = TypeVar("Return_co", covariant=True)
ParamsT = ParamSpec("ParamsT")
Route_co = TypeVar("Route_co", bound="Route[..., Any]", covariant=True)
Router_co = TypeVar("Router_co", bound="Router", covariant=True)


def resolve_name(func: Callable[ParamsT, Return_co], /) -> str:
    name = func.__name__
    name = getattr(func, "__qualname__", name).removesuffix(PATCH_FUNC_NAME)
    fmodule = func.__module__
    if name == "<lambda>":
        name = f"lambda_{uuid.uuid4().hex}"
    if fmodule == "__main__":
        mod = inspect.getmodule(func)
        fallback = sys.argv[0] if sys.argv else "."
        fmodule = Path(getattr(mod, "__file__", fallback)).stem
    return f"{fmodule}:{name}"


class Route(ABC, Generic[ParamsT, Return_co]):
    def __init__(
        self,
        name: str,
        func: Callable[ParamsT, Return_co],
        options: RouteOptions,
    ) -> None:
        self.name: str = name
        self.func: Callable[ParamsT, Return_co] = func
        self.options: RouteOptions = options

    def __call__(
        self,
        *args: ParamsT.args,
        **kwargs: ParamsT.kwargs,
    ) -> Return_co:
        return self.func(*args, **kwargs)

    @overload
    def schedule(
        self: Route[ParamsT, Coroutine[object, object, T_co]],
        *args: ParamsT.args,
        **kwargs: ParamsT.kwargs,
    ) -> ScheduleBuilder[T_co]: ...

    @overload
    def schedule(
        self: Route[ParamsT, Return_co],
        *args: ParamsT.args,
        **kwargs: ParamsT.kwargs,
    ) -> ScheduleBuilder[Return_co]: ...

    @abstractmethod
    def schedule(
        self,
        *args: ParamsT.args,
        **kwargs: ParamsT.kwargs,
    ) -> ScheduleBuilder[Any]:
        raise NotImplementedError

    @overload
    async def push(
        self: Route[ParamsT, Coroutine[object, object, T_co]],
        *args: ParamsT.args,
        **kwargs: ParamsT.kwargs,
    ) -> Job[T_co]: ...

    @overload
    async def push(
        self: Route[ParamsT, Return_co],
        *args: ParamsT.args,
        **kwargs: ParamsT.kwargs,
    ) -> Job[Return_co]: ...

    @abstractmethod
    async def push(
        self,
        *args: ParamsT.args,
        **kwargs: ParamsT.kwargs,
    ) -> Job[Any]:
        raise NotImplementedError


class Registrator(ABC, Generic[Route_co]):
    def __init__(  # noqa: PLR0913
        self,
        *,
        state: State | None,
        lifespan: Lifespan[Router_co] | None,
        route_class: type[Route_co],
        middleware: Sequence[BaseMiddleware] | None,
        outer_middleware: Sequence[BaseOuterMiddleware] | None,
        exception_handlers: MappingExceptionHandlers | None,
    ) -> None:
        self._routes: dict[str, Route_co] = {}
        self._middleware: list[BaseMiddleware] = list(middleware or [])
        self._outer_middleware: list[BaseOuterMiddleware] = list(
            outer_middleware or []
        )
        self._state_lifespan: Final = self._iter_lifespan(
            lifespan or dummy_lifespan
        )
        self._exception_handlers: ExceptionHandlers = dict(
            exception_handlers or {}
        )
        self.state: State = state or State()
        self.route_class: type[Route_co] = route_class

    @overload
    def __call__(
        self,
        func: Callable[ParamsT, Return_co],
    ) -> Route[ParamsT, Return_co]: ...

    @overload
    def __call__(
        self,
        **options: Unpack[RouteOptions],
    ) -> Callable[
        [Callable[ParamsT, Return_co]], Route[ParamsT, Return_co]
    ]: ...

    @overload
    def __call__(
        self,
        func: Callable[ParamsT, Return_co],
        **options: Unpack[RouteOptions],
    ) -> Route[ParamsT, Return_co]: ...

    def __call__(
        self,
        func: Callable[ParamsT, Return_co] | None = None,
        **options: Unpack[RouteOptions],
    ) -> (
        Route[ParamsT, Return_co]
        | Callable[[Callable[ParamsT, Return_co]], Route[ParamsT, Return_co]]
    ):
        wrapper = self._register(options)
        if callable(func):
            return wrapper(func)
        return wrapper  # pragma: no cover

    def _register(
        self,
        options: RouteOptions,
    ) -> Callable[[Callable[ParamsT, Return_co]], Route[ParamsT, Return_co]]:
        def wrapper(
            func: Callable[ParamsT, Return_co],
        ) -> Route[ParamsT, Return_co]:
            if isinstance(func, Route):
                func = cast("Callable[ParamsT, Return_co]", func.func)

            name = options.get("name") or resolve_name(func)
            if name in self._routes:
                raise RouteAlreadyRegisteredError(name)

            return self.register(name, func, options)

        return wrapper

    @abstractmethod
    def register(
        self,
        name: str,
        func: Callable[ParamsT, Return_co],
        options: RouteOptions,
    ) -> Route[ParamsT, Return_co]:
        raise NotImplementedError

    async def _iter_lifespan(
        self,
        user_lifespan: Lifespan[Any],
    ) -> AsyncIterator[None]:
        async with user_lifespan(self) as maybe_state:
            if maybe_state is not None:
                self.state.update(maybe_state)
            yield None

    async def emit_startup(self) -> None:
        await anext(self._state_lifespan)

    async def emit_shutdown(self) -> None:
        await anext(self._state_lifespan, None)


@asynccontextmanager
async def dummy_lifespan(_: Router) -> AsyncIterator[None]:
    yield None


class Router(ABC):
    def __init__(self, *, prefix: str | None) -> None:
        self.prefix: str = prefix if prefix else ""
        self._parent: Router | None = None
        self._sub_routers: list[Router] = []

    @property
    @abstractmethod
    def task(self) -> Registrator[Route[..., Any]]:
        raise NotImplementedError

    @override
    def __repr__(self) -> str:
        return f"<{type(self).__name__}>"

    @property
    def chain_tail(self) -> Iterator[Router]:
        yield self
        for router in self.sub_routers:
            yield from router.chain_tail

    @property
    def routes(self) -> Iterator[Route[..., Any]]:
        yield from self.task._routes.values()

    @property
    def sub_routers(self) -> Sequence[Router]:
        return self._sub_routers

    @property
    def parent(self) -> Router | None:
        return self._parent

    @parent.setter
    def parent(self, router: Router) -> None:
        """Set the parent router for this router (internal use only).

        Do not use this method in own code.
        All routers should be included via the `include_router` method.
        Self- and circular-referencing are not allowed here.
        """
        if self._parent:
            msg = f"Router is already attached to {self._parent!r}"
            raise RuntimeError(msg)
        if self is router:
            msg = "Self-referencing routers is not allowed"
            raise RuntimeError(msg)

        parent: Router | None = router

        while parent is not None:
            if parent is self:
                msg = "Circular referencing of Router is not allowed"
                raise RuntimeError(msg)
            parent = parent.parent

        self._parent = router

    def include_router(self, router: Router) -> None:
        router.parent = self
        self._sub_routers.append(router)

    def include_routers(self, *routers: Router) -> None:
        if not routers:
            msg = "At least one router must be provided"
            raise ValueError(msg)
        for router in routers:
            self.include_router(router)

    def add_middleware(self, middleware: BaseMiddleware) -> None:
        self.task._middleware.append(middleware)

    def add_outer_middleware(self, middleware: BaseOuterMiddleware) -> None:
        self.task._outer_middleware.append(middleware)

    def remove_route(self, name: str) -> None:
        del self.task._routes[name]

    def add_route(self, route: Route[..., Any]) -> None:
        self.task._routes[route.name] = route
