"""Provides a service to interact with the Hydra service."""

import json
from base64 import b64encode
from typing import Any, Generic, TypeVar, get_args

import aiohttp
import jwt
from pydantic import ValidationError

from fastapi_factory_utilities.core.plugins.aiohttp import (
    AioHttpClientResource,
)
from fastapi_factory_utilities.core.security.jwt import JWTBearerAuthenticationConfig
from fastapi_factory_utilities.core.security.types import OAuth2Issuer

from .exceptions import HydraOperationError
from .objects import HydraTokenIntrospectObject
from .types import HydraAccessToken, HydraClientId, HydraClientSecret

HydraIntrospectObjectGeneric = TypeVar("HydraIntrospectObjectGeneric", bound=HydraTokenIntrospectObject)


class HydraIntrospectGenericService(Generic[HydraIntrospectObjectGeneric]):
    """Service to interact with the Hydra introspect service with a generic introspect object."""

    INTROSPECT_ENDPOINT: str = "/admin/oauth2/introspect"
    WELLKNOWN_JWKS_ENDPOINT: str = "/.well-known/jwks.json"

    def __init__(
        self,
        identifier: str,
        config: JWTBearerAuthenticationConfig,
        hydra_admin_http_resource: AioHttpClientResource,
        hydra_public_http_resource: AioHttpClientResource,
    ) -> None:
        """Instanciate the Hydra introspect service.

        Args:
            identifier (str): The identifier of the Hydra introspect service.
            config (JWTBearerAuthenticationConfig): The JWT bearer authentication configuration.
            hydra_admin_http_resource (AioHttpClientResource): The Hydra admin HTTP resource.
            hydra_public_http_resource (AioHttpClientResource): The Hydra public HTTP resource.
        """
        self._identifier: str = identifier
        self._config: JWTBearerAuthenticationConfig = config
        self._hydra_admin_http_resource: AioHttpClientResource = hydra_admin_http_resource
        self._hydra_public_http_resource: AioHttpClientResource = hydra_public_http_resource
        # Retrieve the concrete introspect object class
        generic_args: tuple[Any, ...] = get_args(self.__orig_bases__[0])  # type: ignore
        self._concreate_introspect_object_class: type[HydraIntrospectObjectGeneric] = generic_args[0]

    async def introspect(self, token: HydraAccessToken) -> HydraIntrospectObjectGeneric:
        """Introspects a token using the Hydra introspect service.

        Args:
            token (str): The token to introspect.
        """
        try:
            async with self._hydra_admin_http_resource.acquire_client_session() as session:
                async with session.post(
                    url=self.INTROSPECT_ENDPOINT,
                    data={"token": token},
                ) as response:
                    response.raise_for_status()
                    instrospect: HydraIntrospectObjectGeneric = self._concreate_introspect_object_class.model_validate(
                        await response.json()
                    )
        except aiohttp.ClientResponseError as error:
            raise HydraOperationError(
                "An error occurred while introspecting the token", status_code=error.status
            ) from error
        except json.JSONDecodeError as error:
            raise HydraOperationError("An error occurred while decoding the introspect response") from error
        except ValidationError as error:
            raise HydraOperationError("An error occurred while validating the introspect response") from error
        except Exception as error:
            raise HydraOperationError("An error occurred while introspecting the token") from error

        return instrospect

    def get_issuer(self) -> OAuth2Issuer:
        """Get the issuer from the JWT bearer authentication configuration."""
        return self._config.issuer

    async def get_wellknown_jwks(self) -> list[jwt.PyJWK]:
        """Get the JWKS from the Hydra service."""
        try:
            async with self._hydra_public_http_resource.acquire_client_session() as session:
                async with session.get(
                    url=self.WELLKNOWN_JWKS_ENDPOINT,
                ) as response:
                    response.raise_for_status()
                    jwks_data: dict[str, Any] = await response.json()
                    jwks: jwt.PyJWKSet = jwt.PyJWKSet.from_dict(jwks_data)
                    return jwks.keys  # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
        except aiohttp.ClientResponseError as error:
            raise HydraOperationError(
                "Failed to get the JWKS from the Hydra service", status_code=error.status
            ) from error
        except json.JSONDecodeError as error:
            raise HydraOperationError("Failed to decode the JWKS from the Hydra service") from error
        except ValidationError as error:
            raise HydraOperationError("Failed to validate the JWKS from the Hydra service") from error
        except Exception as error:
            raise HydraOperationError("Failed to get the JWKS from the Hydra service") from error


class HydraIntrospectService(HydraIntrospectGenericService[HydraTokenIntrospectObject]):
    """Service to interact with the Hydra introspect service with the default HydraTokenIntrospectObject."""


class HydraOAuth2ClientCredentialsService:
    """Service to interact with the Hydra service."""

    INTROSPECT_ENDPOINT: str = "/admin/oauth2/introspect"
    CLIENT_CREDENTIALS_ENDPOINT: str = "/oauth2/token"

    def __init__(
        self,
        identifier: str,
        hydra_public_http_resource: AioHttpClientResource,
        config: JWTBearerAuthenticationConfig,
        default_audience: str,
    ) -> None:
        """Instanciate the Hydra service.

        Args:
            identifier (str): The identifier of the Hydra OAuth2 client credentials service.
            hydra_public_http_resource (AioHttpClientResource): The Hydra public HTTP resource.
            config (JWTBearerAuthenticationConfig): The JWT bearer authentication configuration.
            default_audience (str): The default audience.
        """
        self._identifier: str = identifier
        self._hydra_public_http_resource: AioHttpClientResource = hydra_public_http_resource
        self._config: JWTBearerAuthenticationConfig = config
        self._default_audience: str = default_audience

    @classmethod
    def build_bearer_header(cls, client_id: HydraClientId, client_secret: HydraClientSecret) -> str:
        """Build the bearer header.

        Args:
            client_id (str): The client ID.
            client_secret (str): The client secret.

        Returns:
            str: The bearer header.
        """
        auth_string: str = f"{client_id}:{client_secret}"
        auth_bytes: bytes = auth_string.encode("utf-8")
        auth_b64: str = b64encode(auth_bytes).decode("utf-8")
        return f"Basic {auth_b64}"

    async def oauth2_client_credentials(
        self, client_id: HydraClientId, client_secret: HydraClientSecret, scopes: list[str], audience: str | None = None
    ) -> HydraAccessToken:
        """Get the OAuth2 client credentials.

        Args:
            client_id (str): The client ID.
            client_secret (str): The client secret.
            scopes (list[str]): The scopes.
            audience (str, optional): The audience. Defaults to None.

        Returns:
            str: The access token.

        Raises:
            HydraOperationError: If the client credentials request fails.
        """
        enforced_audience: str = audience if audience is not None else self._default_audience
        try:
            async with self._hydra_public_http_resource.acquire_client_session() as session:
                async with session.post(
                    url=self.CLIENT_CREDENTIALS_ENDPOINT,
                    headers={"Authorization": self.build_bearer_header(client_id, client_secret)},
                    data={
                        "grant_type": "client_credentials",
                        "scope": " ".join(scopes),
                        "audience": enforced_audience,
                    },
                ) as response:
                    response.raise_for_status()
                    response_data = await response.json()
                    return response_data["access_token"]
        except aiohttp.ClientResponseError as error:
            raise HydraOperationError(
                "An error occurred while getting the client credentials", status_code=error.status
            ) from error
        except json.JSONDecodeError as error:
            raise HydraOperationError(
                "An error occurred while getting the client credentials, invalid JSON response"
            ) from error
        except ValidationError as error:
            raise HydraOperationError(
                "An error occurred while getting the client credentials, invalid response"
            ) from error
        except Exception as error:
            raise HydraOperationError(
                "An error occurred while getting the client credentials, unknown error"
            ) from error
