# Copyright (c) 2026 Nous Research
"""Typed boundary for Telegram Channel Direct Messages."""

from __future__ import annotations

from dataclasses import dataclass
from enum import StrEnum
from typing import ClassVar, Protocol, override, runtime_checkable

from pydantic import BaseModel, ConfigDict, ValidationError

_MAX_TELEGRAM_TOPIC_ID = (1 << 52) - 1


class ChannelInboxRejectReason(StrEnum):
    """Fail-closed reasons safe to emit without user identifiers."""

    INVALID_TOPIC = "invalid_topic"
    NOT_DIRECT_MESSAGES_CHAT = "not_direct_messages_chat"
    SENDER_IS_NOT_TOPIC_USER = "sender_is_not_topic_user"
    TOPIC_USER_MISSING = "topic_user_missing"


@dataclass(frozen=True, slots=True)
class ChannelInboxIngressError(Exception):
    """A configured inbox update failed its identity boundary."""

    reason: ChannelInboxRejectReason

    @override
    def __str__(self) -> str:
        """Return the stable machine-consumed reason code.

        Returns:
            The rejection reason.

        """
        return self.reason.value


@dataclass(frozen=True, slots=True)
class ChannelInboxIngress:
    """Normalized customer identity and delivery address."""

    user_id: str
    user_name: str | None
    chat_id: str
    topic_id: str


class _TelegramUser(BaseModel):
    model_config: ClassVar[ConfigDict] = ConfigDict(
        extra="ignore",
        frozen=True,
        from_attributes=True,
        strict=True,
    )

    id: int
    full_name: str


class _DirectMessagesTopic(BaseModel):
    model_config: ClassVar[ConfigDict] = ConfigDict(
        extra="ignore",
        frozen=True,
        from_attributes=True,
        strict=True,
    )

    topic_id: int
    user: _TelegramUser | None


class _DirectMessagesChat(BaseModel):
    model_config: ClassVar[ConfigDict] = ConfigDict(
        extra="ignore",
        frozen=True,
        from_attributes=True,
        strict=True,
    )

    id: int
    is_direct_messages: bool | None


class _ChannelInboxMessage(BaseModel):
    """Minimal Bot API message shape used at the boundary."""

    model_config: ClassVar[ConfigDict] = ConfigDict(
        extra="ignore",
        frozen=True,
        from_attributes=True,
        strict=True,
    )

    chat: _DirectMessagesChat
    direct_messages_topic: _DirectMessagesTopic | None
    from_user: _TelegramUser | None


@runtime_checkable
class ChannelInboxActor(Protocol):
    """Minimal Telegram actor accepted at the ingress boundary."""

    @property
    def id(self) -> int:
        """Return the Telegram user ID."""
        ...

    @property
    def full_name(self) -> str:
        """Return the display name."""
        ...


@runtime_checkable
class ChannelInboxChat(Protocol):
    """Minimal containing chat exposed by Telegram messages."""

    @property
    def id(self) -> int:
        """Return the Telegram chat ID."""
        ...


@runtime_checkable
class ChannelInboxMessage(Protocol):
    """Telegram message accepted by Pydantic's attribute parser."""

    @property
    def chat(self) -> ChannelInboxChat:
        """Return the containing chat."""
        ...


class ChannelInboxRuntimeConfig(Protocol):
    """Typed adapter view of enabled inbox configuration."""

    @property
    def enabled(self) -> bool: ...

    @property
    def direct_messages_chat_id(self) -> str: ...

    @property
    def parent_channel_id(self) -> str: ...

    def matches_chat(self, chat_id: int | str) -> bool: ...

    def runtime_matches(
        self,
        *,
        bot_id: int | str,
        parent_channel_id: int | str,
        candidate_digest: str,
        can_manage_direct_messages: bool,
    ) -> bool: ...


def _positive_topic_id(value: int) -> str | None:
    if value <= 0 or value > _MAX_TELEGRAM_TOPIC_ID:
        return None
    return str(value)


def parse_channel_inbox_message(
    message: ChannelInboxMessage,
    config: ChannelInboxRuntimeConfig | None,
    *,
    actor_user: ChannelInboxActor | None = None,
) -> ChannelInboxIngress | None:
    """Parse one configured channel-DM message.

    Returns:
        The exact customer identity and topic, or ``None`` outside the enabled
        inbox.

    Raises:
        ChannelInboxIngressError: The configured inbox message is malformed or
            was not authored by its topic customer.

    """
    if config is None or not config.enabled:
        return None
    try:
        parsed_message = _ChannelInboxMessage.model_validate(message)
    except ValidationError as exc:
        raise ChannelInboxIngressError(
            ChannelInboxRejectReason.INVALID_TOPIC,
        ) from exc
    chat = parsed_message.chat
    if not config.matches_chat(chat.id):
        return None
    if chat.is_direct_messages is not True:
        raise ChannelInboxIngressError(
            ChannelInboxRejectReason.NOT_DIRECT_MESSAGES_CHAT,
        )
    topic = parsed_message.direct_messages_topic
    if topic is None:
        raise ChannelInboxIngressError(ChannelInboxRejectReason.INVALID_TOPIC)
    topic_id = _positive_topic_id(topic.topic_id)
    if topic_id is None:
        raise ChannelInboxIngressError(ChannelInboxRejectReason.INVALID_TOPIC)
    topic_user = topic.user
    if topic_user is None:
        raise ChannelInboxIngressError(ChannelInboxRejectReason.TOPIC_USER_MISSING)
    topic_user_id = topic_user.id
    if actor_user is None:
        sender = parsed_message.from_user
    else:
        try:
            sender = _TelegramUser.model_validate(actor_user)
        except ValidationError as exc:
            raise ChannelInboxIngressError(
                ChannelInboxRejectReason.SENDER_IS_NOT_TOPIC_USER,
            ) from exc
    sender_id = sender.id if sender is not None else None
    if sender_id != topic_user_id:
        raise ChannelInboxIngressError(
            ChannelInboxRejectReason.SENDER_IS_NOT_TOPIC_USER,
        )
    user_name = topic_user.full_name.strip() or None
    return ChannelInboxIngress(
        user_id=str(topic_user_id),
        user_name=user_name,
        chat_id=config.direct_messages_chat_id,
        topic_id=topic_id,
    )
