import functools
import warnings
from typing import Callable, Self, overload

from pyidentity.exc import InvalidOperationError
from pyidentity.identity_constants import IdentityConstants
from pyidentity.security.authentication.jwt.auth import (
    CookieAuthenticationOptions,
    JWTBearerAuthenticationHandler,
    JWTCookieAuthenticationHandler,
    TokenValidationParameters,
)
from pyidentity.security.authentication.options import AuthenticationOptions
from pyidentity.security.authentication.scheme import AuthenticationScheme
from pyidentity.security.authentication.types import ConfigureScheme
from pyidentity.utils import get_machineid_key

__all__ = ("AuthenticationBuilder",)


class AuthenticationBuilder:
    """Used to configure authentication."""

    __slots__ = ("_options",)

    def __init__(self, options: AuthenticationOptions, default_scheme: str = ""):
        self._options = options
        self.set_default_scheme(default_scheme)

    @overload
    def add_scheme(self, name: str, scheme: AuthenticationScheme, /) -> Self: ...

    @overload
    def add_scheme(self, name: str, configure_builder: ConfigureScheme, /) -> Self: ...

    def add_scheme(
        self,
        name: str,
        scheme_or_configure_builder: AuthenticationScheme | ConfigureScheme,
        /,
    ) -> Self:
        """Add an authentication scheme or built from a delegate with the provided name.

        :param name: The name of the scheme.
        :param scheme_or_configure_builder: The scheme or the configure scheme.
        :return:
        """
        self._options.add_scheme(name, scheme_or_configure_builder)
        return self

    def add_jwt_cookie(
        self,
        scheme: str = "Cookie",
        signin_cookie_key: str | None = None,
        algorithm: str = "HS256",
        validation_parameters: TokenValidationParameters | None = None,
        cookie_options: CookieAuthenticationOptions | None = None,
    ) -> Self:
        """Adds cookies authentication to *AuthenticationBuilder* using the specified scheme.

        :param scheme: The authentication scheme.
        :param signin_cookie_key:
        :param algorithm:
        :param validation_parameters:
        :param cookie_options:
        :return:
        """
        return self.add_scheme(
            scheme,
            AuthenticationScheme(
                scheme,
                JWTCookieAuthenticationHandler(
                    signin_cookie_key,
                    algorithm,
                    validation_parameters,
                    cookie_options,
                ),
            ),
        )

    def add_jwt_bearer(
        self,
        scheme: str = "Bearer",
        *,
        validation_parameters: TokenValidationParameters,
    ) -> Self:
        """Enables JWT-bearer authentication using the default scheme Bearer.

        :param scheme: The authentication scheme.
        :param validation_parameters:
        :return:
        """
        self.add_scheme(
            scheme,
            AuthenticationScheme(
                scheme,
                JWTBearerAuthenticationHandler(validation_parameters),
            ),
        )
        return self

    def set_default_scheme(self, scheme: str) -> Self:
        self._options.default_scheme = scheme
        return self

    def add_identity_cookies(self) -> Self:
        warnings.warn(
            "The 'py-machineid' is used to generate the key.",
            ImportWarning,
            stacklevel=2,
        )
        add_jwt_cookie = functools.partial(self.add_jwt_cookie, signin_cookie_key=get_machineid_key())
        add_jwt_cookie(scheme=IdentityConstants.ApplicationScheme)
        add_jwt_cookie(scheme=IdentityConstants.TwoFactorRememberMeScheme)
        add_jwt_cookie(scheme=IdentityConstants.TwoFactorUserIdScheme)
        add_jwt_cookie(
            scheme=IdentityConstants.ExternalScheme,
            cookie_options=CookieAuthenticationOptions(expires_timedelta=600),
        )
        return self

    def configure(self, scheme: str, configure: Callable[[CookieAuthenticationOptions], None]) -> Self:
        def configure_cookie_options(_scheme, _configure, options: AuthenticationOptions) -> None:
            if _scheme := options.scheme_map.get(_scheme):
                if not hasattr(_scheme.handler, "options"):
                    raise InvalidOperationError("The 'handler' does not have the 'options' attribute.")
                options = _scheme.handler.options
                if not isinstance(options, CookieAuthenticationOptions):
                    raise InvalidOperationError("Options is not an instance of 'CookieAuthenticationOptions'.")
                return _configure(options)
            raise InvalidOperationError(f"Scheme not exists: {_scheme}.")

        configure_cookie_options(scheme, configure, self._options)
        return self

    def configure_application_cookie(self, configure: Callable[[CookieAuthenticationOptions], None]) -> Self:
        self.configure(IdentityConstants.ApplicationScheme, configure)
        return self

    def configure_external_cookie(self, configure: Callable[[CookieAuthenticationOptions], None]) -> Self:
        self.configure(IdentityConstants.ExternalScheme, configure)
        return self
