from __future__ import annotations

from collections.abc import Mapping, Sequence
from dataclasses import dataclass, field
from datetime import datetime
from typing import Literal, Protocol, runtime_checkable


CAPABILITY_DISTRIBUTION_LIST_SOURCE = "dist_lists.source"
CAPABILITY_DISTRIBUTION_LIST_EXPAND = "dist_lists.expand"
CAPABILITY_DISTRIBUTION_LIST_WRITER = "dist_lists.writer"
CAPABILITY_RECIPIENT_CHANNEL_FACTS = "addresses.channel_facts"
CAPABILITY_POLICY_DISTRIBUTION_CHANNELS = "policy.distribution_channels"

DistributionDefinitionKind = Literal["static", "parameterized", "dynamic", "template"]
DistributionEntryMode = Literal["include", "exclude", "override"]
DistributionEntryKind = Literal[
    "address_contact",
    "address_list",
    "address_email",
    "raw_email",
    "raw_postal_address",
    "internal_mail",
    "portal",
    "idm_identity",
    "idm_group",
    "organization_unit",
    "function",
    "effective_function_incumbent",
    "dataflow_result",
    "distribution_list",
]
DistributionChannel = Literal["email", "postal", "internal_mail", "portal"]
DistributionOutcome = Literal[
    "usable",
    "unresolved",
    "invalid",
    "suppressed",
    "ambiguous",
    "duplicate",
    "policy_blocked",
    "provider_unavailable",
    "stale",
]
DistributionExplanationSeverity = Literal["info", "warning", "error"]


class DistributionListError(ValueError):
    """Stable base error for provider-neutral distribution-list operations."""


class DistributionListNotFoundError(DistributionListError):
    pass


class DistributionListConflictError(DistributionListError):
    pass


class DistributionListUnavailableError(DistributionListError):
    pass


@dataclass(frozen=True, slots=True)
class DistributionSourceReference:
    provider: str
    resource_type: str
    resource_id: str
    revision: str | None = None
    fingerprint: str | None = None
    label: str | None = None
    metadata: Mapping[str, object] = field(default_factory=dict)


@dataclass(frozen=True, slots=True)
class DistributionExplanation:
    code: str
    message: str
    severity: DistributionExplanationSeverity = "warning"
    provider: str | None = None
    source: DistributionSourceReference | None = None
    provenance: Mapping[str, object] = field(default_factory=dict)


@dataclass(frozen=True, slots=True)
class DistributionParameterDefinition:
    key: str
    value_type: Literal[
        "string",
        "integer",
        "number",
        "boolean",
        "date",
        "datetime",
        "string_list",
    ]
    label: str | None = None
    required: bool = False
    default: object | None = None
    allowed_values: tuple[object, ...] = ()
    minimum: float | None = None
    maximum: float | None = None
    pattern: str | None = None
    description: str | None = None


@dataclass(frozen=True, slots=True)
class DistributionListEntryRef:
    id: str
    kind: DistributionEntryKind
    mode: DistributionEntryMode
    source: DistributionSourceReference
    label: str | None = None
    purpose: str | None = None
    requested_channels: tuple[DistributionChannel, ...] = ()
    effective_from: datetime | None = None
    effective_until: datetime | None = None
    order: int = 0
    configuration: Mapping[str, object] = field(default_factory=dict)


@dataclass(frozen=True, slots=True)
class DistributionListSourceRef:
    id: str
    tenant_id: str
    name: str
    revision_id: str
    revision: int
    definition_hash: str
    definition_kind: DistributionDefinitionKind = "static"
    description: str | None = None
    status: str = "active"
    entry_count: int = 0
    read_only: bool = False
    stale: bool = False
    parameters: tuple[DistributionParameterDefinition, ...] = ()
    updated_at: datetime | None = None
    provenance: Mapping[str, object] = field(default_factory=dict)
    metadata: Mapping[str, object] = field(default_factory=dict)


@dataclass(frozen=True, slots=True)
class DistributionExpansionLimits:
    max_entries: int = 500
    max_results: int = 5_000
    max_depth: int = 8
    max_provider_results: int = 2_000


@dataclass(frozen=True, slots=True)
class DistributionExpansionRequest:
    list_id: str
    revision: int | None = None
    effective_at: datetime | None = None
    purpose: str | None = None
    requested_channels: tuple[DistributionChannel, ...] = ()
    parameters: Mapping[str, object] = field(default_factory=dict)
    preview: bool = False
    freeze: bool = False
    idempotency_key: str | None = None
    limits: DistributionExpansionLimits = field(
        default_factory=DistributionExpansionLimits
    )


@dataclass(frozen=True, slots=True)
class DistributionChannelCandidate:
    channel: DistributionChannel
    target: str
    target_key: str
    status: DistributionOutcome = "usable"
    contact_point_id: str | None = None
    locale: str | None = None
    preferred: bool = False
    reason_code: str | None = None
    explanation: str | None = None
    source: DistributionSourceReference | None = None
    decision_provenance: Mapping[str, object] = field(default_factory=dict)


@dataclass(frozen=True, slots=True)
class DistributionRecipientRef:
    recipient_key: str
    display_name: str
    status: DistributionOutcome
    channels: tuple[DistributionChannelCandidate, ...] = ()
    identity_id: str | None = None
    account_id: str | None = None
    contact_id: str | None = None
    organization_unit_id: str | None = None
    function_id: str | None = None
    source_entry_ids: tuple[str, ...] = ()
    explanations: tuple[DistributionExplanation, ...] = ()
    attributes: Mapping[str, object] = field(default_factory=dict)
    provenance: Mapping[str, object] = field(default_factory=dict)


@dataclass(frozen=True, slots=True)
class DistributionProviderEvidence:
    provider: str
    source: DistributionSourceReference
    actual_revision: str | None = None
    actual_fingerprint: str | None = None
    stale: bool = False
    generated_at: datetime | None = None
    details: Mapping[str, object] = field(default_factory=dict)


@dataclass(frozen=True, slots=True)
class DistributionExpansionResult:
    source: DistributionListSourceRef
    request: DistributionExpansionRequest
    recipients: tuple[DistributionRecipientRef, ...]
    excluded: tuple[DistributionRecipientRef, ...] = ()
    diagnostics: tuple[DistributionExplanation, ...] = ()
    provider_evidence: tuple[DistributionProviderEvidence, ...] = ()
    expansion_hash: str = ""
    generated_at: datetime | None = None
    snapshot_id: str | None = None
    stale: bool = False
    truncated: bool = False


@dataclass(frozen=True, slots=True)
class DistributionSnapshotRef:
    id: str
    tenant_id: str
    list_id: str
    revision_id: str
    revision: int
    expansion_hash: str
    generated_at: datetime
    effective_at: datetime
    recipient_count: int
    excluded_count: int
    stale: bool
    truncated: bool
    request: Mapping[str, object] = field(default_factory=dict)
    recipients: tuple[DistributionRecipientRef, ...] = ()
    excluded: tuple[DistributionRecipientRef, ...] = ()
    diagnostics: tuple[DistributionExplanation, ...] = ()
    provider_evidence: tuple[DistributionProviderEvidence, ...] = ()
    provenance: Mapping[str, object] = field(default_factory=dict)


@dataclass(frozen=True, slots=True)
class DistributionWriteDecision:
    list_id: str | None
    operation: str
    allowed: bool
    reason_code: str
    explanation: str
    read_only: bool = False
    required_scopes: tuple[str, ...] = ()
    provenance: Mapping[str, object] = field(default_factory=dict)


@dataclass(frozen=True, slots=True)
class RecipientChannelFactsRequest:
    tenant_id: str
    source: DistributionSourceReference
    recipient_key: str
    effective_at: datetime
    purpose: str | None = None
    requested_channels: tuple[DistributionChannel, ...] = ()
    context: Mapping[str, object] = field(default_factory=dict)


@dataclass(frozen=True, slots=True)
class RecipientChannelFacts:
    candidates: tuple[DistributionChannelCandidate, ...]
    explanations: tuple[DistributionExplanation, ...] = ()
    source_revision: str | None = None
    source_fingerprint: str | None = None
    provenance: Mapping[str, object] = field(default_factory=dict)


@dataclass(frozen=True, slots=True)
class DistributionChannelPolicyRequest:
    tenant_id: str
    list_id: str
    purpose: str | None
    effective_at: datetime
    recipient: DistributionRecipientRef
    candidate: DistributionChannelCandidate
    context: Mapping[str, object] = field(default_factory=dict)


@dataclass(frozen=True, slots=True)
class DistributionChannelPolicyDecision:
    allowed: bool
    reason_code: str
    explanation: str
    source_path: tuple[Mapping[str, object], ...] = ()
    requirements: tuple[str, ...] = ()
    details: Mapping[str, object] = field(default_factory=dict)


@runtime_checkable
class DistributionListSourceProvider(Protocol):
    def list_sources(
        self,
        session: object,
        principal: object,
        *,
        query: str = "",
        limit: int = 100,
    ) -> Sequence[DistributionListSourceRef]: ...

    def get_source(
        self,
        session: object,
        principal: object,
        *,
        list_id: str,
        revision: int | None = None,
    ) -> DistributionListSourceRef | None: ...


@runtime_checkable
class DistributionListExpansionProvider(Protocol):
    def expand(
        self,
        session: object,
        principal: object,
        *,
        request: DistributionExpansionRequest,
    ) -> DistributionExpansionResult: ...

    def get_snapshot(
        self,
        session: object,
        principal: object,
        *,
        snapshot_id: str,
    ) -> DistributionSnapshotRef | None: ...


@runtime_checkable
class DistributionListWriter(Protocol):
    def explain_write(
        self,
        session: object,
        principal: object,
        *,
        list_id: str | None,
        operation: str,
    ) -> DistributionWriteDecision: ...


@runtime_checkable
class RecipientChannelFactsProvider(Protocol):
    def resolve_channel_facts(
        self,
        session: object,
        principal: object,
        *,
        request: RecipientChannelFactsRequest,
    ) -> RecipientChannelFacts: ...


@runtime_checkable
class DistributionChannelPolicyProvider(Protocol):
    def resolve_distribution_channel(
        self,
        session: object,
        principal: object,
        *,
        request: DistributionChannelPolicyRequest,
    ) -> DistributionChannelPolicyDecision: ...


def distribution_list_source_provider(
    registry: object | None,
) -> DistributionListSourceProvider | None:
    capability = _capability(registry, CAPABILITY_DISTRIBUTION_LIST_SOURCE)
    return capability if isinstance(capability, DistributionListSourceProvider) else None


def distribution_list_expansion_provider(
    registry: object | None,
) -> DistributionListExpansionProvider | None:
    capability = _capability(registry, CAPABILITY_DISTRIBUTION_LIST_EXPAND)
    return (
        capability
        if isinstance(capability, DistributionListExpansionProvider)
        else None
    )


def _capability(registry: object | None, name: str) -> object | None:
    if (
        registry is None
        or not hasattr(registry, "has_capability")
        or not hasattr(registry, "capability")
        or not registry.has_capability(name)
    ):
        return None
    return registry.capability(name)


__all__ = [name for name in globals() if name.startswith("CAPABILITY_") or name.startswith("Distribution") or name.startswith("Recipient") or name.startswith("distribution_")]
