"""OIDC discovery endpoint client.

Fetches and parses OIDC provider configuration from
`.well-known/openid-configuration` endpoints.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

import httpx

from noteflow.domain.auth.oidc import OidcDiscoveryConfig
from noteflow.infrastructure.logging import get_logger

if TYPE_CHECKING:
    from noteflow.domain.auth.oidc import OidcProviderConfig

logger = get_logger(__name__)


class OidcDiscoveryError(Exception):
    """Error during OIDC discovery."""

    def __init__(self, message: str, issuer_url: str | None = None) -> None:
        """Initialize with message and optional issuer URL."""
        super().__init__(message)
        self.issuer_url = issuer_url


class OidcDiscoveryClient:
    """Client for fetching OIDC discovery documents.

    Fetches the `.well-known/openid-configuration` document from an OIDC
    provider and parses it into an OidcDiscoveryConfig.
    """

    def __init__(
        self,
        timeout: float = 10.0,
        verify_ssl: bool = True,
    ) -> None:
        """Initialize the discovery client.

        Args:
            timeout: HTTP request timeout in seconds.
            verify_ssl: Whether to verify SSL certificates.
        """
        self._timeout = timeout
        self._verify_ssl = verify_ssl

    async def discover(self, issuer_url: str) -> OidcDiscoveryConfig:
        """Fetch and parse OIDC discovery document.

        Args:
            issuer_url: OIDC issuer URL (base URL, without .well-known path).

        Returns:
            Parsed discovery configuration.

        Raises:
            OidcDiscoveryError: If discovery fails or document is invalid.
        """
        discovery_url = f"{issuer_url.rstrip('/')}/.well-known/openid-configuration"

        try:
            async with httpx.AsyncClient(
                timeout=self._timeout,
                verify=self._verify_ssl,
            ) as client:
                response = await client.get(discovery_url)
                response.raise_for_status()
                data = response.json()

        except httpx.TimeoutException as exc:
            logger.warning("OIDC discovery timeout for %s", issuer_url)
            raise OidcDiscoveryError(
                f"Timeout fetching discovery document from {discovery_url}",
                issuer_url=issuer_url,
            ) from exc
        except httpx.HTTPStatusError as exc:
            logger.warning(
                "OIDC discovery HTTP error for %s: %s",
                issuer_url,
                exc.response.status_code,
            )
            raise OidcDiscoveryError(
                f"HTTP {exc.response.status_code} fetching discovery document",
                issuer_url=issuer_url,
            ) from exc
        except httpx.RequestError as exc:
            logger.warning("OIDC discovery request error for %s: %s", issuer_url, exc)
            raise OidcDiscoveryError(
                f"Request error fetching discovery document: {exc}",
                issuer_url=issuer_url,
            ) from exc
        except ValueError as exc:
            logger.warning("OIDC discovery JSON parse error for %s", issuer_url)
            raise OidcDiscoveryError(
                "Invalid JSON in discovery document",
                issuer_url=issuer_url,
            ) from exc

        return self._parse_discovery(data, issuer_url)

    def _parse_discovery(
        self,
        data: dict[str, object],
        issuer_url: str,
    ) -> OidcDiscoveryConfig:
        """Parse discovery document into OidcDiscoveryConfig.

        Args:
            data: Raw JSON data from discovery endpoint.
            issuer_url: Expected issuer URL for validation.

        Returns:
            Parsed OidcDiscoveryConfig.

        Raises:
            OidcDiscoveryError: If required fields are missing or invalid.
        """
        # Validate required fields
        issuer = data.get("issuer")
        if not issuer:
            raise OidcDiscoveryError(
                "Missing required 'issuer' in discovery document",
                issuer_url=issuer_url,
            )

        # Validate issuer matches (security check)
        # Allow trailing slash differences
        normalized_expected = issuer_url.rstrip("/")
        normalized_actual = str(issuer).rstrip("/")
        if normalized_expected != normalized_actual:
            logger.warning(
                "Issuer mismatch: expected %s, got %s",
                normalized_expected,
                normalized_actual,
            )
            # Don't fail, but log warning - some providers have quirks

        auth_endpoint = data.get("authorization_endpoint")
        if not auth_endpoint:
            raise OidcDiscoveryError(
                "Missing required 'authorization_endpoint' in discovery document",
                issuer_url=issuer_url,
            )

        if data.get("token_endpoint"):
            return OidcDiscoveryConfig.from_dict(data)
        else:
            raise OidcDiscoveryError(
                "Missing required 'token_endpoint' in discovery document",
                issuer_url=issuer_url,
            )

    async def discover_and_update(
        self,
        provider: OidcProviderConfig,
    ) -> OidcProviderConfig:
        """Fetch discovery and update provider configuration.

        Args:
            provider: Provider to update with discovery information.

        Returns:
            Updated provider with discovery configuration.

        Raises:
            OidcDiscoveryError: If discovery fails.
        """
        discovery = await self.discover(provider.issuer_url)
        provider.update_discovery(discovery)

        logger.info(
            "Updated OIDC discovery for provider %s (%s)",
            provider.name,
            provider.issuer_url,
        )
        return provider

    async def validate_provider(self, provider: OidcProviderConfig) -> list[str]:
        """Validate a provider configuration.

        Checks that discovery works and the provider supports required features.

        Args:
            provider: Provider configuration to validate.

        Returns:
            List of warning messages (empty if all checks pass).

        Raises:
            OidcDiscoveryError: If discovery fails.
        """
        warnings: list[str] = []

        discovery = await self.discover(provider.issuer_url)

        # Check PKCE support
        if not discovery.supports_pkce():
            warnings.append(
                "Provider does not advertise PKCE support with S256. "
                "Authentication may still work if PKCE is optional."
            )

        # Check requested scopes are supported
        if discovery.scopes_supported:
            if unsupported := set(provider.scopes) - set(
                discovery.scopes_supported
            ):
                warnings.append(
                    f"Requested scopes not in supported list: {unsupported}"
                )

        # Check claim mapping claims are supported
        if discovery.claims_supported:
            mapping = provider.claim_mapping
            claim_attrs = [
                mapping.subject_claim,
                mapping.email_claim,
                mapping.name_claim,
            ]
            warnings.extend(
                f"Claim '{claim}' not in supported claims list"
                for claim in claim_attrs
                if claim not in discovery.claims_supported
            )
        return warnings
