"""OIDC provider management mixin for gRPC service."""

from __future__ import annotations

from collections.abc import Sequence
from typing import Protocol, cast
from uuid import UUID

from noteflow.config.constants import ERROR_INVALID_WORKSPACE_ID_FORMAT
from noteflow.domain.auth.oidc import ClaimMapping, OidcProviderConfig, OidcProviderPreset
from noteflow.infrastructure.auth.oidc_discovery import OidcDiscoveryError
from noteflow.infrastructure.auth.oidc_registry import (
    PROVIDER_PRESETS,
    OidcAuthService,
)

from ..proto import noteflow_pb2
from ._types import GrpcContext
from .converters import oidc_provider_to_proto, proto_to_claim_mapping
from .errors import abort_invalid_argument, abort_not_found, parse_workspace_id


class OidcServicer(Protocol):
    """Protocol for hosts that support OIDC operations."""

    oidc_service: OidcAuthService | None

    def get_oidc_service(self) -> OidcAuthService: ...


class _HasField(Protocol):
    def HasField(self, field_name: str) -> bool: ...

# Error message constants
_ENTITY_OIDC_PROVIDER = "OIDC Provider"
_ERR_INVALID_PROVIDER_ID = "Invalid provider_id format"
_ERR_INVALID_PRESET = "Invalid preset value"


def _parse_provider_id(provider_id_str: str) -> UUID:
    """Parse provider ID string to UUID, raising ValueError if invalid."""
    return UUID(provider_id_str)


def _parse_preset(preset_str: str) -> OidcProviderPreset:
    """Parse preset string to OidcProviderPreset enum."""
    return OidcProviderPreset(preset_str.lower())


async def _validate_register_request(
    request: noteflow_pb2.RegisterOidcProviderRequest,
    context: GrpcContext,
) -> None:
    """Validate required fields in RegisterOidcProvider request."""
    if not request.name:
        await abort_invalid_argument(context, "name is required")

    if not request.issuer_url:
        await abort_invalid_argument(context, "issuer_url is required")

    if not request.issuer_url.startswith(("http://", "https://")):
        await abort_invalid_argument(
            context, "issuer_url must start with http:// or https://"
        )

    if not request.client_id:
        await abort_invalid_argument(context, "client_id is required")


def _parse_register_options(
    request: noteflow_pb2.RegisterOidcProviderRequest,
) -> tuple[ClaimMapping | None, tuple[str, ...] | None, tuple[str, ...] | None]:
    """Parse optional fields from RegisterOidcProvider request.

    Returns (claim_mapping, scopes, allowed_groups) tuple.
    """
    claim_mapping: ClaimMapping | None = None
    if cast(_HasField, request).HasField("claim_mapping"):
        claim_mapping = proto_to_claim_mapping(request.claim_mapping)

    scopes_values = cast(Sequence[str], request.scopes)
    scopes = tuple(scopes_values) if scopes_values else None
    allowed_groups: tuple[str, ...] | None = None
    if allowed_values := cast(Sequence[str], request.allowed_groups):
        allowed_groups = tuple(allowed_values)

    return claim_mapping, scopes, allowed_groups


def _apply_custom_provider_config(
    provider: OidcProviderConfig,
    claim_mapping: ClaimMapping | None,
    scopes: tuple[str, ...] | None,
    allowed_groups: tuple[str, ...] | None,
    require_email_verified: bool | None,
) -> None:
    """Apply custom configuration options to a registered provider."""
    if claim_mapping:
        object.__setattr__(provider, "claim_mapping", claim_mapping)
    if scopes:
        object.__setattr__(provider, "scopes", scopes)
    if allowed_groups:
        object.__setattr__(provider, "allowed_groups", allowed_groups)
    if require_email_verified is not None:
        object.__setattr__(provider, "require_email_verified", require_email_verified)


class OidcMixin:
    """Mixin providing OIDC provider management operations.

    Requires host to implement OidcServicer protocol.
    OIDC providers are stored in the in-memory registry (not database).
    """

    oidc_service: OidcAuthService | None

    def get_oidc_service(self: OidcServicer) -> OidcAuthService:
        """Get or create the OIDC auth service."""
        if self.oidc_service is None:
            self.oidc_service = OidcAuthService()
        assert self.oidc_service is not None  # Help type checker
        return self.oidc_service

    async def RegisterOidcProvider(
        self: OidcServicer,
        request: noteflow_pb2.RegisterOidcProviderRequest,
        context: GrpcContext,
    ) -> noteflow_pb2.OidcProviderProto:
        """Register a new OIDC provider."""
        await _validate_register_request(request, context)

        # Parse preset
        try:
            preset = _parse_preset(request.preset) if request.preset else OidcProviderPreset.CUSTOM
        except ValueError:
            await abort_invalid_argument(context, _ERR_INVALID_PRESET)
            return noteflow_pb2.OidcProviderProto()  # unreachable

        # Parse workspace ID
        try:
            workspace_id = UUID(request.workspace_id) if request.workspace_id else UUID(int=0)
        except ValueError:
            await abort_invalid_argument(context, ERROR_INVALID_WORKSPACE_ID_FORMAT)
            return noteflow_pb2.OidcProviderProto()  # unreachable

        claim_mapping, scopes, allowed_groups = _parse_register_options(request)

        # Register provider
        oidc_service = self.get_oidc_service()
        try:
            provider, warnings = await oidc_service.register_provider(
                workspace_id=workspace_id,
                name=request.name,
                issuer_url=request.issuer_url,
                client_id=request.client_id,
                client_secret=(
                    request.client_secret
                    if cast(_HasField, request).HasField("client_secret")
                    else None
                ),
                preset=preset,
            )

            _apply_custom_provider_config(
                provider,
                claim_mapping,
                scopes,
                allowed_groups,
                (
                    request.require_email_verified
                    if cast(_HasField, request).HasField("require_email_verified")
                    else None
                ),
            )

            return oidc_provider_to_proto(provider, warnings)

        except OidcDiscoveryError as e:
            await abort_invalid_argument(context, f"OIDC discovery failed: {e}")
            return noteflow_pb2.OidcProviderProto()  # unreachable

    async def ListOidcProviders(
        self: OidcServicer,
        request: noteflow_pb2.ListOidcProvidersRequest,
        context: GrpcContext,
    ) -> noteflow_pb2.ListOidcProvidersResponse:
        """List all OIDC providers."""
        # Parse optional workspace filter
        workspace_id: UUID | None = None
        if cast(_HasField, request).HasField("workspace_id"):
            workspace_id = await parse_workspace_id(request.workspace_id, context)

        oidc_service = self.get_oidc_service()
        providers = oidc_service.registry.list_providers(
            workspace_id=workspace_id,
            enabled_only=request.enabled_only,
        )

        return noteflow_pb2.ListOidcProvidersResponse(
            providers=[oidc_provider_to_proto(p) for p in providers],
            total_count=len(providers),
        )

    async def GetOidcProvider(
        self: OidcServicer,
        request: noteflow_pb2.GetOidcProviderRequest,
        context: GrpcContext,
    ) -> noteflow_pb2.OidcProviderProto:
        """Get a specific OIDC provider by ID."""
        try:
            provider_id = _parse_provider_id(request.provider_id)
        except ValueError:
            await abort_invalid_argument(context, _ERR_INVALID_PROVIDER_ID)
            return noteflow_pb2.OidcProviderProto()  # unreachable

        oidc_service = self.get_oidc_service()
        provider = oidc_service.registry.get_provider(provider_id)

        if provider is None:
            await abort_not_found(context, _ENTITY_OIDC_PROVIDER, str(provider_id))
            return noteflow_pb2.OidcProviderProto()  # unreachable

        return oidc_provider_to_proto(provider)

    async def UpdateOidcProvider(
        self: OidcServicer,
        request: noteflow_pb2.UpdateOidcProviderRequest,
        context: GrpcContext,
    ) -> noteflow_pb2.OidcProviderProto:
        """Update an existing OIDC provider."""
        try:
            provider_id = _parse_provider_id(request.provider_id)
        except ValueError:
            await abort_invalid_argument(context, _ERR_INVALID_PROVIDER_ID)
            return noteflow_pb2.OidcProviderProto()  # unreachable

        oidc_service = self.get_oidc_service()
        provider = oidc_service.registry.get_provider(provider_id)

        if provider is None:
            await abort_not_found(context, _ENTITY_OIDC_PROVIDER, str(provider_id))
            return noteflow_pb2.OidcProviderProto()  # unreachable

        # Apply updates
        if cast(_HasField, request).HasField("name"):
            object.__setattr__(provider, "name", request.name)

        if scopes_values := cast(Sequence[str], request.scopes):
            object.__setattr__(provider, "scopes", tuple(scopes_values))

        if cast(_HasField, request).HasField("claim_mapping"):
            object.__setattr__(provider, "claim_mapping", proto_to_claim_mapping(request.claim_mapping))

        if allowed_values := cast(Sequence[str], request.allowed_groups):
            object.__setattr__(provider, "allowed_groups", tuple(allowed_values))

        if cast(_HasField, request).HasField("require_email_verified"):
            object.__setattr__(provider, "require_email_verified", request.require_email_verified)

        if cast(_HasField, request).HasField("enabled"):
            if request.enabled:
                provider.enable()
            else:
                provider.disable()

        return oidc_provider_to_proto(provider)

    async def DeleteOidcProvider(
        self: OidcServicer,
        request: noteflow_pb2.DeleteOidcProviderRequest,
        context: GrpcContext,
    ) -> noteflow_pb2.DeleteOidcProviderResponse:
        """Delete an OIDC provider."""
        try:
            provider_id = _parse_provider_id(request.provider_id)
        except ValueError:
            await abort_invalid_argument(context, _ERR_INVALID_PROVIDER_ID)
            return noteflow_pb2.DeleteOidcProviderResponse(success=False)

        oidc_service = self.get_oidc_service()
        success = oidc_service.registry.remove_provider(provider_id)

        if not success:
            await abort_not_found(context, _ENTITY_OIDC_PROVIDER, str(provider_id))

        return noteflow_pb2.DeleteOidcProviderResponse(success=success)

    async def RefreshOidcDiscovery(
        self: OidcServicer,
        request: noteflow_pb2.RefreshOidcDiscoveryRequest,
        context: GrpcContext,
    ) -> noteflow_pb2.RefreshOidcDiscoveryResponse:
        """Refresh OIDC discovery for one or all providers."""
        oidc_service = self.get_oidc_service()

        # Single provider refresh
        if cast(_HasField, request).HasField("provider_id"):
            try:
                provider_id = _parse_provider_id(request.provider_id)
            except ValueError:
                await abort_invalid_argument(context, _ERR_INVALID_PROVIDER_ID)
                return noteflow_pb2.RefreshOidcDiscoveryResponse()

            provider = oidc_service.registry.get_provider(provider_id)
            if provider is None:
                await abort_not_found(context, _ENTITY_OIDC_PROVIDER, str(provider_id))
                return noteflow_pb2.RefreshOidcDiscoveryResponse()

            try:
                await oidc_service.registry.refresh_discovery(provider)
                return noteflow_pb2.RefreshOidcDiscoveryResponse(
                    results={str(provider_id): ""},
                    success_count=1,
                    failure_count=0,
                )
            except OidcDiscoveryError as e:
                return noteflow_pb2.RefreshOidcDiscoveryResponse(
                    results={str(provider_id): str(e)},
                    success_count=0,
                    failure_count=1,
                )

        # Bulk refresh
        workspace_id: UUID | None = None
        if cast(_HasField, request).HasField("workspace_id"):
            workspace_id = await parse_workspace_id(request.workspace_id, context)

        results = await oidc_service.refresh_all_discovery(workspace_id=workspace_id)

        # Convert UUID keys to strings and count results
        results_str = {str(k): v or "" for k, v in results.items()}
        success_count = sum(v is None for v in results.values())
        failure_count = sum(v is not None for v in results.values())

        return noteflow_pb2.RefreshOidcDiscoveryResponse(
            results=results_str,
            success_count=success_count,
            failure_count=failure_count,
        )

    async def ListOidcPresets(
        self: OidcServicer,
        request: noteflow_pb2.ListOidcPresetsRequest,
        context: GrpcContext,
    ) -> noteflow_pb2.ListOidcPresetsResponse:
        """List available OIDC provider presets."""
        presets = [
            noteflow_pb2.OidcPresetProto(
                preset=config.preset.value,
                display_name=config.display_name,
                description=config.description,
                default_scopes=list(config.default_scopes),
                documentation_url=config.documentation_url or "",
                notes=config.notes or "",
            )
            for config in PROVIDER_PRESETS.values()
        ]

        return noteflow_pb2.ListOidcPresetsResponse(presets=presets)
