"""unified message protocol for channel adapters.

defines platform-agnostic dataclasses for inbound messages, outbound responses,
file attachments, and routing protocol that channel adapters implement.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from datetime import UTC, datetime
from typing import Any, Protocol, runtime_checkable

__all__ = [
    "Attachment",
    "ChannelMessage",
    "ChannelResponse",
    "ChannelRouter",
]


@dataclass
class Attachment:
    """file or image attachment carried with channel message or response.

    :param filename: name of attached file
    :ptype filename: str
    :param content: raw file content as bytes
    :ptype content: bytes
    :param content_type: MIME type of file content
    :ptype content_type: str
    :param description: alt text for images or brief description
    :ptype description: str | None
    """

    filename: str
    content: bytes
    content_type: str
    description: str | None = None


@dataclass
class ChannelMessage:
    """inbound message from channel adapter to platform.

    normalizes messages from different platforms (slack, discord, websocket)
    into single unified structure for routing and processing.

    :param channel_type: platform identifier (slack, discord, websocket)
    :ptype channel_type: str
    :param content: message text content
    :ptype content: str
    :param sender_id: platform-specific user identifier
    :ptype sender_id: str
    :param sender_name: display name of sender
    :ptype sender_name: str | None
    :param sender_email: the address the channel platform holds for this sender,
        as reported by the platform — never self-asserted by the message itself.
        None when the platform exposes no address for its users (Discord bots
        cannot read one at all) or when the app lacks the scope to read it
        (Slack omits ``profile.email`` **silently** without ``users:read.email``).
        Carried so a host can reconcile a chat participant with an identity it
        already knows; a host MUST decide for itself whether to trust it.
    :ptype sender_email: str | None
    :param sender_email_verified: whether the CHANNEL PLATFORM asserts the
        address about this member. Deliberately weaker than an IdP's
        ``email_verified``: it means "the platform holds this on the member's
        profile", not "this person was proven to control that mailbox". A
        workspace is administered by the same organisation whose members it
        describes, so a host must treat this as the organisation's word about
        itself and apply its own condition before recording the address as
        verified.
    :ptype sender_email_verified: bool
    :param customer_id: platform customer scope for the sender, surfaced by
        the host from its server-authenticated session (e.g. the websocket
        access-token ``customer_id`` claim). carried so a host that already
        knows the authenticated customer can populate the outbound envelope
        without re-resolving it, and so it is never sourced from
        client-supplied ``metadata`` (which a client could spoof). None when
        the adapter has no authenticated customer (e.g. the realm-gated
        Slack/Discord adapters, which resolve the customer hub-side).
    :ptype customer_id: str | None
    :param conversation_id: platform thread or conversation reference
    :ptype conversation_id: str | None
    :param channel_id: platform channel or room reference
    :ptype channel_id: str | None
    :param workspace_id: platform workspace or guild reference
    :ptype workspace_id: str | None
    :param receiving_bot_id: platform identifier of the bot/app that RECEIVED
        this message (e.g. the slack ``bot_user_id`` of the app whose socket the
        event arrived on). a workspace can host several bots — one per agent —
        so this is what lets a router resolve the target agent unambiguously
        rather than guessing by workspace alone. None when the adapter has a
        single bot (nothing to disambiguate) or cannot determine it.
    :ptype receiving_bot_id: str | None
    :param attachments: files or images attached to message
    :ptype attachments: list[Attachment]
    :param reply_to_id: identifier of message being replied to
    :ptype reply_to_id: str | None
    :param metadata: platform-specific extras not captured by standard fields
    :ptype metadata: dict[str, Any]
    :param timestamp: message creation time in UTC
    :ptype timestamp: datetime
    :param user_timezone: IANA timezone name for the sending user
        (e.g. ``America/Los_Angeles``). populated by the channel
        adapter from its native source: browser
        ``Intl.DateTimeFormat().resolvedOptions().timeZone`` for
        websocket, ``users.info.tz`` for slack, locale-based fallback
        for discord. None when the adapter cannot resolve a
        per-user timezone. consumers (e.g. the ``current_date``
        builtin) read this off the agent ``CallContext`` to render
        timestamps in the user's local time. per-message rather than
        per-channel because users in shared channels have different
        timezones and one user may travel between messages.
    :ptype user_timezone: str | None
    :param user_locale: BCP 47 locale tag for the sending user
        (e.g. ``en-US``, ``ja-JP``). same per-user, per-message
        semantics as ``user_timezone``: each adapter populates from
        its native source. consumers can use this for number /
        currency / date formatting hints in tool output.
    :ptype user_locale: str | None
    """

    channel_type: str
    content: str
    sender_id: str
    sender_name: str | None = None
    sender_email: str | None = None
    sender_email_verified: bool = False
    customer_id: str | None = None
    conversation_id: str | None = None
    channel_id: str | None = None
    workspace_id: str | None = None
    receiving_bot_id: str | None = None
    attachments: list[Attachment] = field(default_factory=list)
    reply_to_id: str | None = None
    metadata: dict[str, Any] = field(default_factory=dict)
    timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
    user_timezone: str | None = None
    user_locale: str | None = None


@dataclass
class ChannelResponse:
    """outbound response from platform to channel adapter.

    carries response content and routing hints back to originating channel
    for delivery to end user.

    :param content: response text in markdown format
    :ptype content: str
    :param conversation_id: thread to reply in
    :ptype conversation_id: str | None
    :param channel_id: channel to reply in
    :ptype channel_id: str | None
    :param attachments: files to send with response
    :ptype attachments: list[Attachment]
    :param format_hints: rich formatting preferences for channel adapter
    :ptype format_hints: dict[str, Any]
    :param metadata: host-application-provided extras
    :ptype metadata: dict[str, Any]
    """

    content: str
    conversation_id: str | None = None
    channel_id: str | None = None
    attachments: list[Attachment] = field(default_factory=list)
    format_hints: dict[str, Any] = field(default_factory=dict)
    metadata: dict[str, Any] = field(default_factory=dict)


@runtime_checkable
class ChannelRouter(Protocol):
    """protocol for routing inbound channel messages to processing pipeline.

    implementations receive normalized channel messages and return optional
    responses for delivery back through channel adapter.
    """

    async def route_inbound(self, message: ChannelMessage) -> ChannelResponse | None:
        """route inbound message from channel adapter.

        :param message: normalized inbound message from channel
        :ptype message: ChannelMessage
        :return: response to deliver back through channel, or None if no response
        :rtype: ChannelResponse | None
        """
        ...
