"""
PDND access token generation via OAuth2 client credentials flow.

This module provides the logic to exchange a client assertion JWT for an
access token from the PDND (Piattaforma Digitale Nazionale Dati) token endpoint.

The token exchange follows the OAuth2 client credentials flow with JWT bearer
assertion as defined in RFC 7523.

Example usage (programmatic):
    >>> from anncsu.common.pdnd_token import TokenConfig, get_access_token
    >>> config = TokenConfig(
    ...     client_id="43508172-aa22-46b0-8c01-3006e745c73c",
    ...     client_assertion="eyJraWQiOi...",  # JWT from create_client_assertion
    ...     token_endpoint="https://auth.uat.interop.pagopa.it/token.oauth2",
    ... )
    >>> token_response = get_access_token(config)
    >>> print(token_response.access_token)

Example usage (with assertion generation):
    >>> from anncsu.common.pdnd_assertion import ClientAssertionConfig, create_client_assertion
    >>> from anncsu.common.pdnd_token import TokenConfig, get_access_token
    >>>
    >>> # First, create the client assertion
    >>> assertion_config = ClientAssertionConfig(
    ...     kid="my-key-id",
    ...     issuer="my-client-id",
    ...     subject="my-client-id",
    ...     audience="auth.uat.interop.pagopa.it/client-assertion",
    ...     purpose_id="my-purpose-id",
    ...     key_path=Path("./private_key.pem"),
    ... )
    >>> client_assertion = create_client_assertion(assertion_config)
    >>>
    >>> # Then, exchange it for an access token
    >>> token_config = TokenConfig(
    ...     client_id="my-client-id",
    ...     client_assertion=client_assertion,
    ...     token_endpoint="https://auth.uat.interop.pagopa.it/token.oauth2",
    ... )
    >>> token_response = get_access_token(token_config)

Example usage (from environment variables):
    >>> from anncsu.common.config import TokenSettings
    >>> from anncsu.common.pdnd_token import get_access_token
    >>> settings = TokenSettings()  # Loads from env
    >>> token_response = get_access_token(settings.to_config())
"""

from __future__ import annotations

from typing import Annotated

import httpx
from pydantic import BaseModel, Field, field_validator

# Constants
CLIENT_ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
GRANT_TYPE = "client_credentials"
DEFAULT_TIMEOUT = 30.0


class TokenError(Exception):
    """Base exception for token-related errors."""

    pass


class TokenRequestError(TokenError):
    """Exception raised when the token request fails."""

    def __init__(
        self,
        message: str,
        status_code: int | None = None,
        response_body: str | None = None,
    ):
        super().__init__(message)
        self.status_code = status_code
        self.response_body = response_body


class TokenResponseError(TokenError):
    """Exception raised when the token response is invalid or contains an error."""

    def __init__(
        self,
        message: str,
        error: str | None = None,
        error_description: str | None = None,
    ):
        super().__init__(message)
        self.error = error
        self.error_description = error_description


class TokenConfig(BaseModel):
    """Configuration for requesting an access token from PDND.

    This model validates all parameters required to exchange a client assertion
    for an access token.

    Attributes:
        client_id: The client ID registered with PDND.
        client_assertion: The JWT client assertion generated by create_client_assertion.
        token_endpoint: The PDND token endpoint URL.
        client_assertion_type: The assertion type (default: urn:ietf:params:oauth:client-assertion-type:jwt-bearer).
        grant_type: The OAuth2 grant type (default: client_credentials).
        timeout: Request timeout in seconds (default: 30.0).

    Example:
        >>> config = TokenConfig(
        ...     client_id="43508172-aa22-46b0-8c01-3006e745c73c",
        ...     client_assertion="eyJraWQiOi...",
        ...     token_endpoint="https://auth.uat.interop.pagopa.it/token.oauth2",
        ... )
    """

    client_id: Annotated[
        str,
        Field(
            description="The client ID registered with PDND",
            min_length=1,
        ),
    ]
    client_assertion: Annotated[
        str,
        Field(
            description="The JWT client assertion",
            min_length=1,
        ),
    ]
    token_endpoint: Annotated[
        str,
        Field(
            description="The PDND token endpoint URL",
            pattern=r"^https://.*",
        ),
    ]
    client_assertion_type: Annotated[
        str,
        Field(
            description="The client assertion type for OAuth2",
        ),
    ] = CLIENT_ASSERTION_TYPE
    grant_type: Annotated[
        str,
        Field(
            description="The OAuth2 grant type",
        ),
    ] = GRANT_TYPE
    timeout: Annotated[
        float,
        Field(
            description="Request timeout in seconds",
            gt=0,
        ),
    ] = DEFAULT_TIMEOUT

    @field_validator("token_endpoint")
    @classmethod
    def validate_token_endpoint(cls, v: str) -> str:
        """Validate that the token endpoint is an HTTPS URL."""
        if not v.startswith("https://"):
            raise ValueError("Token endpoint must be an HTTPS URL")
        return v


class TokenResponse(BaseModel):
    """Response from the PDND token endpoint.

    This model represents the OAuth2 token response containing the access token
    and related metadata.

    Attributes:
        access_token: The access token to use for API requests.
        token_type: The type of token (typically "Bearer").
        expires_in: The token lifetime in seconds.

    Example:
        >>> response = TokenResponse(
        ...     access_token="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
        ...     token_type="Bearer",
        ...     expires_in=600,
        ... )
    """

    access_token: Annotated[
        str,
        Field(
            description="The access token to use for API requests",
        ),
    ]
    token_type: Annotated[
        str,
        Field(
            description="The type of token (typically 'Bearer')",
        ),
    ] = "Bearer"
    expires_in: Annotated[
        int | None,
        Field(
            description="The token lifetime in seconds",
        ),
    ] = None


def get_access_token(
    config: TokenConfig,
    *,
    client: httpx.Client | None = None,
) -> TokenResponse:
    """
    Exchange a client assertion for an access token.

    This function makes a POST request to the PDND token endpoint with the
    client assertion and returns the access token response.

    Args:
        config: Configuration for the token request (validated by Pydantic).
        client: Optional httpx.Client to use for the request.
                If not provided, a new client will be created.

    Returns:
        TokenResponse: The token response containing the access token.

    Raises:
        TokenRequestError: If the HTTP request fails (network error, timeout, etc.).
        TokenResponseError: If the token endpoint returns an error response.

    Example:
        >>> config = TokenConfig(
        ...     client_id="my-client-id",
        ...     client_assertion="eyJraWQiOi...",
        ...     token_endpoint="https://auth.uat.interop.pagopa.it/token.oauth2",
        ... )
        >>> response = get_access_token(config)
        >>> print(response.access_token)
    """
    # Prepare form data
    form_data = {
        "client_id": config.client_id,
        "client_assertion": config.client_assertion,
        "client_assertion_type": config.client_assertion_type,
        "grant_type": config.grant_type,
    }

    headers = {
        "Content-Type": "application/x-www-form-urlencoded",
    }

    # Use provided client or create a new one
    should_close_client = client is None
    if client is None:
        client = httpx.Client(timeout=config.timeout)

    try:
        response = client.post(
            config.token_endpoint,
            data=form_data,
            headers=headers,
        )
    except httpx.TimeoutException as e:
        raise TokenRequestError(
            f"Request timed out after {config.timeout} seconds",
            status_code=None,
            response_body=None,
        ) from e
    except httpx.RequestError as e:
        raise TokenRequestError(
            f"Failed to connect to token endpoint: {e}",
            status_code=None,
            response_body=None,
        ) from e
    finally:
        if should_close_client:
            client.close()

    # Check for HTTP errors
    if response.status_code != 200:
        # Try to parse error response
        try:
            error_data = response.json()

            # Check for standard OAuth2 error format
            error = error_data.get("error")
            error_description = error_data.get("error_description")

            # Check for RFC 7807 Problem Details format (used by PDND)
            if error is None:
                # Try RFC 7807 format
                title = error_data.get("title")
                detail = error_data.get("detail")
                errors = error_data.get("errors", [])
                correlation_id = error_data.get("correlationId")

                # Extract error details from nested errors array if present
                if errors and isinstance(errors, list) and len(errors) > 0:
                    first_error = errors[0]
                    error = first_error.get("code", str(response.status_code))
                    error_description = first_error.get("detail", detail or title)
                else:
                    error = str(response.status_code)
                    error_description = detail or title

                # Include correlation ID in the message if present
                msg = f"Token request failed: {error} - {error_description}"
                if correlation_id:
                    msg += f" (correlationId: {correlation_id})"

                raise TokenResponseError(
                    msg,
                    error=error,
                    error_description=error_description,
                )

            raise TokenResponseError(
                f"Token request failed: {error} - {error_description}",
                error=error,
                error_description=error_description,
            )
        except ValueError:
            # Could not parse error response as JSON
            raise TokenRequestError(
                f"Token request failed with status {response.status_code}",
                status_code=response.status_code,
                response_body=response.text,
            ) from None

    # Parse successful response
    try:
        response_data = response.json()
    except ValueError as e:
        raise TokenResponseError(
            "Invalid JSON response from token endpoint",
            error="invalid_response",
            error_description=str(e),
        ) from e

    # Validate and return the token response
    try:
        return TokenResponse.model_validate(response_data)
    except Exception as e:
        raise TokenResponseError(
            f"Invalid token response format: {e}",
            error="invalid_response",
            error_description=str(e),
        ) from e


async def get_access_token_async(
    config: TokenConfig,
    *,
    client: httpx.AsyncClient | None = None,
) -> TokenResponse:
    """
    Exchange a client assertion for an access token (async version).

    This function makes an async POST request to the PDND token endpoint with the
    client assertion and returns the access token response.

    Args:
        config: Configuration for the token request (validated by Pydantic).
        client: Optional httpx.AsyncClient to use for the request.
                If not provided, a new client will be created.

    Returns:
        TokenResponse: The token response containing the access token.

    Raises:
        TokenRequestError: If the HTTP request fails (network error, timeout, etc.).
        TokenResponseError: If the token endpoint returns an error response.

    Example:
        >>> import asyncio
        >>> config = TokenConfig(
        ...     client_id="my-client-id",
        ...     client_assertion="eyJraWQiOi...",
        ...     token_endpoint="https://auth.uat.interop.pagopa.it/token.oauth2",
        ... )
        >>> response = asyncio.run(get_access_token_async(config))
        >>> print(response.access_token)
    """
    # Prepare form data
    form_data = {
        "client_id": config.client_id,
        "client_assertion": config.client_assertion,
        "client_assertion_type": config.client_assertion_type,
        "grant_type": config.grant_type,
    }

    headers = {
        "Content-Type": "application/x-www-form-urlencoded",
    }

    # Use provided client or create a new one
    should_close_client = client is None
    if client is None:
        client = httpx.AsyncClient(timeout=config.timeout)

    try:
        response = await client.post(
            config.token_endpoint,
            data=form_data,
            headers=headers,
        )
    except httpx.TimeoutException as e:
        raise TokenRequestError(
            f"Request timed out after {config.timeout} seconds",
            status_code=None,
            response_body=None,
        ) from e
    except httpx.RequestError as e:
        raise TokenRequestError(
            f"Failed to connect to token endpoint: {e}",
            status_code=None,
            response_body=None,
        ) from e
    finally:
        if should_close_client:
            await client.aclose()

    # Check for HTTP errors
    if response.status_code != 200:
        # Try to parse error response
        try:
            error_data = response.json()

            # Check for standard OAuth2 error format
            error = error_data.get("error")
            error_description = error_data.get("error_description")

            # Check for RFC 7807 Problem Details format (used by PDND)
            if error is None:
                # Try RFC 7807 format
                title = error_data.get("title")
                detail = error_data.get("detail")
                errors = error_data.get("errors", [])
                correlation_id = error_data.get("correlationId")

                # Extract error details from nested errors array if present
                if errors and isinstance(errors, list) and len(errors) > 0:
                    first_error = errors[0]
                    error = first_error.get("code", str(response.status_code))
                    error_description = first_error.get("detail", detail or title)
                else:
                    error = str(response.status_code)
                    error_description = detail or title

                # Include correlation ID in the message if present
                msg = f"Token request failed: {error} - {error_description}"
                if correlation_id:
                    msg += f" (correlationId: {correlation_id})"

                raise TokenResponseError(
                    msg,
                    error=error,
                    error_description=error_description,
                )

            raise TokenResponseError(
                f"Token request failed: {error} - {error_description}",
                error=error,
                error_description=error_description,
            )
        except ValueError:
            # Could not parse error response as JSON
            raise TokenRequestError(
                f"Token request failed with status {response.status_code}",
                status_code=response.status_code,
                response_body=response.text,
            ) from None

    # Parse successful response
    try:
        response_data = response.json()
    except ValueError as e:
        raise TokenResponseError(
            "Invalid JSON response from token endpoint",
            error="invalid_response",
            error_description=str(e),
        ) from e

    # Validate and return the token response
    try:
        return TokenResponse.model_validate(response_data)
    except Exception as e:
        raise TokenResponseError(
            f"Invalid token response format: {e}",
            error="invalid_response",
            error_description=str(e),
        ) from e


__all__ = [
    "TokenConfig",
    "TokenResponse",
    "TokenError",
    "TokenRequestError",
    "TokenResponseError",
    "get_access_token",
    "get_access_token_async",
    "CLIENT_ASSERTION_TYPE",
    "GRANT_TYPE",
]
