from __future__ import annotations

from collections.abc import AsyncGenerator, Callable  # noqa: TC003
from typing import Protocol, cast
from uuid import UUID

from belgie_proto import (
    AccountProtocol,
    AdapterProtocol,
    DBConnection,
    OAuthStateProtocol,
    SessionProtocol,
    UserProtocol,
)
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.hooks import HookRunner, Hooks
from belgie_core.core.protocols import Plugin
from belgie_core.core.settings import BelgieSettings  # noqa: TC001
from belgie_core.providers.protocols import OAuthProviderProtocol, Providers  # noqa: TC001
from belgie_core.session.manager import SessionManager


class DBDependencyProvider(Protocol):
    """Protocol for database dependency providers.

    This allows Belgie to work with any dependency injection system
    without coupling to a specific implementation.
    """

    @property
    def dependency(self) -> Callable[[], DBConnection | AsyncGenerator[DBConnection, None]]:
        """FastAPI dependency that provides database connections."""
        ...


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

    This allows Depends(belgie) to work seamlessly - each Belgie instance gets its own
    callable that has the Belgie instance's database dependency baked into the signature.
    """

    def __get__(self, obj: Belgie | None, objtype: type | None = None) -> object:
        """Return instance-specific callable when accessed through an instance."""
        if obj is None:
            # Accessed through class, return descriptor itself
            return self

        # Return a callable with this instance's db.dependency
        if obj.db is None:
            msg = "Belgie.db must be configured with a dependency"
            raise RuntimeError(msg)
        dependency = obj.db.dependency

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

        return __call__


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

    The Belgie class provides a complete OAuth 2.0 authentication solution with session management,
    user creation, and FastAPI integration. It automatically loads OAuth providers from environment
    variables and creates router endpoints for authentication.

    Type Parameters:
        UserT: User model type implementing UserProtocol
        AccountT: Account model type implementing AccountProtocol
        SessionT: Session model type implementing SessionProtocol
        OAuthStateT: OAuth state model type implementing OAuthStateProtocol

    Attributes:
        settings: Authentication configuration settings
        adapter: Database adapter for persistence operations
        session_manager: Session manager instance for session operations
        providers: Dictionary of registered OAuth providers keyed by provider_id
        router: FastAPI router with authentication endpoints (cached property)

    Example:
        >>> from belgie_core import Belgie, BelgieSettings
        >>> from belgie_alchemy import AlchemyAdapter
        >>> from belgie_core.providers.google import GoogleOAuthProvider, GoogleProviderSettings
        >>> from myapp.models import User, Account, Session, OAuthState
        >>>
        >>> settings = BelgieSettings(
        ...     secret="your-secret-key",
        ...     base_url="http://localhost:8000",
        ... )
        >>>
        >>> adapter = AlchemyAdapter(
        ...     user=User,
        ...     account=Account,
        ...     session=Session,
        ...     oauth_state=OAuthState,
        ... )
        >>> db = DatabaseSettings(dialect={"type": "sqlite", "database": ":memory:"})
        >>>
        >>> # Explicitly pass provider settings
        >>> providers: Providers = {
        ...     "google": GoogleProviderSettings(
        ...         client_id="your-client-id",
        ...         client_secret="your-client-secret",
        ...         redirect_uri="http://localhost:8000/auth/provider/google/callback",
        ...     ),
        ... }
        >>> belgie = Belgie(settings=settings, adapter=adapter, providers=providers, db=db)
        >>> app.include_router(belgie.router)
    """

    # Use descriptor to make each instance callable with its own dependency
    __call__: Callable[..., BelgieClient] = cast("Callable[..., BelgieClient]", _BelgieCallable())

    def __init__(
        self,
        settings: BelgieSettings,
        adapter: AdapterProtocol[UserT, AccountT, SessionT, OAuthStateT],
        db: DBDependencyProvider,
        providers: Providers | None = None,
        hooks: Hooks | None = None,
    ) -> None:
        """Initialize the Belgie instance.

        Args:
            settings: Authentication configuration including session, cookie, and URL settings
            adapter: Database adapter for user, account, session, and OAuth state persistence
            db: Database dependency provider
            providers: Dictionary of provider settings. Each setting is callable and returns its provider.
                      If None, no providers are registered.

        Raises:
        """
        self.settings = settings
        self.adapter = adapter
        self.db = db

        self.session_manager = SessionManager(
            adapter=adapter,
            max_age=settings.session.max_age,
            update_age=settings.session.update_age,
        )

        self.hook_runner = HookRunner(hooks or Hooks())

        self.plugins: list[Plugin] = []

        # Instantiate providers by calling the settings
        self.providers: dict[str, OAuthProviderProtocol] = (
            {provider_id: provider_settings() for provider_id, provider_settings in providers.items()}  # ty: ignore[call-non-callable]
            if providers
            else {}
        )

    def add_plugin[P: Plugin, **PParams](
        self,
        plugin: Callable[PParams, P],
        *args: PParams.args,
        **kwargs: PParams.kwargs,
    ) -> P:
        """Register and instantiate a plugin.

        Args:
            plugin: The class of the plugin to register.
            *args: Plugin initialization arguments.
            **kwargs: Plugin initialization keyword arguments.

        Returns:
            The instantiated plugin.
        """
        instance = plugin(*args, **kwargs)

        self.plugins.append(instance)  # ty: ignore[arg-type]

        return instance

    def router(self) -> APIRouter:
        """FastAPI router with all provider routes.

        Creates a router with the following structure:
        - /auth/provider/{provider_id}/signin - Provider signin endpoints
        - /auth/provider/{provider_id}/callback - Provider callback endpoints
        - /auth/signout - Global signout endpoint

        Returns:
            APIRouter with all authentication endpoints
        """
        main_router = APIRouter(prefix="/auth", tags=["auth"])
        provider_router = APIRouter(prefix="/provider")

        if self.db is None:
            msg = "Belgie.db must be configured with a dependency"
            raise RuntimeError(msg)
        dependency = self.db.dependency

        # Include all registered provider routers
        for provider in self.providers.values():
            # Provider's router has prefix /{provider_id}
            # Combined with provider_router prefix: /auth/provider/{provider_id}/...
            provider_specific_router = provider.get_router(
                self.adapter,
                self.settings.cookie,
                session_max_age=self.settings.session.max_age,
                signin_redirect=self.settings.urls.signin_redirect,
                signout_redirect=self.settings.urls.signout_redirect,
                hook_runner=self.hook_runner,
                db_dependency=dependency,
            )
            provider_router.include_router(provider_specific_router)

        # Include all registered plugin routers
        for plugin in self.plugins:
            main_router.include_router(plugin.router(self))

        # Add signout endpoint to main router (not provider-specific)
        async def _get_db(db: DBConnection = Depends(dependency)) -> DBConnection:  # noqa: B008
            return db

        @main_router.post("/signout")
        async def signout(
            request: Request,
            db: DBConnection = Depends(_get_db),  # noqa: B008, FAST002
        ) -> RedirectResponse:
            session_id_str = request.cookies.get(self.settings.cookie.name)

            if session_id_str:
                try:
                    session_id = UUID(session_id_str)
                    await self.sign_out(db, session_id)
                except ValueError:
                    pass

            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

        # Include provider router in main router
        main_router.include_router(provider_router)
        root_router = APIRouter()
        root_router.include_router(main_router)

        for plugin in self.plugins:
            root_router.include_router(plugin.public_router(self))

        return root_router

    async def get_user_from_session(
        self,
        db: DBConnection,
        session_id: UUID,
    ) -> UserT | None:
        """Retrieve user from a session ID.

        This method maintains backward compatibility by delegating to BelgieClient internally.

        Args:
            db: Database connection
            session_id: UUID of the session

        Returns:
            User object if session is valid and user exists, None otherwise

        Example:
            >>> user = await belgie.get_user_from_session(db, session_id)
            >>> if user:
            ...     print(f"Found user: {user.email}")
        """
        client = self.__call__(db)
        return await client.get_user_from_session(session_id)

    async def sign_out(
        self,
        db: DBConnection,
        session_id: UUID,
    ) -> bool:
        """Sign out a user by deleting their session.

        This method maintains backward compatibility by delegating to BelgieClient internally.

        Args:
            db: Database connection
            session_id: UUID of the session to delete

        Returns:
            True if session was deleted, False if session didn't exist

        Example:
            >>> success = await belgie.sign_out(db, session_id)
            >>> if success:
            ...     print("User signed out successfully")
        """
        client = self.__call__(db)
        return await client.sign_out(session_id)

    async def _get_session_from_cookie(
        self,
        request: Request,
        db: DBConnection,
    ) -> SessionT | None:
        """Extract and validate session from request cookies.

        This method delegates to BelgieClient for consistency.

        Args:
            request: FastAPI Request object
            db: Database connection

        Returns:
            Session if valid, None otherwise
        """
        client = self.__call__(db)
        return await client._get_session_from_cookie(request)  # noqa: SLF001

    async def user(
        self,
        security_scopes: SecurityScopes,
        request: Request,
        db: DBConnection,
    ) -> UserT:
        """FastAPI dependency for retrieving the authenticated user.

        Extracts the session from cookies, validates it, and returns the authenticated user.
        Optionally validates user-level scopes if specified.

        This method maintains backward compatibility by delegating to BelgieClient internally.

        Args:
            security_scopes: FastAPI SecurityScopes for scope validation
            request: FastAPI Request object containing cookies
            db: Database connection

        Returns:
            Authenticated user object

        Raises:
            HTTPException: 401 if not authenticated or session invalid
            HTTPException: 403 if required scopes are not granted

        Example:
            >>> from fastapi import Depends, Security
            >>>
            >>> @app.get("/protected")
            >>> async def protected_route(user: User = Depends(belgie.user)):
            ...     return {"email": user.email}
            >>>
            >>> @app.get("/resource")
            >>> async def resource_route(user: User = Security(belgie.user, scopes=[Scope.READ])):
            ...     return {"data": "..."}
        """
        client = self.__call__(db)
        return await client.get_user(security_scopes, request)

    async def session(
        self,
        request: Request,
        db: DBConnection,
    ) -> SessionT:
        """FastAPI dependency for retrieving the current session.

        Extracts and validates the session from cookies.

        This method maintains backward compatibility by delegating to BelgieClient internally.

        Args:
            request: FastAPI Request object containing cookies
            db: Database connection

        Returns:
            Active session object

        Raises:
            HTTPException: 401 if not authenticated or session invalid/expired

        Example:
            >>> from fastapi import Depends
            >>>
            >>> @app.get("/session-info")
            >>> async def session_info(session: Session = Depends(belgie.session)):
            ...     return {"session_id": str(session.id), "expires_at": session.expires_at.isoformat()}
        """
        client = self.__call__(db)
        return await client.get_session(request)
