import math
from collections.abc import Collection
from dataclasses import dataclass
from typing import Any

from zerver.lib.mention import MentionData
from zerver.lib.user_groups import get_user_group_member_ids
from zerver.models import NamedUserGroup, UserProfile, UserTopic
from zerver.models.scheduled_jobs import NotificationTriggers


@dataclass
class UserMessageNotificationsData:
    user_id: int
    online_push_enabled: bool
    dm_email_notify: bool
    dm_push_notify: bool
    mention_email_notify: bool
    mention_push_notify: bool
    topic_wildcard_mention_email_notify: bool
    topic_wildcard_mention_push_notify: bool
    stream_wildcard_mention_email_notify: bool
    stream_wildcard_mention_push_notify: bool
    stream_push_notify: bool
    stream_email_notify: bool
    followed_topic_push_notify: bool
    followed_topic_email_notify: bool
    topic_wildcard_mention_in_followed_topic_push_notify: bool
    topic_wildcard_mention_in_followed_topic_email_notify: bool
    stream_wildcard_mention_in_followed_topic_push_notify: bool
    stream_wildcard_mention_in_followed_topic_email_notify: bool
    sender_is_muted: bool
    disable_external_notifications: bool

    def __post_init__(self) -> None:
        # Check that there's no dubious data.
        if self.dm_email_notify or self.dm_push_notify:
            assert not (
                self.stream_email_notify
                or self.stream_push_notify
                or self.followed_topic_email_notify
                or self.followed_topic_push_notify
            )

        if (
            self.stream_email_notify
            or self.stream_push_notify
            or self.followed_topic_email_notify
            or self.followed_topic_push_notify
        ):
            assert not (self.dm_email_notify or self.dm_push_notify)

    @classmethod
    def from_user_id_sets(
        cls,
        *,
        user_id: int,
        flags: Collection[str],
        private_message: bool,
        disable_external_notifications: bool,
        online_push_user_ids: set[int],
        dm_mention_push_disabled_user_ids: set[int],
        dm_mention_email_disabled_user_ids: set[int],
        stream_push_user_ids: set[int],
        stream_email_user_ids: set[int],
        topic_wildcard_mention_user_ids: set[int],
        stream_wildcard_mention_user_ids: set[int],
        followed_topic_push_user_ids: set[int],
        followed_topic_email_user_ids: set[int],
        topic_wildcard_mention_in_followed_topic_user_ids: set[int],
        stream_wildcard_mention_in_followed_topic_user_ids: set[int],
        muted_sender_user_ids: set[int],
        all_bot_user_ids: set[int],
        push_device_registered_user_ids: set[int] | None,
    ) -> "UserMessageNotificationsData":
        if user_id in all_bot_user_ids:
            # Don't send any notifications to bots
            return cls(
                user_id=user_id,
                dm_email_notify=False,
                mention_email_notify=False,
                topic_wildcard_mention_email_notify=False,
                stream_wildcard_mention_email_notify=False,
                dm_push_notify=False,
                mention_push_notify=False,
                topic_wildcard_mention_push_notify=False,
                stream_wildcard_mention_push_notify=False,
                online_push_enabled=False,
                stream_push_notify=False,
                stream_email_notify=False,
                followed_topic_push_notify=False,
                followed_topic_email_notify=False,
                topic_wildcard_mention_in_followed_topic_push_notify=False,
                topic_wildcard_mention_in_followed_topic_email_notify=False,
                stream_wildcard_mention_in_followed_topic_push_notify=False,
                stream_wildcard_mention_in_followed_topic_email_notify=False,
                sender_is_muted=False,
                disable_external_notifications=False,
            )

        # `stream_wildcard_mention_user_ids`, `topic_wildcard_mention_user_ids`,
        # `stream_wildcard_mention_in_followed_topic_user_ids` and `topic_wildcard_mention_in_followed_topic_user_ids`
        # are those user IDs for whom stream or topic wildcard mentions should obey notification
        # settings for personal mentions. Hence, it isn't an independent notification setting and acts as a wrapper.
        dm_email_notify = user_id not in dm_mention_email_disabled_user_ids and private_message
        mention_email_notify = (
            user_id not in dm_mention_email_disabled_user_ids and "mentioned" in flags
        )
        topic_wildcard_mention_email_notify = (
            user_id in topic_wildcard_mention_user_ids
            and user_id not in dm_mention_email_disabled_user_ids
            and "topic_wildcard_mentioned" in flags
        )
        stream_wildcard_mention_email_notify = (
            user_id in stream_wildcard_mention_user_ids
            and user_id not in dm_mention_email_disabled_user_ids
            and "stream_wildcard_mentioned" in flags
        )
        topic_wildcard_mention_in_followed_topic_email_notify = (
            user_id in topic_wildcard_mention_in_followed_topic_user_ids
            and user_id not in dm_mention_email_disabled_user_ids
            and "topic_wildcard_mentioned" in flags
        )
        stream_wildcard_mention_in_followed_topic_email_notify = (
            user_id in stream_wildcard_mention_in_followed_topic_user_ids
            and user_id not in dm_mention_email_disabled_user_ids
            and "stream_wildcard_mentioned" in flags
        )

        # TODO/compatibility: `push_device_registered_user_ids` is None when
        # `process_message_event`/`process_message_update_event` handles events
        # prior to the introduction of `push_device_registered_user_ids` field in the events.
        # We hardcode `push_device_registered` to True as the check is meant for newer events.
        #
        # Remove the `if` block when one can no longer directly upgrade from 11.x to main.
        if push_device_registered_user_ids is None:
            push_device_registered = True
        else:
            push_device_registered = user_id in push_device_registered_user_ids

        dm_push_notify = (
            push_device_registered
            and user_id not in dm_mention_push_disabled_user_ids
            and private_message
        )
        mention_push_notify = (
            push_device_registered
            and user_id not in dm_mention_push_disabled_user_ids
            and "mentioned" in flags
        )
        topic_wildcard_mention_push_notify = (
            push_device_registered
            and user_id in topic_wildcard_mention_user_ids
            and user_id not in dm_mention_push_disabled_user_ids
            and "topic_wildcard_mentioned" in flags
        )
        stream_wildcard_mention_push_notify = (
            push_device_registered
            and user_id in stream_wildcard_mention_user_ids
            and user_id not in dm_mention_push_disabled_user_ids
            and "stream_wildcard_mentioned" in flags
        )
        topic_wildcard_mention_in_followed_topic_push_notify = (
            push_device_registered
            and user_id in topic_wildcard_mention_in_followed_topic_user_ids
            and user_id not in dm_mention_push_disabled_user_ids
            and "topic_wildcard_mentioned" in flags
        )
        stream_wildcard_mention_in_followed_topic_push_notify = (
            push_device_registered
            and user_id in stream_wildcard_mention_in_followed_topic_user_ids
            and user_id not in dm_mention_push_disabled_user_ids
            and "stream_wildcard_mentioned" in flags
        )
        online_push_enabled = push_device_registered and user_id in online_push_user_ids
        stream_push_notify = push_device_registered and user_id in stream_push_user_ids
        followed_topic_push_notify = (
            push_device_registered and user_id in followed_topic_push_user_ids
        )
        return cls(
            user_id=user_id,
            dm_email_notify=dm_email_notify,
            mention_email_notify=mention_email_notify,
            topic_wildcard_mention_email_notify=topic_wildcard_mention_email_notify,
            stream_wildcard_mention_email_notify=stream_wildcard_mention_email_notify,
            dm_push_notify=dm_push_notify,
            mention_push_notify=mention_push_notify,
            topic_wildcard_mention_push_notify=topic_wildcard_mention_push_notify,
            stream_wildcard_mention_push_notify=stream_wildcard_mention_push_notify,
            online_push_enabled=online_push_enabled,
            stream_push_notify=stream_push_notify,
            stream_email_notify=user_id in stream_email_user_ids,
            followed_topic_push_notify=followed_topic_push_notify,
            followed_topic_email_notify=user_id in followed_topic_email_user_ids,
            topic_wildcard_mention_in_followed_topic_push_notify=topic_wildcard_mention_in_followed_topic_push_notify,
            topic_wildcard_mention_in_followed_topic_email_notify=topic_wildcard_mention_in_followed_topic_email_notify,
            stream_wildcard_mention_in_followed_topic_push_notify=stream_wildcard_mention_in_followed_topic_push_notify,
            stream_wildcard_mention_in_followed_topic_email_notify=stream_wildcard_mention_in_followed_topic_email_notify,
            sender_is_muted=user_id in muted_sender_user_ids,
            disable_external_notifications=disable_external_notifications,
        )

    # For these functions, acting_user_id is the user sent a message
    # (or edited a message) triggering the event for which we need to
    # determine notifiability.
    def trivially_should_not_notify(self, acting_user_id: int) -> bool:
        """Common check for reasons not to trigger a notification that are
        independent of users' notification settings and thus don't
        depend on what type of notification (email/push) it is.
        """
        if self.user_id == acting_user_id:
            return True

        if self.sender_is_muted:
            return True

        if self.disable_external_notifications:
            return True

        return False

    def is_notifiable(self, acting_user_id: int, idle: bool) -> bool:
        return self.is_email_notifiable(acting_user_id, idle) or self.is_push_notifiable(
            acting_user_id, idle
        )

    def is_push_notifiable(self, acting_user_id: int, idle: bool) -> bool:
        return self.get_push_notification_trigger(acting_user_id, idle) is not None

    def get_push_notification_trigger(self, acting_user_id: int, idle: bool) -> str | None:
        if not idle and not self.online_push_enabled:
            return None

        if self.trivially_should_not_notify(acting_user_id):
            return None

        # The order here is important. If, for example, both
        # `mention_push_notify` and `stream_push_notify` are True, we
        # want to classify it as a mention, since that's more salient.
        if self.dm_push_notify:
            return NotificationTriggers.DIRECT_MESSAGE
        elif self.mention_push_notify:
            return NotificationTriggers.MENTION
        elif self.topic_wildcard_mention_in_followed_topic_push_notify:
            return NotificationTriggers.TOPIC_WILDCARD_MENTION_IN_FOLLOWED_TOPIC
        elif self.stream_wildcard_mention_in_followed_topic_push_notify:
            return NotificationTriggers.STREAM_WILDCARD_MENTION_IN_FOLLOWED_TOPIC
        elif self.topic_wildcard_mention_push_notify:
            return NotificationTriggers.TOPIC_WILDCARD_MENTION
        elif self.stream_wildcard_mention_push_notify:
            return NotificationTriggers.STREAM_WILDCARD_MENTION
        elif self.followed_topic_push_notify:
            return NotificationTriggers.FOLLOWED_TOPIC_PUSH
        elif self.stream_push_notify:
            return NotificationTriggers.STREAM_PUSH
        else:
            return None

    def is_email_notifiable(self, acting_user_id: int, idle: bool) -> bool:
        return self.get_email_notification_trigger(acting_user_id, idle) is not None

    def get_email_notification_trigger(self, acting_user_id: int, idle: bool) -> str | None:
        if not idle:
            return None

        if self.trivially_should_not_notify(acting_user_id):
            return None

        # The order here is important. If, for example, both
        # `mention_email_notify` and `stream_email_notify` are True, we
        # want to classify it as a mention, since that's more salient.
        if self.dm_email_notify:
            return NotificationTriggers.DIRECT_MESSAGE
        elif self.mention_email_notify:
            return NotificationTriggers.MENTION
        elif self.topic_wildcard_mention_in_followed_topic_email_notify:
            return NotificationTriggers.TOPIC_WILDCARD_MENTION_IN_FOLLOWED_TOPIC
        elif self.stream_wildcard_mention_in_followed_topic_email_notify:
            return NotificationTriggers.STREAM_WILDCARD_MENTION_IN_FOLLOWED_TOPIC
        elif self.topic_wildcard_mention_email_notify:
            return NotificationTriggers.TOPIC_WILDCARD_MENTION
        elif self.stream_wildcard_mention_email_notify:
            return NotificationTriggers.STREAM_WILDCARD_MENTION
        elif self.followed_topic_email_notify:
            return NotificationTriggers.FOLLOWED_TOPIC_EMAIL
        elif self.stream_email_notify:
            return NotificationTriggers.STREAM_EMAIL
        else:
            return None


def user_allows_notifications_in_StreamTopic(
    stream_is_muted: bool,
    visibility_policy: int,
    stream_specific_setting: bool | None,
    global_setting: bool,
) -> bool:
    """
    Captures the hierarchy of notification settings, where visibility policy is considered first,
    followed by stream-specific settings, and the global-setting in the UserProfile is the fallback.
    """
    if stream_is_muted and visibility_policy != UserTopic.VisibilityPolicy.UNMUTED:
        return False

    if visibility_policy == UserTopic.VisibilityPolicy.MUTED:
        return False

    if stream_specific_setting is not None:
        return stream_specific_setting

    return global_setting


def get_user_group_mentions_data(
    mentioned_user_ids: set[int], mentioned_user_group_ids: list[int], mention_data: MentionData
) -> dict[int, int]:
    # Maps user_id -> mentioned user_group_id
    mentioned_user_groups_map: dict[int, int] = dict()

    # Add members of the mentioned user groups into `mentions_user_ids`.
    for group_id in mentioned_user_group_ids:
        member_ids = mention_data.get_group_members(group_id)
        for member_id in member_ids:
            if member_id in mentioned_user_ids:
                # If a user is also mentioned personally, we use that as a trigger
                # for notifications.
                continue

            if member_id in mentioned_user_groups_map:
                # If multiple user groups are mentioned, we prefer the
                # user group with the least members for email/mobile
                # notifications.
                previous_group_id = mentioned_user_groups_map[member_id]
                previous_group_member_ids = mention_data.get_group_members(previous_group_id)

                if len(previous_group_member_ids) > len(member_ids):
                    mentioned_user_groups_map[member_id] = group_id
            else:
                mentioned_user_groups_map[member_id] = group_id

    return mentioned_user_groups_map


@dataclass
class MentionedUserGroup:
    id: int
    name: str
    members_count: int


def get_mentioned_user_group(
    messages: list[dict[str, Any]], user_profile: UserProfile
) -> MentionedUserGroup | None:
    """Returns the user group name to display in the email notification
    if user group(s) are mentioned.

    This implements the same algorithm as get_user_group_mentions_data
    in zerver/lib/notification_data.py, but we're passed a list of
    messages instead.
    """
    for message in messages:
        if (
            message.get("mentioned_user_group_id") is None
            and message["trigger"] == NotificationTriggers.MENTION
        ):
            # The user has also been personally mentioned, so that gets prioritized.
            return None

    # These IDs are those of the smallest user groups mentioned in each message.
    mentioned_user_group_ids = [
        message["mentioned_user_group_id"]
        for message in messages
        if message.get("mentioned_user_group_id") is not None
    ]

    if len(mentioned_user_group_ids) == 0:
        return None

    # We now want to calculate the name of the smallest user group mentioned among
    # all these messages.
    smallest_user_group_size = math.inf
    for user_group_id in mentioned_user_group_ids:
        current_user_group = NamedUserGroup.objects.get(
            id=user_group_id, realm_for_sharding=user_profile.realm
        )
        current_mentioned_user_group = MentionedUserGroup(
            id=current_user_group.id,
            name=current_user_group.name,
            members_count=len(get_user_group_member_ids(current_user_group)),
        )

        if current_mentioned_user_group.members_count < smallest_user_group_size:
            # If multiple user groups are mentioned, we prefer the
            # user group with the least members.
            smallest_user_group_size = current_mentioned_user_group.members_count
            smallest_mentioned_user_group = current_mentioned_user_group

    return smallest_mentioned_user_group
