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

from govoplan_core.core.events import EventClassification


POSTBOX_MODULE_ID = "postbox"
CAPABILITY_POSTBOX_DIRECTORY = "postbox.directory"
CAPABILITY_POSTBOX_ACCESS = "postbox.access"
CAPABILITY_POSTBOX_MESSAGES = "postbox.messages"
CAPABILITY_POSTBOX_DELIVERY = "postbox.delivery"
CAPABILITY_POSTBOX_EVIDENCE = "postbox.evidence"
CAPABILITY_POSTBOX_ROUTING = "postbox.routing"

PostboxAction = Literal[
    "discover",
    "read",
    "send",
    "reply",
    "acknowledge",
    "administer",
]
PostboxMessageListState = Literal["all", "unread", "read", "acknowledged"]
PostboxMessageAvailability = Literal["available", "withdrawn", "expired"]
PostboxBindingStatus = Literal[
    "active",
    "missing",
    "not_effective",
    "unit_missing",
    "unit_inactive",
    "unit_tenant_mismatch",
    "function_missing",
    "function_inactive",
    "function_tenant_mismatch",
    "function_reassigned",
    "directory_unavailable",
]

POSTBOX_CLASSIFICATIONS: tuple[EventClassification, ...] = (
    "public",
    "internal",
    "confidential",
    "restricted",
)
_POSTBOX_CLASSIFICATION_RANK = {
    value: rank for rank, value in enumerate(POSTBOX_CLASSIFICATIONS)
}


def normalize_postbox_classification(
    value: str,
) -> EventClassification | None:
    candidate = value.strip().casefold()
    if candidate not in _POSTBOX_CLASSIFICATION_RANK:
        return None
    return candidate  # type: ignore[return-value]


def postbox_classification_allows(
    ceiling: str,
    content: str,
) -> bool:
    normalized_ceiling = normalize_postbox_classification(ceiling)
    normalized_content = normalize_postbox_classification(content)
    if normalized_ceiling is None or normalized_content is None:
        return False
    return (
        _POSTBOX_CLASSIFICATION_RANK[normalized_content]
        <= _POSTBOX_CLASSIFICATION_RANK[normalized_ceiling]
    )


@dataclass(frozen=True, slots=True)
class PostboxActorRef:
    account_id: str
    identity_id: str | None = None
    selected_assignment_id: str | None = None
    acting_for_account_id: str | None = None
    authorized_actions: frozenset[PostboxAction] = frozenset()
    authorized_classifications: frozenset[EventClassification] = frozenset(
        {"public", "internal"}
    )


@dataclass(frozen=True, slots=True)
class PostboxTargetRef:
    postbox_id: str | None = None
    address_key: str | None = None
    template_id: str | None = None
    organization_unit_id: str | None = None
    function_id: str | None = None
    context_key: str | None = None


@dataclass(frozen=True, slots=True)
class PostboxDeliveryTemplateRef:
    id: str
    slug: str
    name: str
    description: str | None
    published_revision_id: str
    function_type_id: str | None
    scope_kind: str
    scope_id: str | None
    classification: str
    allow_vacant_delivery: bool


@dataclass(frozen=True, slots=True)
class PostboxOrganizationFunctionTargetRef:
    id: str
    slug: str
    name: str
    function_type_id: str | None = None


@dataclass(frozen=True, slots=True)
class PostboxOrganizationUnitTargetRef:
    id: str
    slug: str
    name: str
    unit_type_id: str | None = None
    parent_id: str | None = None
    functions: tuple[PostboxOrganizationFunctionTargetRef, ...] = ()


@dataclass(frozen=True, slots=True)
class PostboxDeliveryCatalogRef:
    postboxes: tuple["PostboxDirectoryEntryRef", ...] = ()
    templates: tuple[PostboxDeliveryTemplateRef, ...] = ()
    organization_units: tuple[PostboxOrganizationUnitTargetRef, ...] = ()


@dataclass(frozen=True, slots=True)
class PostboxAccessDecisionRef:
    allowed: bool
    action: PostboxAction
    postbox_id: str
    reason_code: str
    explanation: str
    organization_unit_id: str | None = None
    function_id: str | None = None
    assignment_ids: tuple[str, ...] = ()
    assignment_sources: tuple[str, ...] = ()
    selected_assignment_id: str | None = None
    holder_count: int = 0
    vacant: bool = True
    classification: str = "internal"
    classification_allowed: bool = True
    binding_status: PostboxBindingStatus = "active"


@dataclass(frozen=True, slots=True)
class PostboxDirectoryEntryRef:
    id: str
    tenant_id: str
    address: str
    address_key: str
    name: str
    status: str
    classification: str
    organization_unit_id: str | None = None
    organization_unit_name: str | None = None
    function_id: str | None = None
    function_name: str | None = None
    context_key: str | None = None
    template_revision_id: str | None = None
    holder_count: int = 0
    vacant: bool = True
    access: PostboxAccessDecisionRef | None = None
    resource_revision: int = 1
    etag: str | None = None


@dataclass(frozen=True, slots=True)
class PostboxParticipantRef:
    kind: str
    reference_type: str
    reference_id: str | None = None
    label: str | None = None
    address: str | None = None


@dataclass(frozen=True, slots=True)
class PostboxAttachmentRef:
    reference_type: str
    reference_id: str
    name: str | None = None
    media_type: str | None = None
    size_bytes: int | None = None
    digest: str | None = None
    metadata: Mapping[str, object] = field(default_factory=dict)


@dataclass(frozen=True, slots=True)
class PostboxWrappedKeyRef:
    """Opaque envelope-key record; key material remains crypto-provider owned."""

    recipient_type: str
    recipient_id: str
    key_epoch: int
    wrapped_key_ref: str
    algorithm: str | None = None
    metadata: Mapping[str, object] = field(default_factory=dict)


@dataclass(frozen=True, slots=True)
class PostboxExternalRecipientTokenRef:
    """External grant state without the bearer secret itself."""

    token_id: str
    state: str
    expires_at: datetime | None = None
    one_time: bool = False
    key_fetched_at: datetime | None = None
    revoked_at: datetime | None = None
    assurance_profile: str | None = None
    metadata: Mapping[str, object] = field(default_factory=dict)


@dataclass(frozen=True, slots=True)
class PostboxMessageRef:
    id: str
    tenant_id: str
    postbox_id: str
    subject: str
    body_text: str | None
    status: str
    availability: PostboxMessageAvailability
    classification: str
    sender_label: str | None
    delivered_at: datetime
    read_at: datetime | None = None
    acknowledged_at: datetime | None = None
    expires_at: datetime | None = None
    withdrawn_at: datetime | None = None
    producer_module: str | None = None
    producer_resource_type: str | None = None
    producer_resource_id: str | None = None
    in_reply_to_message_id: str | None = None
    replaces_message_id: str | None = None
    encryption_profile: str = "plaintext_v1"
    key_epoch: int = 1
    ciphertext_ref: str | None = None
    signed_manifest_ref: str | None = None
    wrapped_keys: tuple[PostboxWrappedKeyRef, ...] = ()
    external_recipient_tokens: tuple[PostboxExternalRecipientTokenRef, ...] = ()
    participants: tuple[PostboxParticipantRef, ...] = ()
    attachments: tuple[PostboxAttachmentRef, ...] = ()
    metadata: Mapping[str, object] = field(default_factory=dict)


@dataclass(frozen=True, slots=True)
class PostboxMessageAuthoringRequest:
    idempotency_key: str
    subject: str
    body_text: str | None = None
    classification: str = "internal"
    participants: tuple[PostboxParticipantRef, ...] = ()
    attachments: tuple[PostboxAttachmentRef, ...] = ()
    metadata: Mapping[str, object] = field(default_factory=dict)


@dataclass(frozen=True, slots=True)
class PostboxDeliveryReceiptSummaryRef:
    delivery_id: str
    message_id: str
    postbox_id: str
    delivery_status: str
    accepted_at: datetime
    current_holder_count: int = 0
    currently_readable: bool = False
    message_count: int = 1
    routed_message_count: int = 0
    readable_message_count: int = 0
    read_receipt_count: int = 0
    acknowledged_receipt_count: int = 0
    withdrawn_message_count: int = 0
    expired_message_count: int = 0
    first_read_at: datetime | None = None
    last_read_at: datetime | None = None
    first_acknowledged_at: datetime | None = None
    last_acknowledged_at: datetime | None = None
    route_status_counts: Mapping[str, int] = field(default_factory=dict)


@dataclass(frozen=True, slots=True)
class PostboxDeliveryRequest:
    tenant_id: str
    target: PostboxTargetRef
    producer_module: str
    producer_resource_type: str
    producer_resource_id: str | None
    idempotency_key: str
    subject: str
    body_text: str | None = None
    sender_label: str | None = None
    classification: str = "internal"
    participants: tuple[PostboxParticipantRef, ...] = ()
    attachments: tuple[PostboxAttachmentRef, ...] = ()
    expires_at: datetime | None = None
    ciphertext_ref: str | None = None
    signed_manifest_ref: str | None = None
    wrapped_keys: tuple[PostboxWrappedKeyRef, ...] = ()
    external_recipient_tokens: tuple[PostboxExternalRecipientTokenRef, ...] = ()
    metadata: Mapping[str, object] = field(default_factory=dict)


@dataclass(frozen=True, slots=True)
class PostboxDeliveryResult:
    delivery_id: str
    postbox_id: str
    message_id: str
    address: str
    status: str
    vacant: bool
    holder_count: int
    duplicate: bool = False
    evidence: Mapping[str, object] = field(default_factory=dict)


class PostboxDeliveryRejected(RuntimeError):
    """A delivery was rejected before the provider accepted any effect."""

    def __init__(
        self,
        code: str,
        message: str,
        *,
        temporary: bool = False,
    ) -> None:
        super().__init__(message)
        self.code = code
        self.temporary = temporary


class PostboxDeliveryOutcomeUnknown(RuntimeError):
    """The provider may have accepted an effect and must not be bypassed."""

    def __init__(self, code: str, message: str) -> None:
        super().__init__(message)
        self.code = code


@runtime_checkable
class PostboxDirectoryProvider(Protocol):
    def list_visible_postboxes(
        self,
        session: object,
        *,
        tenant_id: str,
        actor: PostboxActorRef,
    ) -> Sequence[PostboxDirectoryEntryRef]:
        ...

    def resolve_postbox(
        self,
        session: object,
        *,
        tenant_id: str,
        target: PostboxTargetRef,
        materialize: bool = False,
    ) -> PostboxDirectoryEntryRef | None:
        ...

    def delivery_catalog(
        self,
        session: object,
        *,
        tenant_id: str,
    ) -> PostboxDeliveryCatalogRef:
        ...


@runtime_checkable
class PostboxAccessProvider(Protocol):
    def explain_access(
        self,
        session: object,
        *,
        tenant_id: str,
        postbox_id: str,
        actor: PostboxActorRef,
        action: PostboxAction,
    ) -> PostboxAccessDecisionRef:
        ...


@runtime_checkable
class PostboxMessagesProvider(Protocol):
    def list_messages(
        self,
        session: object,
        *,
        tenant_id: str,
        postbox_ids: Sequence[str],
        actor: PostboxActorRef,
        limit: int = 100,
        offset: int = 0,
        query: str | None = None,
        state: PostboxMessageListState = "all",
    ) -> Sequence[PostboxMessageRef]:
        ...

    def get_message(
        self,
        session: object,
        *,
        tenant_id: str,
        message_id: str,
        actor: PostboxActorRef,
    ) -> PostboxMessageRef | None:
        ...

    def mark_message(
        self,
        session: object,
        *,
        tenant_id: str,
        message_id: str,
        actor: PostboxActorRef,
        state: Literal["read", "acknowledged"],
    ) -> PostboxMessageRef:
        ...

    def create_message(
        self,
        session: object,
        *,
        tenant_id: str,
        postbox_id: str,
        actor: PostboxActorRef,
        request: PostboxMessageAuthoringRequest,
    ) -> PostboxMessageRef:
        ...

    def reply_to_message(
        self,
        session: object,
        *,
        tenant_id: str,
        message_id: str,
        actor: PostboxActorRef,
        request: PostboxMessageAuthoringRequest,
    ) -> PostboxMessageRef:
        ...


@runtime_checkable
class PostboxDeliveryProvider(Protocol):
    def deliver(
        self,
        session: object,
        request: PostboxDeliveryRequest,
    ) -> PostboxDeliveryResult:
        ...


@runtime_checkable
class PostboxEvidenceProvider(Protocol):
    def link_evidence(
        self,
        session: object,
        *,
        tenant_id: str,
        message_id: str,
        attachment: PostboxAttachmentRef,
    ) -> PostboxMessageRef:
        ...

    def delivery_receipt_summaries(
        self,
        session: object,
        *,
        tenant_id: str,
        producer_module: str,
        delivery_ids: Sequence[str],
    ) -> Mapping[str, PostboxDeliveryReceiptSummaryRef]:
        ...


@runtime_checkable
class PostboxRoutingProvider(Protocol):
    def dispatch_due_routes(
        self,
        session: object,
        *,
        tenant_id: str | None = None,
        limit: int = 50,
    ) -> Mapping[str, object]:
        ...


def _postbox_provider(
    registry: object | None,
    *,
    capability_name: str,
    provider_type: type,
) -> object | None:
    if registry is None or not hasattr(registry, "has_capability"):
        return None
    if not registry.has_capability(capability_name):
        return None
    capability = registry.capability(capability_name)
    return capability if isinstance(capability, provider_type) else None


def postbox_directory_provider(
    registry: object | None,
) -> PostboxDirectoryProvider | None:
    provider = _postbox_provider(
        registry,
        capability_name=CAPABILITY_POSTBOX_DIRECTORY,
        provider_type=PostboxDirectoryProvider,
    )
    return provider if isinstance(provider, PostboxDirectoryProvider) else None


def postbox_access_provider(
    registry: object | None,
) -> PostboxAccessProvider | None:
    provider = _postbox_provider(
        registry,
        capability_name=CAPABILITY_POSTBOX_ACCESS,
        provider_type=PostboxAccessProvider,
    )
    return provider if isinstance(provider, PostboxAccessProvider) else None


def postbox_messages_provider(
    registry: object | None,
) -> PostboxMessagesProvider | None:
    provider = _postbox_provider(
        registry,
        capability_name=CAPABILITY_POSTBOX_MESSAGES,
        provider_type=PostboxMessagesProvider,
    )
    return provider if isinstance(provider, PostboxMessagesProvider) else None


def postbox_delivery_provider(
    registry: object | None,
) -> PostboxDeliveryProvider | None:
    provider = _postbox_provider(
        registry,
        capability_name=CAPABILITY_POSTBOX_DELIVERY,
        provider_type=PostboxDeliveryProvider,
    )
    return provider if isinstance(provider, PostboxDeliveryProvider) else None


def postbox_evidence_provider(
    registry: object | None,
) -> PostboxEvidenceProvider | None:
    provider = _postbox_provider(
        registry,
        capability_name=CAPABILITY_POSTBOX_EVIDENCE,
        provider_type=PostboxEvidenceProvider,
    )
    return provider if isinstance(provider, PostboxEvidenceProvider) else None


def postbox_routing_provider(
    registry: object | None,
) -> PostboxRoutingProvider | None:
    provider = _postbox_provider(
        registry,
        capability_name=CAPABILITY_POSTBOX_ROUTING,
        provider_type=PostboxRoutingProvider,
    )
    return provider if isinstance(provider, PostboxRoutingProvider) else None
