from __future__ import annotations

import logging
from collections.abc import AsyncGenerator, Awaitable, Callable  # noqa: TC003
from contextlib import suppress
from inspect import signature
from typing import TYPE_CHECKING, Annotated
from uuid import UUID

from belgie_proto.core.account import AccountProtocol
from belgie_proto.core.connection import DBConnection
from belgie_proto.core.individual import IndividualProtocol
from belgie_proto.core.oauth_state import OAuthStateProtocol
from belgie_proto.core.session import SessionProtocol
from fastapi import APIRouter, Depends, Request, status
from fastapi.responses import RedirectResponse
from fastapi.security import SecurityScopes  # noqa: TC002

from belgie_core.core.client import BelgieClient
from belgie_core.core.plugin import AfterAuthenticateHook, AfterSignUpHook, AuthenticatedProfile, Plugin, PluginClient
from belgie_core.session.manager import SessionManager

if TYPE_CHECKING:
    from belgie_proto.core import AdapterProtocol

    from belgie_core.core.settings import BelgieSettings


logger = logging.getLogger(__name__)


class _BelgieCallable:
    """Descriptor that makes Belgie instances callable with instance-specific dependencies.

    This allows Depends(belgie) to work seamlessly because each Belgie instance gets a
    callable with its own database dependency baked into the signature.
    """

    def __get__(self, obj: Belgie | None, objtype: type | None = None) -> object:
        if obj is None:
            return self

        dependency = obj.database

        def __call__(  # noqa: N807
            db: Annotated[DBConnection, Depends(dependency)],
        ) -> BelgieClient:
            return BelgieClient(
                db=db,
                adapter=obj.adapter,
                session_manager=obj.session_manager,
                cookie_settings=obj.settings.cookie,
                after_sign_up=obj.after_sign_up,
            )

        __call__.__annotations__["db"] = Annotated[DBConnection, Depends(dependency)]
        return __call__


class Belgie[
    IndividualT: IndividualProtocol,
    AccountT: AccountProtocol,
    SessionT: SessionProtocol,
    OAuthStateT: OAuthStateProtocol,
]:
    """Main authentication orchestrator for Belgie."""

    __call__: Callable[..., BelgieClient[IndividualT, AccountT, SessionT, OAuthStateT]] = _BelgieCallable()

    def __init__(
        self,
        settings: BelgieSettings,
        adapter: AdapterProtocol[IndividualT, AccountT, SessionT, OAuthStateT],
        *,
        database: Callable[[], DBConnection | AsyncGenerator[DBConnection, None]],
    ) -> None:
        self.settings = settings
        self.adapter = adapter
        self.database = database
        self.session_manager = SessionManager(
            adapter=adapter,
            max_age=settings.session.max_age,
            update_age=settings.session.update_age,
        )
        self.plugins: list[PluginClient] = []

    def add_plugin[P: PluginClient](self, plugin: Plugin[P]) -> P:
        try:
            signature(plugin).bind(self.settings)
        except TypeError as exc:
            msg = "plugin callable must follow __call__(belgie_settings)"
            raise TypeError(msg) from exc

        instance = plugin(self.settings)
        if not isinstance(instance, PluginClient):
            msg = "plugin callable must return an object implementing router(belgie) and public(belgie)"
            raise TypeError(msg)

        self.plugins.append(instance)
        return instance

    @property
    def router(self) -> APIRouter:
        main_router = APIRouter(prefix="/auth", tags=["auth"])
        dependency = self.database

        for plugin in self.plugins:
            if (plugin_router := plugin.router(self)) is not None:
                main_router.include_router(plugin_router)

        async def _get_db(db: Annotated[DBConnection, Depends(dependency)]) -> DBConnection:
            return db

        _get_db.__annotations__["db"] = Annotated[DBConnection, Depends(dependency)]

        @main_router.post("/signout")
        async def signout(
            request: Request,
            db: Annotated[DBConnection, Depends(_get_db)],
        ) -> RedirectResponse:
            if session_id_str := request.cookies.get(self.settings.cookie.name):
                with suppress(ValueError):
                    await self.sign_out(db, UUID(session_id_str))

            response = RedirectResponse(
                url=self.settings.urls.signout_redirect,
                status_code=status.HTTP_302_FOUND,
            )
            response.delete_cookie(
                key=self.settings.cookie.name,
                domain=self.settings.cookie.domain,
            )
            return response

        signout.__annotations__["db"] = Annotated[DBConnection, Depends(_get_db)]

        root_router = APIRouter()
        root_router.include_router(main_router)

        for plugin in self.plugins:
            if (public_router := plugin.public(self)) is not None:
                root_router.include_router(public_router)

        return root_router

    async def get_individual_from_session(
        self,
        db: DBConnection,
        session_id: UUID,
    ) -> IndividualT | None:
        client = self.__call__(db)
        return await client.get_individual_from_session(session_id)

    async def sign_out(
        self,
        db: DBConnection,
        session_id: UUID,
    ) -> bool:
        client = self.__call__(db)
        return await client.sign_out(session_id)

    async def after_authenticate(
        self,
        *,
        client: BelgieClient[IndividualT, AccountT, SessionT, OAuthStateT],
        request: Request,
        individual: IndividualProtocol[str],
        profile: AuthenticatedProfile,
    ) -> None:
        for plugin in self.plugins:
            if not isinstance(plugin, AfterAuthenticateHook):
                continue
            try:
                await plugin.after_authenticate(
                    belgie=self,
                    client=client,
                    request=request,
                    individual=individual,
                    profile=profile,
                )
            except Exception:
                logger.exception(
                    "after_authenticate hook failed",
                    extra={
                        "provider": profile.provider,
                        "plugin": plugin.__class__.__name__,
                    },
                )

    async def after_sign_up(
        self,
        *,
        client: BelgieClient[IndividualT, AccountT, SessionT, OAuthStateT],
        request: Request | None,
        individual: IndividualProtocol[str],
    ) -> None:
        for plugin in self.plugins:
            if not isinstance(plugin, AfterSignUpHook):
                continue
            try:
                await plugin.after_sign_up(
                    belgie=self,
                    client=client,
                    request=request,
                    individual=individual,
                )
            except Exception:
                logger.exception(
                    "after_sign_up hook failed",
                    extra={"plugin": plugin.__class__.__name__},
                )

    async def _get_session_from_cookie(
        self,
        request: Request,
        db: DBConnection,
    ) -> SessionT | None:
        client = self.__call__(db)
        return await client._get_session_from_cookie(request)  # noqa: SLF001

    @property
    def individual(self) -> Callable[[SecurityScopes, Request, DBConnection], Awaitable[IndividualT]]:
        dependency = self.database

        async def _individual(
            security_scopes: SecurityScopes,
            request: Request,
            db: Annotated[DBConnection, Depends(dependency)],
        ) -> IndividualT:
            client = self.__call__(db)
            return await client.get_individual(security_scopes, request)

        _individual.__annotations__["db"] = Annotated[DBConnection, Depends(dependency)]
        return _individual

    @property
    def session(self) -> Callable[[Request, DBConnection], Awaitable[SessionT]]:
        dependency = self.database

        async def _session(
            request: Request,
            db: Annotated[DBConnection, Depends(dependency)],
        ) -> SessionT:
            client = self.__call__(db)
            return await client.get_session(request)

        _session.__annotations__["db"] = Annotated[DBConnection, Depends(dependency)]
        return _session
