# See https://zulip.readthedocs.io/en/latest/subsystems/events-system.html for
# high-level documentation on how this system works.
import copy
import logging
import time
from collections.abc import Callable, Collection, Iterable, Sequence
from typing import Any, Literal

from django.conf import settings
from django.utils.translation import gettext as _
from typing_extensions import NotRequired, TypedDict

from version import API_FEATURE_LEVEL, ZULIP_MERGE_BASE, ZULIP_VERSION
from zerver.actions.default_streams import default_stream_groups_to_dicts_sorted
from zerver.actions.realm_settings import get_realm_authentication_methods_for_page_params_api
from zerver.actions.saved_snippets import do_get_saved_snippets
from zerver.actions.users import get_owned_bot_dicts
from zerver.lib import emoji
from zerver.lib.alert_words import user_alert_words
from zerver.lib.avatar import avatar_url
from zerver.lib.bot_config import load_bot_config_template
from zerver.lib.channel_folders import (
    get_channel_folders_for_spectators,
    get_channel_folders_in_realm,
)
from zerver.lib.compatibility import is_outdated_server
from zerver.lib.default_streams import get_default_stream_ids_for_realm
from zerver.lib.exceptions import JsonableError
from zerver.lib.external_accounts import get_default_external_accounts
from zerver.lib.integrations import (
    EMBEDDED_BOTS,
    INCOMING_WEBHOOK_INTEGRATIONS,
    get_all_event_types_for_integration,
)
from zerver.lib.message import (
    add_message_to_unread_msgs,
    aggregate_unread_data,
    apply_unread_message_event,
    extract_unread_data_from_um_rows,
    get_raw_unread_data,
    get_recent_conversations_recipient_id,
    get_recent_private_conversations,
    get_starred_message_ids,
    remove_message_id_from_unread_mgs,
)
from zerver.lib.muted_users import get_user_mutes
from zerver.lib.narrow_helpers import NeverNegatedNarrowTerm, read_stop_words
from zerver.lib.narrow_predicate import check_narrow_for_events
from zerver.lib.navigation_views import get_navigation_views_for_user
from zerver.lib.onboarding_steps import get_next_onboarding_steps
from zerver.lib.presence import get_presence_for_user, get_presences_for_realm
from zerver.lib.push_notifications import get_push_devices
from zerver.lib.realm_icon import realm_icon_url
from zerver.lib.realm_logo import get_realm_logo_source, get_realm_logo_url
from zerver.lib.scheduled_messages import (
    get_undelivered_reminders,
    get_undelivered_scheduled_messages,
)
from zerver.lib.soft_deactivation import reactivate_user_if_soft_deactivated
from zerver.lib.sounds import get_available_notification_sounds
from zerver.lib.stream_subscription import handle_stream_notifications_compatibility
from zerver.lib.streams import do_get_streams, get_web_public_streams
from zerver.lib.subscription_info import (
    build_unsubscribed_sub_from_stream_dict,
    gather_subscriptions_helper,
    get_web_public_subs,
)
from zerver.lib.thumbnail import THUMBNAIL_OUTPUT_FORMATS
from zerver.lib.timestamp import datetime_to_timestamp
from zerver.lib.timezone import canonicalize_timezone
from zerver.lib.topic import TOPIC_NAME, maybe_rename_general_chat_to_empty_topic
from zerver.lib.types import UserGroupMembersData
from zerver.lib.user_groups import (
    get_group_setting_value_for_register_api,
    get_recursive_membership_groups,
    get_role_based_system_groups_dict,
    get_server_supported_permission_settings,
    user_groups_in_realm_serialized,
)
from zerver.lib.user_status import get_all_users_status_dict
from zerver.lib.user_topics import get_topic_mutes, get_user_topics
from zerver.lib.users import (
    get_cross_realm_dicts,
    get_data_for_inaccessible_user,
    get_users_for_api,
    is_administrator_role,
    is_moderator_role,
    max_message_id_for_user,
)
from zerver.lib.utils import optional_bytes_to_mib
from zerver.models import (
    ChannelFolder,
    Client,
    CustomProfileField,
    Draft,
    Message,
    NamedUserGroup,
    Realm,
    RealmUserDefault,
    Recipient,
    Stream,
    Subscription,
    UserProfile,
    UserStatus,
    UserTopic,
)
from zerver.models.constants import MAX_TOPIC_NAME_LENGTH
from zerver.models.custom_profile_fields import custom_profile_fields_for_realm
from zerver.models.linkifiers import linkifiers_for_realm
from zerver.models.realm_emoji import get_all_custom_emoji_for_realm
from zerver.models.realm_playgrounds import get_realm_playgrounds
from zerver.models.realms import (
    MessageEditHistoryVisibilityPolicyEnum,
    RealmTopicsPolicyEnum,
    get_corresponding_policy_value_for_group_setting,
    get_realm_domains,
)
from zerver.models.streams import get_default_stream_groups
from zerver.models.users import ResolvedTopicNoticeAutoReadPolicyEnum
from zerver.tornado.django_api import get_user_events, request_event_queue
from zproject.backends import email_auth_enabled, password_auth_enabled


def add_realm_logo_fields(state: dict[str, Any], realm: Realm) -> None:
    state["realm_logo_url"] = get_realm_logo_url(realm, night=False)
    state["realm_logo_source"] = get_realm_logo_source(realm, night=False)
    state["realm_night_logo_url"] = get_realm_logo_url(realm, night=True)
    state["realm_night_logo_source"] = get_realm_logo_source(realm, night=True)
    state["max_logo_file_size_mib"] = settings.MAX_LOGO_FILE_SIZE_MIB


def always_want(msg_type: str) -> bool:
    """
    This function is used as a helper in
    fetch_initial_state_data, when the user passes
    in None for event_types, and we want to fetch
    info for every event type.  Defining this at module
    level makes it easier to mock.
    """
    return True


def has_pending_sponsorship_request(
    user_profile: UserProfile | None, user_has_billing_access: bool | None = None
) -> bool:
    sponsorship_pending = False

    if user_has_billing_access is None:
        user_has_billing_access = user_profile is not None and user_profile.has_billing_access

    if settings.CORPORATE_ENABLED and user_profile is not None and user_has_billing_access:
        from corporate.models.customers import get_customer_by_realm

        customer = get_customer_by_realm(user_profile.realm)
        if customer is not None:
            sponsorship_pending = customer.sponsorship_pending

    return sponsorship_pending


def fetch_initial_state_data(
    user_profile: UserProfile | None,
    *,
    realm: Realm,
    event_types: Iterable[str] | None = None,
    queue_id: str | None = "",
    client_gravatar: bool = False,
    user_avatar_url_field_optional: bool = False,
    slim_presence: bool = False,
    presence_last_update_id_fetched_by_client: int | None = None,
    presence_history_limit_days: int | None = None,
    include_subscribers: bool | Literal["partial"] = True,
    include_streams: bool = True,
    spectator_requested_language: str | None = None,
    pronouns_field_type_supported: bool = True,
    linkifier_url_template: bool = False,
    user_list_incomplete: bool = False,
    include_deactivated_groups: bool = False,
    archived_channels: bool = False,
    simplified_presence_events: bool = False,
) -> dict[str, Any]:
    """When `event_types` is None, fetches the core data powering the
    web app's `page_params` and `/api/v1/register` (for mobile/terminal
    apps).  Can also fetch a subset as determined by `event_types`.

    The user_profile=None code path is used for logged-out public
    access to streams with is_web_public=True.

    Whenever you add new code to this function, you should also add
    corresponding events for changes in the data structures and new
    code to apply_events (and add a test in test_events.py).
    """
    state: dict[str, Any] = {"queue_id": queue_id}

    if event_types is None:
        # return True always
        want: Callable[[str], bool] = always_want
    else:
        want = set(event_types).__contains__

    # Show the version info unconditionally.
    state["zulip_version"] = ZULIP_VERSION
    state["zulip_feature_level"] = API_FEATURE_LEVEL
    state["zulip_merge_base"] = ZULIP_MERGE_BASE

    if user_profile is not None:
        settings_user = user_profile
    else:
        assert spectator_requested_language is not None
        # When UserProfile=None, we want to serve the values for various
        # settings as the defaults.  Instead of copying the default values
        # from models/users.py here, we access these default values from a
        # temporary UserProfile object that will not be saved to the database.
        #
        # We also can set various fields to avoid duplicating code
        # unnecessarily.
        settings_user = UserProfile(
            full_name="Anonymous User",
            email="username@example.com",
            delivery_email="username@example.com",
            realm=realm,
            # We tag logged-out users as guests because most guest
            # restrictions apply to these users as well, and it lets
            # us avoid unnecessary conditionals.
            role=UserProfile.ROLE_GUEST,
            avatar_source=UserProfile.AVATAR_FROM_GRAVATAR,
            # ID=0 is not used in real Zulip databases, ensuring this is unique.
            id=0,
            default_language=spectator_requested_language,
            # Set home view to recent conversations for spectators regardless of default.
            web_home_view="recent_topics",
        )

    # We fetch early some collections of group that we need to
    # efficiently compute permissions.
    settings_user_recursive_group_ids = set()
    if want("realm_billing") or want("realm_user"):
        settings_user_recursive_group_ids = set(
            get_recursive_membership_groups(settings_user).values_list("id", flat=True)
        )

    if (
        want("realm_user_groups")
        or want("realm")
        or (want("stream") and include_streams)
        or want("subscription")
    ):
        # Optimizing opportunity: This fetches more data than
        # we strictly need when "realm_user_groups" is not in
        # fetch_event_types; we need the membership of the
        # anonymous groups in realm_setting_group_ids and the
        # IDs of the NamedUserGroup objects used there, but
        # don't need the other NamedUserGroup fields.
        realm_groups_data = user_groups_in_realm_serialized(
            realm,
            include_deactivated_groups=include_deactivated_groups,
            fetch_anonymous_group_membership=True,
        )
        anonymous_group_membership_data_dict: dict[int, UserGroupMembersData] = {}
        for key, value in realm_groups_data.anonymous_group_membership.items():
            anonymous_group_membership_data_dict[key] = UserGroupMembersData(
                direct_members=value["direct_members"],
                direct_subgroups=value["direct_subgroups"],
            )

    if want("alert_words"):
        state["alert_words"] = [] if user_profile is None else user_alert_words(user_profile)

    if want("custom_profile_fields"):
        if user_profile is None:
            # Spectators can't access full user profiles or
            # personal settings, so we send an empty list.
            state["custom_profile_fields"] = []
        else:
            fields = custom_profile_fields_for_realm(realm.id)
            state["custom_profile_fields"] = [f.as_dict() for f in fields]
        state["custom_profile_field_types"] = {
            item[4]: {"id": item[0], "name": str(item[1])}
            for item in CustomProfileField.ALL_FIELD_TYPES
        }

        if not pronouns_field_type_supported:
            for field in state["custom_profile_fields"]:
                if field["type"] == CustomProfileField.PRONOUNS:
                    field["type"] = CustomProfileField.SHORT_TEXT

            del state["custom_profile_field_types"]["PRONOUNS"]

    if want("onboarding_steps"):
        # Even if we offered special onboarding steps for guests without an
        # account, we'd maybe need to store their state using cookies
        # or local storage, rather than in the database.
        state["onboarding_steps"] = (
            [] if user_profile is None else get_next_onboarding_steps(user_profile)
        )
        state["navigation_tour_video_url"] = settings.NAVIGATION_TOUR_VIDEO_URL

    if want("message"):
        # Since the introduction of `anchor="latest"` in the API,
        # `max_message_id` is primarily used for generating `local_id`
        # values that are higher than this.  We likely can eventually
        # remove this parameter from the API.
        state["max_message_id"] = max_message_id_for_user(user_profile)

    if want("saved_snippets"):
        if user_profile is None:
            state["saved_snippets"] = []
        else:
            state["saved_snippets"] = do_get_saved_snippets(user_profile)

    if want("navigation_views"):
        if user_profile is None:
            state["navigation_views"] = []
        else:
            state["navigation_views"] = get_navigation_views_for_user(user_profile)

    if want("drafts"):
        if user_profile is None:
            state["drafts"] = []
        else:
            # Note: if a user ever disables syncing drafts then all of
            # their old drafts stored on the server will be deleted and
            # simply retained in local storage. In which case user_drafts
            # would just be an empty queryset.
            user_draft_objects = Draft.objects.filter(user_profile=user_profile).order_by(
                "-last_edit_time"
            )[: settings.MAX_DRAFTS_IN_REGISTER_RESPONSE]
            user_draft_dicts = [draft.to_dict() for draft in user_draft_objects]
            state["drafts"] = user_draft_dicts

    if want("scheduled_messages"):
        state["scheduled_messages"] = (
            [] if user_profile is None else get_undelivered_scheduled_messages(user_profile)
        )

    if want("reminders"):
        state["reminders"] = [] if user_profile is None else get_undelivered_reminders(user_profile)

    if want("muted_topics") and (
        # Suppress muted_topics data for clients that explicitly
        # support user_topic. This allows clients to request both the
        # user_topic and muted_topics, and receive the duplicate
        # muted_topics data only from older servers that don't yet
        # support user_topic.
        event_types is None or not want("user_topic")
    ):
        state["muted_topics"] = [] if user_profile is None else get_topic_mutes(user_profile)

    if want("muted_users"):
        state["muted_users"] = [] if user_profile is None else get_user_mutes(user_profile)

    if want("presence"):
        if presence_last_update_id_fetched_by_client is not None or simplified_presence_events:
            # This param being submitted by the client, means they want to use
            # the modern API.
            slim_presence = True

        if user_profile is not None:
            presences, presence_last_update_id_fetched_by_server = get_presences_for_realm(
                realm,
                slim_presence,
                last_update_id_fetched_by_client=presence_last_update_id_fetched_by_client,
                history_limit_days=presence_history_limit_days,
                requesting_user_profile=user_profile,
            )
            state["presences"] = presences
            state["presence_last_update_id"] = presence_last_update_id_fetched_by_server
        else:
            state["presences"] = {}

        # Send server_timestamp, to match the format of `GET /presence` requests.
        state["server_timestamp"] = time.time()

    if want("realm_user_groups"):
        state["realm_user_groups"] = realm_groups_data.api_groups

    if want("realm"):
        # The realm bundle includes both realm properties and server
        # properties, since it's rare that one would want one and not
        # the other. We expect most clients to want it.
        #
        # A note on naming: For some settings, one could imagine
        # having a server-level value and a realm-level value (with
        # the server value serving as the default for the realm
        # value). For such settings, we prefer the following naming
        # scheme:
        #
        # * realm_inline_image_preview (current realm setting)
        # * server_inline_image_preview (server-level default)
        #
        # In situations where for backwards-compatibility reasons we
        # have an unadorned name, we should arrange that clients using
        # that unadorned name work correctly (i.e. that should be the
        # currently active setting, not a server-level default).
        #
        # Other settings, which are just server-level settings or data
        # about the version of Zulip, can be named without prefixes,
        # e.g. gif_rating_options or development_environment.
        for property_name in Realm.property_types:
            state["realm_" + property_name] = getattr(realm, property_name)

        for setting_name in Realm.REALM_PERMISSION_GROUP_SETTINGS:
            setting_group_id = getattr(realm, setting_name + "_id")
            state["realm_" + setting_name] = get_group_setting_value_for_register_api(
                setting_group_id, anonymous_group_membership_data_dict
            )

        state["realm_owner_full_content_access"] = realm.owner_full_content_access

        state["realm_create_public_stream_policy"] = (
            get_corresponding_policy_value_for_group_setting(
                realm,
                "can_create_public_channel_group",
                Realm.COMMON_POLICY_TYPES,
                realm_groups_data.system_groups_name_dict,
            )
        )
        state["realm_create_private_stream_policy"] = (
            get_corresponding_policy_value_for_group_setting(
                realm,
                "can_create_private_channel_group",
                Realm.COMMON_POLICY_TYPES,
                realm_groups_data.system_groups_name_dict,
            )
        )
        state["realm_create_web_public_stream_policy"] = (
            get_corresponding_policy_value_for_group_setting(
                realm,
                "can_create_web_public_channel_group",
                Realm.CREATE_WEB_PUBLIC_STREAM_POLICY_TYPES,
                realm_groups_data.system_groups_name_dict,
            )
        )
        state["realm_wildcard_mention_policy"] = get_corresponding_policy_value_for_group_setting(
            realm,
            "can_mention_many_users_group",
            Realm.WILDCARD_MENTION_POLICY_TYPES,
            realm_groups_data.system_groups_name_dict,
        )

        # Most state is handled via the property_types framework;
        # these manual entries are for those realm settings that don't
        # fit into that framework.
        realm_authentication_methods_dict = realm.authentication_methods_dict()
        state["realm_authentication_methods"] = (
            get_realm_authentication_methods_for_page_params_api(
                realm, realm_authentication_methods_dict
            )
        )

        # We pretend these features are disabled because anonymous
        # users can't access them.  In the future, we may want to move
        # this logic to the frontends, so that we can correctly
        # display what these fields are in the settings.
        state["realm_allow_message_editing"] = (
            False if user_profile is None else realm.allow_message_editing
        )

        # This setting determines whether to send presence and also
        # whether to display of users list in the right sidebar; we
        # want both behaviors for logged-out users.  We may in the
        # future choose to move this logic to the frontend.
        state["realm_presence_disabled"] = True if user_profile is None else realm.presence_disabled

        # Important: Encode units in the client-facing API name.
        state["max_avatar_file_size_mib"] = settings.MAX_AVATAR_FILE_SIZE_MIB
        state["max_file_upload_size_mib"] = realm.get_max_file_upload_size_mebibytes()
        state["max_icon_file_size_mib"] = settings.MAX_ICON_FILE_SIZE_MIB
        upload_quota_bytes = realm.upload_quota_bytes()
        state["realm_upload_quota_mib"] = optional_bytes_to_mib(upload_quota_bytes)

        state["realm_icon_url"] = realm_icon_url(realm)
        state["realm_icon_source"] = realm.icon_source
        add_realm_logo_fields(state, realm)

        # TODO/compatibility: realm_uri is a deprecated alias for realm_url that
        # can be removed once there are no longer clients relying on it.
        state["realm_url"] = state["realm_uri"] = realm.url
        state["realm_bot_domain"] = realm.get_bot_domain()
        state["realm_available_video_chat_providers"] = realm.get_enabled_video_chat_providers()
        state["settings_send_digest_emails"] = settings.SEND_DIGEST_EMAILS

        state["realm_digest_emails_enabled"] = (
            realm.digest_emails_enabled and settings.SEND_DIGEST_EMAILS
        )
        state["realm_email_auth_enabled"] = email_auth_enabled(
            realm, realm_authentication_methods_dict
        )
        state["realm_password_auth_enabled"] = password_auth_enabled(
            realm, realm_authentication_methods_dict
        )

        state["server_generation"] = settings.SERVER_GENERATION
        state["development_environment"] = settings.DEVELOPMENT
        state["realm_org_type"] = realm.org_type
        state["realm_plan_type"] = realm.plan_type
        state["zulip_plan_is_not_limited"] = realm.plan_type != Realm.PLAN_TYPE_LIMITED
        state["upgrade_text_for_wide_organization_logo"] = str(Realm.UPGRADE_TEXT_STANDARD)

        if realm.push_notifications_enabled_end_timestamp is not None:
            state["realm_push_notifications_enabled_end_timestamp"] = datetime_to_timestamp(
                realm.push_notifications_enabled_end_timestamp
            )
        else:
            state["realm_push_notifications_enabled_end_timestamp"] = None

        state["password_min_length"] = settings.PASSWORD_MIN_LENGTH
        state["password_max_length"] = settings.PASSWORD_MAX_LENGTH
        state["password_min_guesses"] = settings.PASSWORD_MIN_GUESSES
        state["server_inline_image_preview"] = settings.INLINE_IMAGE_PREVIEW
        state["server_inline_url_embed_preview"] = settings.INLINE_URL_EMBED_PREVIEW
        state["server_thumbnail_formats"] = [
            {
                "name": str(thumbnail_format),
                "max_width": thumbnail_format.max_width,
                "max_height": thumbnail_format.max_height,
                "format": thumbnail_format.extension,
                "animated": thumbnail_format.animated,
            }
            for thumbnail_format in THUMBNAIL_OUTPUT_FORMATS
        ]
        state["server_avatar_changes_disabled"] = settings.AVATAR_CHANGES_DISABLED
        state["server_name_changes_disabled"] = settings.NAME_CHANGES_DISABLED
        state["server_web_public_streams_enabled"] = settings.WEB_PUBLIC_STREAMS_ENABLED
        state["gif_rating_options"] = realm.get_gif_rating_options()

        state["server_emoji_data_url"] = emoji.data_url()

        state["server_needs_upgrade"] = is_outdated_server(user_profile)
        state["event_queue_longpoll_timeout_seconds"] = (
            settings.EVENT_QUEUE_LONGPOLL_TIMEOUT_SECONDS
        )

        # TODO: This probably belongs on the server object.
        state["realm_default_external_accounts"] = get_default_external_accounts()

        server_default_jitsi_server_url = (
            settings.JITSI_SERVER_URL.rstrip("/") if settings.JITSI_SERVER_URL is not None else None
        )
        state["server_jitsi_server_url"] = server_default_jitsi_server_url
        state["jitsi_server_url"] = (
            realm.jitsi_server_url
            if realm.jitsi_server_url is not None
            else server_default_jitsi_server_url
        )

        state["server_can_summarize_topics"] = settings.TOPIC_SUMMARIZATION_MODEL is not None

        for channel_field in [
            "moderation_request_channel_id",
            "new_stream_announcements_stream_id",
            "signup_announcements_stream_id",
            "zulip_update_announcements_stream_id",
        ]:
            if getattr(realm, channel_field) is None:
                state["realm_" + channel_field] = -1
            else:
                state["realm_" + channel_field] = getattr(realm, channel_field)

        state["max_stream_name_length"] = Stream.MAX_NAME_LENGTH
        state["max_stream_description_length"] = Stream.MAX_DESCRIPTION_LENGTH
        state["max_bulk_new_subscription_messages"] = settings.MAX_BULK_NEW_SUBSCRIPTION_MESSAGES
        state["max_topic_length"] = MAX_TOPIC_NAME_LENGTH
        state["max_message_length"] = settings.MAX_MESSAGE_LENGTH
        state["max_channel_folder_name_length"] = ChannelFolder.MAX_NAME_LENGTH
        state["max_channel_folder_description_length"] = ChannelFolder.MAX_DESCRIPTION_LENGTH
        state["max_reminder_note_length"] = settings.MAX_REMINDER_NOTE_LENGTH
        if realm.demo_organization_scheduled_deletion_date is not None:
            state["demo_organization_scheduled_deletion_date"] = datetime_to_timestamp(
                realm.demo_organization_scheduled_deletion_date
            )
        state["realm_date_created"] = datetime_to_timestamp(realm.date_created)

        state["server_report_message_types"] = [
            {"key": type_id, "name": str(type_name)}
            for type_id, type_name in Realm.REPORT_MESSAGE_REASONS.items()
        ]

        # Presence system parameters for client behavior.
        state["server_presence_ping_interval_seconds"] = settings.PRESENCE_PING_INTERVAL_SECS
        state["server_presence_offline_threshold_seconds"] = settings.OFFLINE_THRESHOLD_SECS
        # Typing notifications protocol parameters for client behavior.
        state["server_typing_started_expiry_period_milliseconds"] = (
            settings.TYPING_STARTED_EXPIRY_PERIOD_MILLISECONDS
        )
        state["server_typing_stopped_wait_period_milliseconds"] = (
            settings.TYPING_STOPPED_WAIT_PERIOD_MILLISECONDS
        )
        state["server_typing_started_wait_period_milliseconds"] = (
            settings.TYPING_STARTED_WAIT_PERIOD_MILLISECONDS
        )

        state["server_supported_permission_settings"] = get_server_supported_permission_settings()

        state["server_min_deactivated_realm_deletion_days"] = (
            settings.MIN_DEACTIVATED_REALM_DELETION_DAYS
        )
        state["server_max_deactivated_realm_deletion_days"] = (
            settings.MAX_DEACTIVATED_REALM_DELETION_DAYS
        )

        state["realm_empty_topic_display_name"] = Message.EMPTY_TOPIC_FALLBACK_NAME

        state["realm_allow_edit_history"] = (
            realm.message_edit_history_visibility_policy
            != MessageEditHistoryVisibilityPolicyEnum.none.value
        )

        state["realm_message_edit_history_visibility_policy"] = (
            MessageEditHistoryVisibilityPolicyEnum(
                realm.message_edit_history_visibility_policy
            ).name
        )

        state["realm_topics_policy"] = RealmTopicsPolicyEnum(realm.topics_policy).name

        state["realm_mandatory_topics"] = (
            realm.topics_policy == RealmTopicsPolicyEnum.disable_empty_topic.value
        )

    if want("realm_user_settings_defaults"):
        realm_user_default = RealmUserDefault.objects.get(realm=realm)
        state["realm_user_settings_defaults"] = {}
        for property_name in RealmUserDefault.property_types:
            state["realm_user_settings_defaults"][property_name] = getattr(
                realm_user_default, property_name
            )

        state["realm_user_settings_defaults"]["emojiset_choices"] = (
            RealmUserDefault.emojiset_choices()
        )
        state["realm_user_settings_defaults"]["available_notification_sounds"] = (
            get_available_notification_sounds()
        )
        state["realm_user_settings_defaults"]["resolved_topic_notice_auto_read_policy"] = (
            ResolvedTopicNoticeAutoReadPolicyEnum(
                realm_user_default.resolved_topic_notice_auto_read_policy
            ).name
        )

    if want("realm_domains"):
        state["realm_domains"] = get_realm_domains(realm)

    if want("realm_emoji"):
        state["realm_emoji"] = get_all_custom_emoji_for_realm(realm.id)

    if want("realm_linkifiers"):
        if linkifier_url_template:
            state["realm_linkifiers"] = linkifiers_for_realm(realm.id)
        else:
            # When URL template is not supported by the client, return an empty list
            # because the new format is incompatible with the old URL format strings
            # and the client would not render it properly.
            state["realm_linkifiers"] = []

    # Backwards compatibility code.
    if want("realm_filters"):
        # Always return an empty list because the new URL template format is incompatible
        # with the old URL format string, because legacy clients that use the
        # backwards-compatible `realm_filters` event would not render the it properly.
        state["realm_filters"] = []

    if want("realm_playgrounds"):
        state["realm_playgrounds"] = get_realm_playgrounds(realm)

    if want("realm_billing"):
        state["realm_billing"] = {}
        user_has_billing_access = (
            realm.can_manage_billing_group_id in settings_user_recursive_group_ids
        )
        state["realm_billing"]["has_pending_sponsorship_request"] = has_pending_sponsorship_request(
            settings_user, user_has_billing_access
        )

    if want("realm_user"):
        state["raw_users"] = get_users_for_api(
            realm,
            user_profile,
            client_gravatar=client_gravatar,
            user_avatar_url_field_optional=user_avatar_url_field_optional,
            # Don't send custom profile field values to spectators.
            include_custom_profile_fields=user_profile is not None,
            user_list_incomplete=user_list_incomplete,
        )
        state["cross_realm_bots"] = list(get_cross_realm_dicts())

        # For the user's own avatar URL, we force
        # client_gravatar=False, since that saves some unnecessary
        # client-side code for handing medium-size avatars.  See #8253
        # for details.
        state["avatar_source"] = settings_user.avatar_source
        state["avatar_url_medium"] = avatar_url(
            settings_user,
            medium=True,
            client_gravatar=False,
        )
        state["avatar_url"] = avatar_url(
            settings_user,
            medium=False,
            client_gravatar=False,
        )

        state["can_create_private_streams"] = (
            realm.can_create_private_channel_group_id in settings_user_recursive_group_ids
        )
        state["can_create_public_streams"] = (
            realm.can_create_public_channel_group_id in settings_user_recursive_group_ids
        )

        state["can_create_web_public_streams"] = (
            realm.can_create_web_public_channel_group_id in settings_user_recursive_group_ids
        )
        # TODO/compatibility: Deprecated in Zulip 5.0 (feature level
        # 102); we can remove this once we no longer need to support
        # legacy mobile app versions that read the old property.
        state["can_create_streams"] = (
            state["can_create_private_streams"]
            or state["can_create_public_streams"]
            or state["can_create_web_public_streams"]
        )
        state["can_invite_others_to_realm"] = (
            realm.can_invite_users_group_id in settings_user_recursive_group_ids
        )
        state["is_admin"] = settings_user.is_realm_admin
        state["is_owner"] = settings_user.is_realm_owner
        state["is_moderator"] = settings_user.is_moderator
        state["is_guest"] = settings_user.is_guest
        state["user_id"] = settings_user.id
        state["email"] = settings_user.email
        state["delivery_email"] = settings_user.delivery_email
        state["full_name"] = settings_user.full_name

    if want("realm_bot"):
        state["realm_bots"] = [] if user_profile is None else get_owned_bot_dicts(user_profile)

    # This does not yet have an apply_event counterpart, since currently,
    # new entries for EMBEDDED_BOTS can only be added directly in the codebase.
    if want("realm_embedded_bots"):
        state["realm_embedded_bots"] = [
            {"name": bot.name, "config": load_bot_config_template(bot.name)}
            for bot in EMBEDDED_BOTS
        ]

