"""OIDC provider configuration domain entities.

This module defines the domain entities for OIDC provider configuration,
including provider presets for common identity providers like Authentik,
Authelia, and Keycloak.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from datetime import datetime
from enum import StrEnum
from typing import Self, cast
from uuid import UUID, uuid4

from noteflow.domain.utils.time import utc_now


def _tuple_from_list(value: object) -> tuple[str, ...]:
    if isinstance(value, list):
        items = cast(list[object], value)
        return tuple(str(item) for item in items)
    return ()


def _tuple_from_list_or_default(
    value: object,
    default: tuple[str, ...],
) -> tuple[str, ...]:
    if isinstance(value, list):
        items = cast(list[object], value)
        return tuple(str(item) for item in items)
    return default


class OidcProviderPreset(StrEnum):
    """Preset configurations for common OIDC providers."""

    AUTHENTIK = "authentik"
    AUTHELIA = "authelia"
    KEYCLOAK = "keycloak"
    AUTH0 = "auth0"
    OKTA = "okta"
    AZURE_AD = "azure_ad"
    CUSTOM = "custom"


@dataclass(frozen=True, slots=True)
class ClaimMapping:
    """Map OIDC claims to NoteFlow user attributes.

    OIDC providers may use different claim names for user attributes.
    This mapping allows configuring which claims to use for each attribute.
    """

    # Standard OIDC claims with sensible defaults
    subject_claim: str = "sub"
    email_claim: str = "email"
    email_verified_claim: str = "email_verified"
    name_claim: str = "name"
    preferred_username_claim: str = "preferred_username"
    groups_claim: str = "groups"
    picture_claim: str = "picture"

    # Optional custom claims
    first_name_claim: str | None = None
    last_name_claim: str | None = None
    phone_claim: str | None = None

    def to_dict(self) -> dict[str, str | None]:
        """Convert to dictionary for serialization."""
        return {
            "subject_claim": self.subject_claim,
            "email_claim": self.email_claim,
            "email_verified_claim": self.email_verified_claim,
            "name_claim": self.name_claim,
            "preferred_username_claim": self.preferred_username_claim,
            "groups_claim": self.groups_claim,
            "picture_claim": self.picture_claim,
            "first_name_claim": self.first_name_claim,
            "last_name_claim": self.last_name_claim,
            "phone_claim": self.phone_claim,
        }

    @classmethod
    def from_dict(cls, data: dict[str, str | None]) -> Self:
        """Create from dictionary."""
        return cls(
            subject_claim=data.get("subject_claim") or "sub",
            email_claim=data.get("email_claim") or "email",
            email_verified_claim=data.get("email_verified_claim") or "email_verified",
            name_claim=data.get("name_claim") or "name",
            preferred_username_claim=data.get("preferred_username_claim") or "preferred_username",
            groups_claim=data.get("groups_claim") or "groups",
            picture_claim=data.get("picture_claim") or "picture",
            first_name_claim=data.get("first_name_claim"),
            last_name_claim=data.get("last_name_claim"),
            phone_claim=data.get("phone_claim"),
        )


@dataclass(frozen=True, slots=True)
class OidcDiscoveryConfig:
    """OIDC discovery document fields.

    These fields are populated from the provider's
    `.well-known/openid-configuration` endpoint.
    """

    issuer: str
    authorization_endpoint: str
    token_endpoint: str
    userinfo_endpoint: str | None = None
    jwks_uri: str | None = None
    end_session_endpoint: str | None = None
    revocation_endpoint: str | None = None
    introspection_endpoint: str | None = None
    scopes_supported: tuple[str, ...] = field(default_factory=tuple)
    response_types_supported: tuple[str, ...] = field(default_factory=tuple)
    grant_types_supported: tuple[str, ...] = field(default_factory=tuple)
    claims_supported: tuple[str, ...] = field(default_factory=tuple)
    code_challenge_methods_supported: tuple[str, ...] = field(default_factory=tuple)

    def to_dict(self) -> dict[str, object]:
        """Convert to dictionary for serialization."""
        return {
            "issuer": self.issuer,
            "authorization_endpoint": self.authorization_endpoint,
            "token_endpoint": self.token_endpoint,
            "userinfo_endpoint": self.userinfo_endpoint,
            "jwks_uri": self.jwks_uri,
            "end_session_endpoint": self.end_session_endpoint,
            "revocation_endpoint": self.revocation_endpoint,
            "introspection_endpoint": self.introspection_endpoint,
            "scopes_supported": list(self.scopes_supported),
            "response_types_supported": list(self.response_types_supported),
            "grant_types_supported": list(self.grant_types_supported),
            "claims_supported": list(self.claims_supported),
            "code_challenge_methods_supported": list(self.code_challenge_methods_supported),
        }

    @classmethod
    def from_dict(cls, data: dict[str, object]) -> Self:
        """Create from dictionary (e.g., discovery document)."""
        scopes = data.get("scopes_supported")
        response_types = data.get("response_types_supported")
        grant_types = data.get("grant_types_supported")
        claims = data.get("claims_supported")
        code_challenge = data.get("code_challenge_methods_supported")

        return cls(
            issuer=str(data.get("issuer", "")),
            authorization_endpoint=str(data.get("authorization_endpoint", "")),
            token_endpoint=str(data.get("token_endpoint", "")),
            userinfo_endpoint=str(data["userinfo_endpoint"]) if data.get("userinfo_endpoint") else None,
            jwks_uri=str(data["jwks_uri"]) if data.get("jwks_uri") else None,
            end_session_endpoint=str(data["end_session_endpoint"]) if data.get("end_session_endpoint") else None,
            revocation_endpoint=str(data["revocation_endpoint"]) if data.get("revocation_endpoint") else None,
            introspection_endpoint=str(data["introspection_endpoint"]) if data.get("introspection_endpoint") else None,
            scopes_supported=_tuple_from_list(scopes),
            response_types_supported=_tuple_from_list(response_types),
            grant_types_supported=_tuple_from_list(grant_types),
            claims_supported=_tuple_from_list(claims),
            code_challenge_methods_supported=_tuple_from_list(code_challenge),
        )

    def supports_pkce(self) -> bool:
        """Check if provider supports PKCE with S256."""
        return "S256" in self.code_challenge_methods_supported


@dataclass(frozen=True, slots=True)
class OidcProviderCreateParams:
    """Parameters for creating an OIDC provider configuration.

    Groups optional creation parameters to reduce parameter count.
    """

    preset: OidcProviderPreset = OidcProviderPreset.CUSTOM
    """Provider preset for defaults."""

    scopes: tuple[str, ...] | None = None
    """OAuth scopes to request (defaults to OIDC standard)."""

    claim_mapping: ClaimMapping | None = None
    """Custom claim mapping."""

    allowed_groups: tuple[str, ...] | None = None
    """Groups allowed to authenticate."""

    require_email_verified: bool = True
    """Whether to require email verification."""


@dataclass
class OidcProviderConfig:
    """OIDC provider configuration.

    Represents a configured OIDC provider that can be used for SSO authentication.
    The discovery field is populated from the provider's discovery endpoint.
    """

    id: UUID
    workspace_id: UUID
    name: str
    preset: OidcProviderPreset
    issuer_url: str
    client_id: str
    enabled: bool = True

    # Discovery configuration (populated from .well-known/openid-configuration)
    discovery: OidcDiscoveryConfig | None = None

    # Claim mapping configuration
    claim_mapping: ClaimMapping = field(default_factory=ClaimMapping)

    # OAuth scopes to request (defaults to OIDC standard)
    scopes: tuple[str, ...] = field(default_factory=lambda: ("openid", "profile", "email"))

    # Whether to require email verification
    require_email_verified: bool = True

    # Optional group-based access control
    allowed_groups: tuple[str, ...] = field(default_factory=tuple)

    # Timestamps
    created_at: datetime = field(default_factory=utc_now)
    updated_at: datetime = field(default_factory=utc_now)
    discovery_refreshed_at: datetime | None = None

    @classmethod
    def create(
        cls,
        workspace_id: UUID,
        name: str,
        issuer_url: str,
        client_id: str,
        params: OidcProviderCreateParams | None = None,
    ) -> OidcProviderConfig:
        """Create a new OIDC provider configuration.

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

        Returns:
            New OidcProviderConfig instance.
        """
        p = params or OidcProviderCreateParams()
        now = utc_now()
        return cls(
            id=uuid4(),
            workspace_id=workspace_id,
            name=name,
            preset=p.preset,
            issuer_url=issuer_url.rstrip("/"),
            client_id=client_id,
            scopes=p.scopes or ("openid", "profile", "email"),
            claim_mapping=p.claim_mapping or ClaimMapping(),
            allowed_groups=p.allowed_groups or (),
            require_email_verified=p.require_email_verified,
            created_at=now,
            updated_at=now,
        )

    @property
    def discovery_url(self) -> str:
        """Get the OIDC discovery URL."""
        return f"{self.issuer_url}/.well-known/openid-configuration"

    def update_discovery(self, discovery: OidcDiscoveryConfig) -> None:
        """Update the discovery configuration.

        Args:
            discovery: New discovery configuration from provider.
        """
        object.__setattr__(self, "discovery", discovery)
        object.__setattr__(self, "discovery_refreshed_at", utc_now())
        object.__setattr__(self, "updated_at", utc_now())

    def disable(self) -> None:
        """Disable this provider."""
        object.__setattr__(self, "enabled", False)
        object.__setattr__(self, "updated_at", utc_now())

    def enable(self) -> None:
        """Enable this provider."""
        object.__setattr__(self, "enabled", True)
        object.__setattr__(self, "updated_at", utc_now())

    def to_dict(self) -> dict[str, object]:
        """Convert to dictionary for serialization."""
        return {
            "id": str(self.id),
            "workspace_id": str(self.workspace_id),
            "name": self.name,
            "preset": self.preset.value,
            "issuer_url": self.issuer_url,
            "client_id": self.client_id,
            "enabled": self.enabled,
            "discovery": self.discovery.to_dict() if self.discovery else None,
            "claim_mapping": self.claim_mapping.to_dict(),
            "scopes": list(self.scopes),
            "require_email_verified": self.require_email_verified,
            "allowed_groups": list(self.allowed_groups),
            "created_at": self.created_at.isoformat(),
            "updated_at": self.updated_at.isoformat(),
            "discovery_refreshed_at": self.discovery_refreshed_at.isoformat() if self.discovery_refreshed_at else None,
        }

    @classmethod
    def from_dict(cls, data: dict[str, object]) -> OidcProviderConfig:
        """Create from dictionary."""
        discovery_data = data.get("discovery")
        claim_mapping_data = data.get("claim_mapping")
        scopes_data = data.get("scopes")
        allowed_groups_data = data.get("allowed_groups")
        created_at_str = data.get("created_at")
        updated_at_str = data.get("updated_at")
        discovery_refreshed_str = data.get("discovery_refreshed_at")

        return cls(
            id=UUID(str(data["id"])),
            workspace_id=UUID(str(data["workspace_id"])),
            name=str(data["name"]),
            preset=OidcProviderPreset(str(data["preset"])),
            issuer_url=str(data["issuer_url"]),
            client_id=str(data["client_id"]),
            enabled=bool(data.get("enabled", True)),
            discovery=(
                OidcDiscoveryConfig.from_dict(cast(dict[str, object], discovery_data))
                if isinstance(discovery_data, dict)
                else None
            ),
            claim_mapping=(
                ClaimMapping.from_dict(cast(dict[str, str | None], claim_mapping_data))
                if isinstance(claim_mapping_data, dict)
                else ClaimMapping()
            ),
            scopes=_tuple_from_list_or_default(
                scopes_data,
                ("openid", "profile", "email"),
            ),
            require_email_verified=bool(data.get("require_email_verified", True)),
            allowed_groups=_tuple_from_list(allowed_groups_data),
            created_at=datetime.fromisoformat(str(created_at_str)) if created_at_str else utc_now(),
            updated_at=datetime.fromisoformat(str(updated_at_str)) if updated_at_str else utc_now(),
            discovery_refreshed_at=datetime.fromisoformat(str(discovery_refreshed_str)) if discovery_refreshed_str else None,
        )
