"""OIDC provider registry with presets for common identity providers.

This module provides pre-configured settings for popular OIDC providers
like Authentik, Authelia, Keycloak, Auth0, Okta, and Azure AD.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import TYPE_CHECKING
from uuid import UUID

from noteflow.domain.auth.oidc import (
    ClaimMapping,
    OidcProviderConfig,
    OidcProviderCreateParams,
    OidcProviderPreset,
)
from noteflow.infrastructure.auth.oidc_discovery import (
    OidcDiscoveryClient,
    OidcDiscoveryError,
)
from noteflow.infrastructure.logging import get_logger

if TYPE_CHECKING:
    from noteflow.domain.ports.unit_of_work import UnitOfWork

logger = get_logger(__name__)


@dataclass(frozen=True, slots=True)
class ProviderPresetConfig:
    """Pre-configured settings for a provider preset.

    These settings represent the defaults and recommendations for each
    provider type. Users can override these when creating a provider.
    """

    preset: OidcProviderPreset
    display_name: str
    description: str
    default_scopes: tuple[str, ...]
    claim_mapping: ClaimMapping
    documentation_url: str | None = None
    notes: str | None = None


# Provider preset configurations
PROVIDER_PRESETS: dict[OidcProviderPreset, ProviderPresetConfig] = {
    OidcProviderPreset.AUTHENTIK: ProviderPresetConfig(
        preset=OidcProviderPreset.AUTHENTIK,
        display_name="Authentik",
        description="Open-source Identity Provider focused on flexibility and versatility",
        default_scopes=("openid", "profile", "email", "groups"),
        claim_mapping=ClaimMapping(
            subject_claim="sub",
            email_claim="email",
            email_verified_claim="email_verified",
            name_claim="name",
            preferred_username_claim="preferred_username",
            groups_claim="groups",
            picture_claim="picture",
        ),
        documentation_url="https://docs.goauthentik.io/docs/providers/oauth2",
        notes="Authentik supports standard OIDC claims and custom attributes via property mappings.",
    ),
    OidcProviderPreset.AUTHELIA: ProviderPresetConfig(
        preset=OidcProviderPreset.AUTHELIA,
        display_name="Authelia",
        description="Open-source authentication and authorization server",
        default_scopes=("openid", "profile", "email", "groups"),
        claim_mapping=ClaimMapping(
            subject_claim="sub",
            email_claim="email",
            email_verified_claim="email_verified",
            name_claim="name",
            preferred_username_claim="preferred_username",
            groups_claim="groups",
            picture_claim="picture",
        ),
        documentation_url="https://www.authelia.com/integration/openid-connect/",
        notes="Authelia requires explicit client registration in configuration.yml.",
    ),
    OidcProviderPreset.KEYCLOAK: ProviderPresetConfig(
        preset=OidcProviderPreset.KEYCLOAK,
        display_name="Keycloak",
        description="Open-source Identity and Access Management",
        default_scopes=("openid", "profile", "email"),
        claim_mapping=ClaimMapping(
            subject_claim="sub",
            email_claim="email",
            email_verified_claim="email_verified",
            name_claim="name",
            preferred_username_claim="preferred_username",
            groups_claim="groups",  # Requires mapper configuration
            picture_claim="picture",
        ),
        documentation_url="https://www.keycloak.org/docs/latest/server_admin/#_oidc",
        notes="For groups claim, add a 'Group Membership' mapper to the client scope.",
    ),
    OidcProviderPreset.AUTH0: ProviderPresetConfig(
        preset=OidcProviderPreset.AUTH0,
        display_name="Auth0",
        description="Identity platform for application builders",
        default_scopes=("openid", "profile", "email"),
        claim_mapping=ClaimMapping(
            subject_claim="sub",
            email_claim="email",
            email_verified_claim="email_verified",
            name_claim="name",
            preferred_username_claim="nickname",  # Auth0 uses nickname
            groups_claim="https://your-namespace/groups",  # Custom claim with namespace
            picture_claim="picture",
        ),
        documentation_url="https://auth0.com/docs/get-started/apis/scopes/openid-connect-scopes",
        notes="Groups require an Auth0 Action or Rule to add custom claims.",
    ),
    OidcProviderPreset.OKTA: ProviderPresetConfig(
        preset=OidcProviderPreset.OKTA,
        display_name="Okta",
        description="Enterprise identity management",
        default_scopes=("openid", "profile", "email", "groups"),
        claim_mapping=ClaimMapping(
            subject_claim="sub",
            email_claim="email",
            email_verified_claim="email_verified",
            name_claim="name",
            preferred_username_claim="preferred_username",
            groups_claim="groups",
            picture_claim="picture",
        ),
        documentation_url="https://developer.okta.com/docs/reference/api/oidc/",
        notes="Ensure 'groups' scope is enabled for the application in Okta.",
    ),
    OidcProviderPreset.AZURE_AD: ProviderPresetConfig(
        preset=OidcProviderPreset.AZURE_AD,
        display_name="Microsoft Entra ID (Azure AD)",
        description="Microsoft's cloud-based identity service",
        default_scopes=("openid", "profile", "email"),
        claim_mapping=ClaimMapping(
            subject_claim="sub",
            email_claim="email",
            email_verified_claim="email_verified",  # Not standard in Azure
            name_claim="name",
            preferred_username_claim="preferred_username",
            groups_claim="groups",  # Requires optional claim configuration
            picture_claim="picture",
        ),
        documentation_url="https://learn.microsoft.com/en-us/entra/identity-platform/",
        notes="Configure optional claims in App Registration for groups. Use v2.0 endpoint.",
    ),
    OidcProviderPreset.CUSTOM: ProviderPresetConfig(
        preset=OidcProviderPreset.CUSTOM,
        display_name="Custom OIDC Provider",
        description="Any OIDC-compliant identity provider",
        default_scopes=("openid", "profile", "email"),
        claim_mapping=ClaimMapping(),
        documentation_url=None,
        notes="Configure endpoints and claims based on your provider's documentation.",
    ),
}


@dataclass
class OidcProviderRegistry:
    """Registry for managing OIDC provider configurations.

    Handles provider creation, discovery refresh, and validation.
    """

    discovery_client: OidcDiscoveryClient = field(
        default_factory=OidcDiscoveryClient
    )
    _providers: dict[UUID, OidcProviderConfig] = field(default_factory=dict)

    def get_preset_config(self, preset: OidcProviderPreset) -> ProviderPresetConfig:
        """Get the preset configuration for a provider type.

        Args:
            preset: Provider preset type.

        Returns:
            Preset configuration with defaults.
        """
        return PROVIDER_PRESETS.get(preset, PROVIDER_PRESETS[OidcProviderPreset.CUSTOM])

    async def create_provider(
        self,
        workspace_id: UUID,
        name: str,
        issuer_url: str,
        client_id: str,
        params: OidcProviderCreateParams | None = None,
        *,
        auto_discover: bool = True,
    ) -> OidcProviderConfig:
        """Create and configure a new OIDC provider.

        Args:
            workspace_id: Workspace this provider belongs to.
            name: Display name for the provider.
            issuer_url: OIDC issuer URL.
            client_id: OAuth client ID.
            params: Optional creation parameters (preset, scopes, etc.).
            auto_discover: Whether to fetch discovery document.

        Returns:
            Configured provider.

        Raises:
            OidcDiscoveryError: If auto_discover is True and discovery fails.
        """
        p = params or OidcProviderCreateParams()
        preset_config = self.get_preset_config(p.preset)

        # Apply preset defaults where params don't override
        effective_params = OidcProviderCreateParams(
            preset=p.preset,
            scopes=p.scopes or preset_config.default_scopes,
            claim_mapping=p.claim_mapping or preset_config.claim_mapping,
            allowed_groups=p.allowed_groups,
            require_email_verified=p.require_email_verified,
        )

        provider = OidcProviderConfig.create(
            workspace_id=workspace_id,
            name=name,
            issuer_url=issuer_url,
            client_id=client_id,
            params=effective_params,
        )

        if auto_discover:
            await self.refresh_discovery(provider)

        self._providers[provider.id] = provider
        logger.info(
            "Created OIDC provider %s (%s) for workspace %s",
            provider.name,
            provider.preset.value,
            provider.workspace_id,
        )

        return provider

    async def refresh_discovery(self, provider: OidcProviderConfig) -> None:
        """Refresh the discovery configuration for a provider.

        Args:
            provider: Provider to refresh.

        Raises:
            OidcDiscoveryError: If discovery fails.
        """
        await self.discovery_client.discover_and_update(provider)

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

        Args:
            provider: Provider to validate.

        Returns:
            List of warning messages.

        Raises:
            OidcDiscoveryError: If discovery fails.
        """
        return await self.discovery_client.validate_provider(provider)

    def get_provider(self, provider_id: UUID) -> OidcProviderConfig | None:
        """Get a provider by ID.

        Args:
            provider_id: Provider ID.

        Returns:
            Provider configuration or None if not found.
        """
        return self._providers.get(provider_id)

    def list_providers(
        self,
        workspace_id: UUID | None = None,
        enabled_only: bool = False,
    ) -> list[OidcProviderConfig]:
        """List all providers.

        Args:
            workspace_id: Filter by workspace ID.
            enabled_only: Only return enabled providers.

        Returns:
            List of provider configurations.
        """
        providers = list(self._providers.values())

        if workspace_id:
            providers = [p for p in providers if p.workspace_id == workspace_id]

        if enabled_only:
            providers = [p for p in providers if p.enabled]

        return providers

    def remove_provider(self, provider_id: UUID) -> bool:
        """Remove a provider from the registry.

        Args:
            provider_id: Provider ID to remove.

        Returns:
            True if removed, False if not found.
        """
        if provider_id in self._providers:
            provider = self._providers.pop(provider_id)
            logger.info(
                "Removed OIDC provider %s (%s)",
                provider.name,
                provider_id,
            )
            return True
        return False


class OidcAuthService:
    """Service for OIDC authentication operations.

    Orchestrates provider management, discovery, and token handling.
    """

    def __init__(
        self,
        registry: OidcProviderRegistry | None = None,
    ) -> None:
        """Initialize the OIDC auth service.

        Args:
            registry: Provider registry (created if not provided).
        """
        self._registry = registry or OidcProviderRegistry()

    @property
    def registry(self) -> OidcProviderRegistry:
        """Get the provider registry."""
        return self._registry

    async def register_provider(
        self,
        workspace_id: UUID,
        name: str,
        issuer_url: str,
        client_id: str,
        client_secret: str | None = None,
        *,
        preset: OidcProviderPreset = OidcProviderPreset.CUSTOM,
        uow: UnitOfWork | None = None,
    ) -> tuple[OidcProviderConfig, list[str]]:
        """Register a new OIDC provider with validation.

        Args:
            workspace_id: Workspace this provider belongs to.
            name: Display name for the provider.
            issuer_url: OIDC issuer URL.
            client_id: OAuth client ID.
            client_secret: Optional client secret (for confidential clients).
            preset: Provider preset.
            uow: Unit of work for persistence.

        Returns:
            Tuple of (provider config, validation warnings).

        Raises:
            OidcDiscoveryError: If discovery fails.
        """
        provider = await self._registry.create_provider(
            workspace_id=workspace_id,
            name=name,
            issuer_url=issuer_url,
            client_id=client_id,
            params=OidcProviderCreateParams(preset=preset),
        )

        warnings = await self._registry.validate_provider(provider)

        # Store client secret securely if provided
        if client_secret and uow:
            # Would store in IntegrationSecretModel
            logger.info("Client secret provided for provider %s", provider.id)

        return provider, warnings

    async def refresh_all_discovery(
        self,
        workspace_id: UUID | None = None,
    ) -> dict[UUID, str | None]:
        """Refresh discovery for all providers.

        Args:
            workspace_id: Optional workspace filter.

        Returns:
            Dict mapping provider ID to error message (None if success).
        """
        results: dict[UUID, str | None] = {}
        providers = self._registry.list_providers(
            workspace_id=workspace_id,
            enabled_only=True,
        )

        for provider in providers:
            try:
                await self._registry.refresh_discovery(provider)
                results[provider.id] = None
            except OidcDiscoveryError as e:
                logger.warning(
                    "Failed to refresh discovery for %s: %s",
                    provider.name,
                    e,
                )
                results[provider.id] = str(e)

        return results

    def get_preset_options(self) -> list[dict[str, object]]:
        """Get all available provider presets with their configurations.

        Returns:
            List of preset information dictionaries.
        """
        return [
            {
                "preset": config.preset.value,
                "display_name": config.display_name,
                "description": config.description,
                "default_scopes": list(config.default_scopes),
                "documentation_url": config.documentation_url,
                "notes": config.notes,
            }
            for config in PROVIDER_PRESETS.values()
        ]
