from logging import Logger
from typing import TypedDict

from tornado.ioloop import IOLoop
from tornado.web import RequestHandler

from pcs.daemon import log
from pcs.daemon.app.auth_provider import (
    ApiAuthProviderFactoryInterface,
    ApiAuthProviderInterface,
    NotAuthorizedException,
)
from pcs.daemon.app.webui.session import Session, Storage
from pcs.lib.auth.provider import AuthProvider
from pcs.lib.auth.types import AuthUser

# Constants for session authentication
PCSD_SESSION = "pcsd.sid"


class CookieOptions(TypedDict):
    secure: bool
    httponly: bool
    samesite: str


# Cookie options for session cookie
# Matches settings from webui/auth.py for compatibility
SESSION_COOKIE_OPTIONS: CookieOptions = {
    "secure": True,
    "httponly": True,
    # rhbz#2097393
    # Prevent a cookie to be sent on cross-site requests, allow it to be
    # sent when navigating to pcs web UI from an external site.
    "samesite": "Lax",
}


class SessionAuthProvider(ApiAuthProviderInterface):
    """
    Authentication provider for session-based authentication (WebUI).

    Authenticates users via session cookie. Sessions are managed by the
    session storage and identified by the "pcsd.sid" cookie.
    """

    def __init__(
        self,
        handler: RequestHandler,
        lib_auth_provider: AuthProvider,
        session_storage: Storage,
        logger: Logger,
    ) -> None:
        """
        Initialize session auth provider.

        handler -- Tornado request handler
        lib_auth_provider -- library auth provider for user validation
        session_storage -- session storage
        """
        self._handler = handler
        self._lib_auth_provider = lib_auth_provider
        self._session_storage = session_storage
        self._session: Session | None = None
        self._logger = logger

    @property
    def __sid_from_client(self) -> str | None:
        return self._handler.get_cookie(PCSD_SESSION, default=None)

    def can_handle_request(self) -> bool:
        if self._session is not None:
            return True

        if self.__sid_from_client is None:
            return False
        self._session = self._session_storage.get(self.__sid_from_client)
        return self._session is not None

    async def auth_user(self) -> AuthUser:
        self._logger.debug("Attempting authentication via session")
        if not self.can_handle_request():
            self._logger.debug(
                "Credentials for authentication via session not provided, "
                "or the session is not known to pcsd"
            )
            raise NotAuthorizedException()

        # mypy complains that self.__session can be None when passed into
        # the lambda below. Passing local variable fixes this
        assert self._session is not None
        session = self._session
        auth_user = await IOLoop.current().run_in_executor(
            executor=None,
            func=lambda: self._lib_auth_provider.login_user(session.username),
        )
        if auth_user is None:
            raise NotAuthorizedException()

        # Update session cookie in response
        self._handler.set_cookie(
            PCSD_SESSION, self._session.sid, **SESSION_COOKIE_OPTIONS
        )

        return auth_user


class SessionAuthProviderFactory(ApiAuthProviderFactoryInterface):
    def __init__(
        self, lib_auth_provider: AuthProvider, session_storage: Storage
    ) -> None:
        self._lib_auth_provider = lib_auth_provider
        self._session_storage = session_storage

    def create(self, handler: RequestHandler) -> SessionAuthProvider:
        return SessionAuthProvider(
            handler, self._lib_auth_provider, self._session_storage, log.pcsd
        )
