{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE RecordWildCards #-}

-- This file is part of the Wire Server implementation.
--
-- Copyright (C) 2025 Wire Swiss GmbH <opensource@wire.com>
--
-- This program is free software: you can redistribute it and/or modify it under
-- the terms of the GNU Affero General Public License as published by the Free
-- Software Foundation, either version 3 of the License, or (at your option) any
-- later version.
--
-- This program is distributed in the hope that it will be useful, but WITHOUT
-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
-- details.
--
-- You should have received a copy of the GNU Affero General Public License along
-- with this program. If not, see <https://www.gnu.org/licenses/>.

module Wire.ConversationSubsystem.Update
  ( -- * Managing Conversations
    acceptConv,
    blockConv,
    blockConvUnqualified,
    unblockConv,
    unblockConvUnqualified,
    checkReusableCode,
    joinConversationByReusableCode,
    joinConversationById,
    addCodeUnqualified,
    rmCodeUnqualified,
    getCode,
    updateConversationName,
    updateConversationReceiptMode,
    updateConversationMessageTimer,
    updateConversationAccess,
    updateConversationHistory,
    updateChannelAddPermission,
    deleteLocalConversation,
    updateRemoteConversation,
    updateConversationProtocolWithLocalUser,
    updateLocalStateOfRemoteConv,
    updateCellsState,
    adminlessAutopromoteOrDelete,
    adminlessAutopromoteOrSendReminder,
    setupAdminlessGroupsCleanup,

    -- * Managing Members
    addQualifiedMembersUnqualified,
    addMembers,
    replaceMembers,
    updateSelfMember,
    updateOtherMember,
    eligibleAdminFallbackMembers,
    isLeavingLastConversationAdmin,
    guardPreventAdminlessGroups,
    guardPreventAdminlessGroupsFor,
    removeMemberQualified,
    deleteUserFromTeamConversationsImpl,
    removeMemberFromLocalConv,
    removeMemberFromRemoteConv,

    -- * Talking
    postProteusMessage,
    postOtrMessageUnqualified,
    postProteusBroadcast,
    postOtrBroadcastUnqualified,
    memberTyping,

    -- * External Services
    addBot,
    rmBot,
    postBotMessageUnqualified,
  )
where

import Control.Error.Util (hush)
import Control.Lens
import Data.Bits
import Data.ByteString qualified as BS
import Data.Code
import Data.Default
import Data.Id
import Data.Json.Util
import Data.List.NonEmpty (NonEmpty (..), appendList, nonEmpty)
import Data.Map.Strict qualified as Map
import Data.Misc
import Data.Qualified
import Data.Set qualified as Set
import Data.Singletons
import Data.Time.Clock (NominalDiffTime, addUTCTime)
import Data.Vector qualified as V
import Galley.Types.Error
import Imports hiding (forkIO)
import Polysemy
import Polysemy.Error
import Polysemy.Input
import Polysemy.TinyLog
import Wire.API.Bot hiding (addBot)
import Wire.API.Conversation hiding (Member)
import Wire.API.Conversation.Action
import Wire.API.Conversation.CellsState
import Wire.API.Conversation.Code
import Wire.API.Conversation.Config (ConversationSubsystemConfig)
import Wire.API.Conversation.Protocol qualified as P
import Wire.API.Conversation.Role
import Wire.API.Conversation.Typing
import Wire.API.Error
import Wire.API.Error.Galley
import Wire.API.Event.Conversation
import Wire.API.Event.LeaveReason
import Wire.API.Federation.API
import Wire.API.Federation.API.Galley
import Wire.API.Federation.Client (FederatorClient)
import Wire.API.Federation.Error
import Wire.API.Message
import Wire.API.Routes.Public (ZHostValue)
import Wire.API.Routes.Public.Galley.Messaging
import Wire.API.Routes.Public.Util (UpdateResult (..))
import Wire.API.ServantProto (RawProto (..))
import Wire.API.Team.Feature
import Wire.API.Team.FeatureFlags (FanoutLimit, FeatureFlags)
import Wire.API.Team.Member
import Wire.API.User qualified as User
import Wire.API.User.Client
import Wire.API.UserGroup
import Wire.BackendNotificationQueueAccess
import Wire.BrigAPIAccess (BrigAPIAccess)
import Wire.BrigAPIAccess qualified as Brig
import Wire.CodeStore (CodeStore)
import Wire.CodeStore qualified as E
import Wire.CodeStore.Code
import Wire.ConversationStore (ConversationStore)
import Wire.ConversationStore qualified as E
import Wire.ConversationSubsystem (RemoveMemberResponseMode (..))
import Wire.ConversationSubsystem.Action
import Wire.ConversationSubsystem.Action.Kick (kickMember)
import Wire.ConversationSubsystem.AdminlessGroups (selectAutopromotionCandidate)
import Wire.ConversationSubsystem.Message
import Wire.ConversationSubsystem.Notify qualified as Notify
import Wire.ConversationSubsystem.Query qualified as Query
import Wire.ConversationSubsystem.Util
import Wire.ExternalAccess qualified as E
import Wire.FeaturesConfigSubsystem
import Wire.FederationAPIAccess qualified as E
import Wire.FederationSubsystem
import Wire.FireAndForget
import Wire.HashPassword as HashPassword
import Wire.JobSubsystem (JobSubsystem, scheduleAdminlessDeletionJob, scheduleAdminlessReminderJob)
import Wire.LegalHoldStore (LegalHoldStore)
import Wire.NotificationSubsystem
import Wire.Options.Galley
import Wire.ProposalStore (ProposalStore)
import Wire.RateLimit
import Wire.Sem.Now (Now)
import Wire.Sem.Now qualified as Now
import Wire.Sem.Random (Random)
import Wire.Sem.Random qualified as Random
import Wire.StoredConversation
import Wire.TeamCollaboratorsSubsystem
import Wire.TeamStore
import Wire.TeamSubsystem (TeamSubsystem, permissionCheck)
import Wire.TeamSubsystem qualified as TeamSubsystem
import Wire.UserClientIndexStore qualified as E
import Wire.UserGroupStore (UserGroupStore, getUserGroupsForConv)
import Wire.UserList
import Wire.Util

acceptConv ::
  ( Member ConversationStore r,
    Member (Error InternalError) r,
    Member (ErrorS 'ConvNotFound) r,
    Member (ErrorS 'InvalidOperation) r,
    Member NotificationSubsystem r,
    Member Now r,
    Member TinyLog r
  ) =>
  Local UserId ->
  Maybe ConnId ->
  ConvId ->
  Sem r (OwnConversation GroupConvType)
acceptConv lusr conn cnv = do
  conv <-
    E.getConversation cnv >>= noteS @'ConvNotFound
  conv' <- acceptOne2One lusr conv conn
  maybe (throwIfNotOwnConversation lusr cnv) pure $ ownConversationView lusr conv'

blockConv ::
  ( Member ConversationStore r,
    Member (ErrorS 'ConvNotFound) r,
    Member (ErrorS 'InvalidOperation) r
  ) =>
  Local UserId ->
  Qualified ConvId ->
  Sem r ()
blockConv lusr qcnv =
  foldQualified
    lusr
    (blockConvUnqualified (tUnqualified lusr) . tUnqualified)
    (blockRemoteConv lusr)
    qcnv

blockConvUnqualified ::
  ( Member ConversationStore r,
    Member (ErrorS 'ConvNotFound) r,
    Member (ErrorS 'InvalidOperation) r
  ) =>
  UserId ->
  ConvId ->
  Sem r ()
blockConvUnqualified zusr cnv = do
  conv <- E.getConversation cnv >>= noteS @'ConvNotFound
  unless (convType conv `elem` [ConnectConv, One2OneConv]) $
    throwS @'InvalidOperation
  let mems = conv.localMembers
  when (zusr `isMember` mems) $
    E.deleteMembers cnv (UserList [zusr] [])

blockRemoteConv ::
  ( Member (ErrorS 'ConvNotFound) r,
    Member ConversationStore r
  ) =>
  Local UserId ->
  Remote ConvId ->
  Sem r ()
blockRemoteConv (tUnqualified -> usr) rcnv = do
  unlessM (E.checkLocalMemberRemoteConv usr rcnv) $ throwS @'ConvNotFound
  E.deleteMembersInRemoteConversation rcnv [usr]

unblockConv ::
  ( Member ConversationStore r,
    Member (Error InternalError) r,
    Member (ErrorS 'ConvNotFound) r,
    Member (ErrorS 'InvalidOperation) r,
    Member NotificationSubsystem r,
    Member Now r,
    Member TinyLog r
  ) =>
  Local UserId ->
  Maybe ConnId ->
  Qualified ConvId ->
  Sem r ()
unblockConv lusr conn =
  foldQualified
    lusr
    (void . unblockConvUnqualified lusr conn . tUnqualified)
    (unblockRemoteConv lusr)

unblockConvUnqualified ::
  ( Member ConversationStore r,
    Member (Error InternalError) r,
    Member (ErrorS 'ConvNotFound) r,
    Member (ErrorS 'InvalidOperation) r,
    Member NotificationSubsystem r,
    Member Now r,
    Member TinyLog r
  ) =>
  Local UserId ->
  Maybe ConnId ->
  ConvId ->
  Sem r (OwnConversation GroupConvType)
unblockConvUnqualified lusr conn cnv = do
  conv <-
    E.getConversation cnv >>= noteS @'ConvNotFound
  unless (convType conv `elem` [ConnectConv, One2OneConv]) $
    throwS @'InvalidOperation
  conv' <- acceptOne2One lusr conv conn
  maybe (throwIfNotOwnConversation lusr cnv) pure $ ownConversationView lusr conv'

unblockRemoteConv ::
  (Member ConversationStore r) =>
  Local UserId ->
  Remote ConvId ->
  Sem r ()
unblockRemoteConv lusr rcnv = do
  E.upsertMembersInRemoteConversation rcnv [tUnqualified lusr]

-- conversation updates

type UpdateConversationAccessEffects =
  '[ BackendNotificationQueueAccess,
     BrigAPIAccess,
     CodeStore,
     ConversationStore,
     Error FederationError,
     Error InternalError,
     Error InvalidInput,
     ErrorS ('ActionDenied 'ModifyConversationAccess),
     ErrorS ('ActionDenied 'RemoveConversationMember),
     ErrorS 'ConvNotFound,
     ErrorS 'InvalidOperation,
     ErrorS 'InvalidTargetAccess,
     E.ExternalAccess,
     E.FederationAPIAccess FederatorClient,
     FireAndForget,
     NotificationSubsystem,
     Input ConversationSubsystemConfig,
     ProposalStore,
     Random,
     TeamStore,
     TinyLog
   ]

updateConversationAccess ::
  ( Members UpdateConversationAccessEffects r,
    Member Now r,
    Member TeamSubsystem r
  ) =>
  Local UserId ->
  ConnId ->
  Qualified ConvId ->
  ConversationAccessData ->
  Sem r (UpdateResult Event)
updateConversationAccess lusr con qcnv update = do
  lcnv <- ensureLocal lusr qcnv
  getUpdateResult . fmap lcuEvent $
    updateLocalConversationAccessData lcnv (tUntagged lusr) (Just con) update

updateConversationHistory ::
  ( Member (Error FederationError) r,
    Member (ErrorS (ActionDenied ModifyConversationAccess)) r,
    Member (ErrorS InvalidOperation) r,
    Member (ErrorS ConvNotFound) r,
    Member (ErrorS HistoryNotSupported) r,
    Member ConversationStore r,
    Member NotificationSubsystem r,
    Member E.ExternalAccess r,
    Member BackendNotificationQueueAccess r,
    Member Now r,
    Member TeamSubsystem r
  ) =>
  Local UserId ->
  ConnId ->
  Qualified ConvId ->
  ConversationHistoryUpdate ->
  Sem r (UpdateResult Event)
updateConversationHistory lusr con qcnv update = do
  lcnv <- ensureLocal lusr qcnv
  getUpdateResult . fmap lcuEvent $
    updateLocalConversationHistoryUpdate
      lcnv
      (tUntagged lusr)
      (Just con)
      update.history

updateConversationReceiptMode ::
  ( Member BrigAPIAccess r,
    Member ConversationStore r,
    Member (Error FederationError) r,
    Member (Error InternalError) r,
    Member (ErrorS ('ActionDenied 'ModifyConversationReceiptMode)) r,
    Member (ErrorS 'ConvNotFound) r,
    Member (ErrorS 'InvalidOperation) r,
    Member (ErrorS 'MLSReadReceiptsNotAllowed) r,
    Member E.ExternalAccess r,
    Member BackendNotificationQueueAccess r,
    Member Now r,
    Member (E.FederationAPIAccess FederatorClient) r,
    Member NotificationSubsystem r,
    Member (Input (Local ())) r,
    Member TinyLog r,
    Member TeamSubsystem r
  ) =>
  Local UserId ->
  ConnId ->
  Qualified ConvId ->
  ConversationReceiptModeUpdate ->
  Sem r (UpdateResult Event)
updateConversationReceiptMode lusr zcon qcnv update =
  mapError @UnreachableBackends @InternalError (\_ -> InternalErrorWithDescription "Unexpected UnreachableBackends error when updating remote receipt mode")
    . mapError @NonFederatingBackends @InternalError (\_ -> InternalErrorWithDescription "Unexpected NonFederatingBackends error when updating remote receipt mode")
    $ foldQualified
      lusr
      ( \lcnv ->
          getUpdateResult . fmap lcuEvent $
            updateLocalConversationReceiptModeUpdate
              lcnv
              (tUntagged lusr)
              (Just zcon)
              update
      )
      (\rcnv -> updateRemoteConversation @'ConversationReceiptModeUpdateTag rcnv lusr (Just zcon) update)
      qcnv

updateRemoteConversation ::
  forall tag r.
  ( Member BrigAPIAccess r,
    Member E.ExternalAccess r,
    Member (E.FederationAPIAccess FederatorClient) r,
    Member NotificationSubsystem r,
    Member (Input (Local ())) r,
    Member ConversationStore r,
    Member TinyLog r,
    RethrowErrors (HasConversationActionGalleyErrors tag) r,
    Member (Error NonFederatingBackends) r,
    Member (Error UnreachableBackends) r,
    Member (Error FederationError) r,
    SingI tag
  ) =>
  Remote ConvId ->
  Local UserId ->
  Maybe ConnId ->
  ConversationAction tag ->
  Sem r (UpdateResult Event)
updateRemoteConversation rcnv lusr mconn action = getUpdateResult $ do
  let updateRequest =
        ConversationUpdateRequest
          { user = tUnqualified lusr,
            convId = tUnqualified rcnv,
            action = SomeConversationAction (sing @tag) action
          }
  eResponse <- E.runFederatedEither rcnv (fedClient @'Galley @"update-conversation" updateRequest)
  response <- case eResponse of
    Left e -> throw e
    Right x -> pure x
  convUpdate <- case response of
    ConversationUpdateResponseNoChanges -> throw NoChanges
    ConversationUpdateResponseError err' -> raise $ rethrowErrors @(HasConversationActionGalleyErrors tag) err'
    ConversationUpdateResponseUpdate convUpdate -> pure convUpdate
    ConversationUpdateResponseNonFederatingBackends e -> throw e
    ConversationUpdateResponseUnreachableBackends e -> throw e
  updateLocalStateOfRemoteConv (qualifyAs rcnv convUpdate) mconn >>= note NoChanges

updateConversationMessageTimer ::
  ( Member ConversationStore r,
    Member (ErrorS ('ActionDenied 'ModifyConversationMessageTimer)) r,
    Member (ErrorS 'ConvNotFound) r,
    Member (ErrorS 'InvalidOperation) r,
    Member (Error FederationError) r,
    Member NotificationSubsystem r,
    Member E.ExternalAccess r,
    Member Now r,
    Member BackendNotificationQueueAccess r,
    Member TeamSubsystem r
  ) =>
  Local UserId ->
  ConnId ->
  Qualified ConvId ->
  ConversationMessageTimerUpdate ->
  Sem r (UpdateResult Event)
updateConversationMessageTimer lusr zcon qcnv update =
  getUpdateResult $
    foldQualified
      lusr
      ( \lcnv ->
          lcuEvent
            <$> updateLocalConversationMessageTimerUpdate
              lcnv
              (tUntagged lusr)
              (Just zcon)
              update
      )
      (\_ -> throw FederationNotImplemented)
      qcnv

deleteLocalConversation ::
  ( Member CodeStore r,
    Member ConversationStore r,
    Member (Error FederationError) r,
    Member (ErrorS 'NotATeamMember) r,
    Member (ErrorS ('ActionDenied 'DeleteConversation)) r,
    Member (ErrorS 'ConvNotFound) r,
    Member (ErrorS 'InvalidOperation) r,
    Member NotificationSubsystem r,
    Member E.ExternalAccess r,
    Member Now r,
    Member BackendNotificationQueueAccess r,
    Member ProposalStore r,
    Member TeamSubsystem r
  ) =>
  Local UserId ->
  ConnId ->
  Local ConvId ->
  Sem r (UpdateResult Event)
deleteLocalConversation lusr con lcnv =
  getUpdateResult . fmap lcuEvent $
    updateLocalConversationDelete lcnv (tUntagged lusr) (Just con)

addCodeUnqualified ::
  forall r.
  ( Member CodeStore r,
    Member ConversationStore r,
    Member (ErrorS 'ConvAccessDenied) r,
    Member (ErrorS 'ConvNotFound) r,
    Member (ErrorS 'GuestLinksDisabled) r,
    Member (ErrorS 'CreateConversationCodeConflict) r,
    Member E.ExternalAccess r,
    Member NotificationSubsystem r,
    Member (Input (Local ())) r,
    Member Now r,
    Member (Input (Maybe GuestLinkTTLSeconds)) r,
    Member HashPassword r,
    Member FeaturesConfigSubsystem r,
    Member RateLimit r,
    Member TeamSubsystem r
  ) =>
  Maybe CreateConversationCodeRequest ->
  UserId ->
  Maybe ZHostValue ->
  Maybe ConnId ->
  ConvId ->
  Sem r AddCodeResult
addCodeUnqualified mReq usr mbZHost mZcon cnv = do
  lusr <- qualifyLocal usr
  lcnv <- qualifyLocal cnv
  addCode lusr mbZHost mZcon lcnv mReq

addCode ::
  forall r.
  ( Member CodeStore r,
    Member ConversationStore r,
    Member (ErrorS 'ConvNotFound) r,
    Member (ErrorS 'ConvAccessDenied) r,
    Member (ErrorS 'GuestLinksDisabled) r,
    Member (ErrorS 'CreateConversationCodeConflict) r,
    Member E.ExternalAccess r,
    Member HashPassword r,
    Member NotificationSubsystem r,
    Member Now r,
    Member (Input (Maybe GuestLinkTTLSeconds)) r,
    Member FeaturesConfigSubsystem r,
    Member RateLimit r,
    Member TeamSubsystem r
  ) =>
  Local UserId ->
  Maybe ZHostValue ->
  Maybe ConnId ->
  Local ConvId ->
  Maybe CreateConversationCodeRequest ->
  Sem r AddCodeResult
addCode lusr mbZHost mZcon lcnv mReq = do
  conv <- E.getConversation (tUnqualified lcnv) >>= noteS @'ConvNotFound
  Query.ensureGuestLinksEnabled (convTeam conv)
  mTeamMember <- maybe (pure Nothing) (TeamSubsystem.internalGetTeamMember (tUnqualified lusr)) conv.metadata.cnvmTeam
  Query.ensureConvAdmin conv (tUnqualified lusr) mTeamMember
  ensureAccess conv CodeAccess
  ensureGuestsOrNonTeamMembersAllowed conv
  convUri <- getConversationCodeURI mbZHost
  key <- E.makeKey (CodeReferentConv (tUnqualified lcnv))
  E.getCode key >>= \case
    Nothing -> do
      ttl <- inputs (realToFrac . unGuestLinkTTLSeconds . fromMaybe defGuestLinkTTLSeconds)
      code <- E.generateCode (CodeReferentConv (tUnqualified lcnv)) (Timeout ttl)
      mPw <- for (mReq >>= (.password)) $ HashPassword.hashPassword8 (RateLimitUser (tUnqualified lusr))
      E.createCode code mPw
      now <- Now.get
      let event = Event (tUntagged lcnv) Nothing (EventFromUser (tUntagged lusr)) now Nothing (EdConvCodeUpdate (mkConversationCodeInfo (isJust mPw) (codeKey code) (codeValue code) convUri))
      let (bots, users) = localBotsAndUsers $ conv.localMembers
      pushConversationEvent mZcon conv event (qualifyAs lusr (map (.id_) users)) bots
      pure $ CodeAdded event
    -- In case conversation already has a code this case covers the allowed no-ops
    Just (code, mPw) -> do
      when (isJust mPw || isJust (mReq >>= (.password))) $ throwS @'CreateConversationCodeConflict
      pure $ CodeAlreadyExisted (mkConversationCodeInfo (isJust mPw) (codeKey code) (codeValue code) convUri)
  where
    ensureGuestsOrNonTeamMembersAllowed :: StoredConversation -> Sem r ()
    ensureGuestsOrNonTeamMembersAllowed conv =
      unless
        ( GuestAccessRole `Set.member` convAccessRoles conv
            || NonTeamMemberAccessRole `Set.member` convAccessRoles conv
        )
        $ throwS @'ConvAccessDenied

rmCodeUnqualified ::
  ( Member CodeStore r,
    Member ConversationStore r,
    Member (ErrorS 'ConvNotFound) r,
    Member (ErrorS 'ConvAccessDenied) r,
    Member E.ExternalAccess r,
    Member NotificationSubsystem r,
    Member (Input (Local ())) r,
    Member Now r,
    Member TeamSubsystem r
  ) =>
  Local UserId ->
  ConnId ->
  ConvId ->
  Sem r Event
rmCodeUnqualified lusr zcon cnv = do
  lcnv <- qualifyLocal cnv
  rmCode lusr zcon lcnv

rmCode ::
  ( Member CodeStore r,
    Member ConversationStore r,
    Member (ErrorS 'ConvAccessDenied) r,
    Member (ErrorS 'ConvNotFound) r,
    Member E.ExternalAccess r,
    Member NotificationSubsystem r,
    Member Now r,
    Member TeamSubsystem r
  ) =>
  Local UserId ->
  ConnId ->
  Local ConvId ->
  Sem r Event
rmCode lusr zcon lcnv = do
  conv <-
    E.getConversation (tUnqualified lcnv) >>= noteS @'ConvNotFound
  mTeamMember <- maybe (pure Nothing) (TeamSubsystem.internalGetTeamMember (tUnqualified lusr)) conv.metadata.cnvmTeam
  Query.ensureConvAdmin conv (tUnqualified lusr) mTeamMember
  ensureAccess conv CodeAccess
  let (bots, users) = localBotsAndUsers $ conv.localMembers
  key <- E.makeKey (CodeReferentConv (tUnqualified lcnv))
  E.deleteCode key
  now <- Now.get
  let event = Event (tUntagged lcnv) Nothing (EventFromUser (tUntagged lusr)) now Nothing EdConvCodeDelete
  pushConversationEvent (Just zcon) conv event (qualifyAs lusr (map (.id_) users)) bots
  pure event

getCode ::
  forall r.
  ( Member CodeStore r,
    Member ConversationStore r,
    Member (ErrorS 'CodeNotFound) r,
    Member (ErrorS 'ConvAccessDenied) r,
    Member (ErrorS 'ConvNotFound) r,
    Member (ErrorS 'GuestLinksDisabled) r,
    Member FeaturesConfigSubsystem r
  ) =>
  Maybe ZHostValue ->
  Local UserId ->
  ConvId ->
  Sem r ConversationCodeInfo
getCode mbZHost lusr cnv = do
  conv <-
    E.getConversation cnv >>= noteS @'ConvNotFound
  Query.ensureGuestLinksEnabled (convTeam conv)
  ensureAccess conv CodeAccess
  ensureConvMember (conv.localMembers) (tUnqualified lusr)
  key <- E.makeKey (CodeReferentConv cnv)
  (c, mPw) <- E.getCode key >>= noteS @'CodeNotFound
  convUri <- getConversationCodeURI mbZHost
  pure $ mkConversationCodeInfo (isJust mPw) (codeKey c) (codeValue c) convUri

checkReusableCode ::
  forall r.
  ( Member CodeStore r,
    Member ConversationStore r,
    Member FeaturesConfigSubsystem r,
    Member (ErrorS 'CodeNotFound) r,
    Member (ErrorS 'ConvNotFound) r,
    Member (ErrorS 'InvalidConversationPassword) r,
    Member HashPassword r,
    Member RateLimit r
  ) =>
  IpAddr ->
  ConversationCode ->
  Sem r ()
checkReusableCode origIp convCode = do
  code <- verifyReusableCode (RateLimitIp origIp) False Nothing convCode
  codeCid <- noteS @'ConvNotFound (codeConvId code)
  conv <- E.getConversation codeCid >>= noteS @'ConvNotFound
  mapErrorS @'GuestLinksDisabled @'CodeNotFound $
    Query.ensureGuestLinksEnabled (convTeam conv)

updateConversationProtocolWithLocalUser ::
  forall r.
  ( Member (ErrorS 'ConvNotFound) r,
    Member (ErrorS 'ConvInvalidProtocolTransition) r,
    Member (ErrorS ('ActionDenied 'LeaveConversation)) r,
    Member (ErrorS 'InvalidOperation) r,
    Member (Error FederationError) r,
    Member (ErrorS 'MLSMigrationCriteriaNotSatisfied) r,
    Member (Error InternalError) r,
    Member Now r,
    Member (Input (Local ())) r,
    Member BackendNotificationQueueAccess r,
    Member BrigAPIAccess r,
    Member ConversationStore r,
    Member TinyLog r,
    Member NotificationSubsystem r,
    Member E.ExternalAccess r,
    Member (E.FederationAPIAccess FederatorClient) r,
    Member Random r,
    Member ProposalStore r,
    Member FeaturesConfigSubsystem r,
    Member TeamSubsystem r,
    Member (Input ConversationSubsystemConfig) r
  ) =>
  Local UserId ->
  ConnId ->
  Qualified ConvId ->
  P.ProtocolUpdate ->
  Sem r (UpdateResult Event)
updateConversationProtocolWithLocalUser lusr conn qcnv (P.ProtocolUpdate newProtocol) =
  mapError @UnreachableBackends @InternalError (\_ -> InternalErrorWithDescription "Unexpected UnreachableBackends error when updating remote protocol")
    . mapError @NonFederatingBackends @InternalError (\_ -> InternalErrorWithDescription "Unexpected NonFederatingBackends error when updating remote protocol")
    $ foldQualified
      lusr
      ( \lcnv -> do
          fmap (maybe Unchanged (Updated . lcuEvent) . hush)
            . runError @NoChanges
            . updateLocalConversationUpdateProtocol lcnv (tUntagged lusr) (Just conn)
            $ newProtocol
      )
      ( \rcnv ->
          updateRemoteConversation @'ConversationUpdateProtocolTag rcnv lusr (Just conn) $
            newProtocol
      )
      qcnv

updateChannelAddPermission ::
  ( Member ConversationStore r,
    Member (ErrorS ('ActionDenied 'ModifyAddPermission)) r,
    Member (ErrorS 'ConvNotFound) r,
    Member (ErrorS 'InvalidOperation) r,
    Member (Error FederationError) r,
    Member E.ExternalAccess r,
    Member Now r,
    Member BackendNotificationQueueAccess r,
    Member NotificationSubsystem r,
    Member (Input (Local ())) r,
    Member TinyLog r,
    Member (Error NonFederatingBackends) r,
    Member (Error UnreachableBackends) r,
    Member BrigAPIAccess r,
    Member (E.FederationAPIAccess FederatorClient) r,
    Member (ErrorS 'InvalidTargetAccess) r,
    Member TeamSubsystem r
  ) =>
  Local UserId ->
  ConnId ->
  Qualified ConvId ->
  AddPermissionUpdate ->
  Sem r (UpdateResult Event)
updateChannelAddPermission lusr zcon qcnv update =
  foldQualified
    lusr
    ( \lcnv ->
        getUpdateResult $
          lcuEvent
            <$> updateLocalConversationUpdateAddPermission
              lcnv
              (tUntagged lusr)
              (Just zcon)
              update
    )
    (\rcnv -> updateRemoteConversation @'ConversationUpdateAddPermissionTag rcnv lusr (Just zcon) update)
    qcnv

joinConversationByReusableCode ::
  forall r.
  ( Member BrigAPIAccess r,
    Member CodeStore r,
    Member ConversationStore r,
    Member (ErrorS 'CodeNotFound) r,
    Member (ErrorS 'InvalidConversationPassword) r,
    Member (ErrorS 'ConvAccessDenied) r,
    Member (ErrorS 'ConvNotFound) r,
    Member (ErrorS 'GuestLinksDisabled) r,
    Member (ErrorS 'InvalidOperation) r,
    Member (ErrorS 'NotATeamMember) r,
    Member (ErrorS 'TooManyMembers) r,
    Member (Error FederationError) r,
    Member BackendNotificationQueueAccess r,
    Member NotificationSubsystem r,
    Member E.ExternalAccess r,
    Member FeaturesConfigSubsystem r,
    Member HashPassword r,
    Member RateLimit r,
    Member TeamSubsystem r,
    Member Now r,
    Member (Input ConversationSubsystemConfig) r
  ) =>
  Local UserId ->
  ConnId ->
  JoinConversationByCode ->
  Sem r (UpdateResult Event)
joinConversationByReusableCode lusr zcon req = do
  c <- verifyReusableCode (RateLimitUser (tUnqualified lusr)) True req.password req.code
  codeCid <- noteS @'ConvNotFound (codeConvId c)
  conv <- E.getConversation codeCid >>= noteS @'ConvNotFound
  Query.ensureGuestLinksEnabled (convTeam conv)
  joinConversation lusr zcon conv CodeAccess

joinConversationById ::
  forall r.
  ( Member BrigAPIAccess r,
    Member ConversationStore r,
    Member (ErrorS 'ConvAccessDenied) r,
    Member (ErrorS 'ConvNotFound) r,
    Member (ErrorS 'InvalidOperation) r,
    Member (ErrorS 'NotATeamMember) r,
    Member (ErrorS 'TooManyMembers) r,
    Member (Error FederationError) r,
    Member (Input ConversationSubsystemConfig) r,
    Member BackendNotificationQueueAccess r,
    Member NotificationSubsystem r,
    Member E.ExternalAccess r,
    Member Now r,
    Member TeamSubsystem r
  ) =>
  Local UserId ->
  ConnId ->
  ConvId ->
  Sem r (UpdateResult Event)
joinConversationById lusr zcon cnv = do
  conv <- E.getConversation cnv >>= noteS @'ConvNotFound
  joinConversation lusr zcon conv LinkAccess

joinConversation ::
  forall r.
  ( Member BrigAPIAccess r,
    Member (ErrorS 'ConvAccessDenied) r,
    Member (ErrorS 'InvalidOperation) r,
    Member (ErrorS 'NotATeamMember) r,
    Member (ErrorS 'TooManyMembers) r,
    Member (Error FederationError) r,
    Member (Input ConversationSubsystemConfig) r,
    Member BackendNotificationQueueAccess r,
    Member E.ExternalAccess r,
    Member ConversationStore r,
    Member Now r,
    Member NotificationSubsystem r,
    Member TeamSubsystem r
  ) =>
  Local UserId ->
  ConnId ->
  StoredConversation ->
  Access ->
  Sem r (UpdateResult Event)
joinConversation lusr zcon conv access = do
  let lcnv = qualifyAs lusr conv.id_
  ensureConversationAccess (tUnqualified lusr) conv access
  ensureGroupConversation conv
  -- FUTUREWORK: remote users?
  ensureMemberLimit (convProtocolTag conv) (toList $ conv.localMembers) [tUnqualified lusr]
  getUpdateResult $ do
    -- NOTE: When joining conversations, all users become members
    -- as this is our desired behavior for these types of conversations
    -- where there is no way to control who joins, etc.
    let users = filter (notIsConvMember lusr conv) [tUnqualified lusr]
    (extraTargets, action) <-
      addMembersToLocalConversation lcnv (UserList users []) roleNameWireMember InternalAdd
    lcuEvent
      <$> sendConversationActionNotifications
        (sing @'ConversationJoinTag)
        (tUntagged lusr)
        False
        (Just zcon)
        (qualifyAs lusr conv)
        (convBotsAndMembers conv <> extraTargets)
        action
        def

mkJoinType :: StoredConversation -> JoinType
mkJoinType conv =
  if conv.metadata.cnvmGroupConvType == Just Channel && isJust conv.metadata.cnvmTeam
    then ExternalAdd
    else InternalAdd

addMembers ::
  forall r.
  ( Member BackendNotificationQueueAccess r,
    Member BrigAPIAccess r,
    Member ConversationStore r,
    Member (ErrorS ('ActionDenied 'AddConversationMember)) r,
    Member (ErrorS 'ConvAccessDenied) r,
    Member (ErrorS 'ConvNotFound) r,
    Member (ErrorS 'InvalidOperation) r,
    Member (ErrorS 'NotConnected) r,
    Member (ErrorS 'NotATeamMember) r,
    Member (ErrorS 'TooManyMembers) r,
    Member (ErrorS 'MissingLegalholdConsent) r,
    Member (ErrorS 'GroupIdVersionNotSupported) r,
    Member (Error FederationError) r,
    Member (Error UnreachableBackends) r,
    Member E.ExternalAccess r,
    Member (E.FederationAPIAccess FederatorClient) r,
    Member NotificationSubsystem r,
    Member Now r,
    Member LegalHoldStore r,
    Member ProposalStore r,
    Member Random r,
    Member TeamStore r,
    Member TinyLog r,
    Member TeamCollaboratorsSubsystem r,
    Member FederationSubsystem r,
    Member TeamSubsystem r,
    Member (Input ConversationSubsystemConfig) r
  ) =>
  Local UserId ->
  ConnId ->
  Qualified ConvId ->
  InviteQualified ->
  Sem r (UpdateResult Event)
addMembers lusr zcon qcnv (InviteQualified users role) = do
  lcnv <- ensureLocal lusr qcnv
  conv <- getConversationWithError lcnv

  when (null conv.metadata.cnvmParent) $
    mapErrorS @OperationDenied @('ActionDenied 'AddConversationMember) $
      forM_ conv.metadata.cnvmTeam $ \tid -> do
        forM_ users $ \u -> do
          mTeamMembership <- TeamSubsystem.internalGetTeamMember (qUnqualified u) tid
          forM_ (mTeamMembership >>= permissionsRole . Wire.API.Team.Member.getPermissions) $
            permissionCheck JoinRegularConversations . Just

  let joinType = mkJoinType conv
      action = ConversationJoin {..}
  getUpdateResult . fmap lcuEvent $
    updateLocalConversationJoin lcnv (tUntagged lusr) (Just zcon) action

addQualifiedMembersUnqualified ::
  ( Member BackendNotificationQueueAccess r,
    Member BrigAPIAccess r,
    Member ConversationStore r,
    Member (Error FederationError) r,
    Member (ErrorS ('ActionDenied 'AddConversationMember)) r,
    Member (ErrorS 'ConvAccessDenied) r,
    Member (ErrorS 'ConvNotFound) r,
    Member (ErrorS 'InvalidOperation) r,
    Member (ErrorS 'NotConnected) r,
    Member (ErrorS 'NotATeamMember) r,
    Member (ErrorS 'TooManyMembers) r,
    Member (ErrorS 'MissingLegalholdConsent) r,
    Member (ErrorS 'GroupIdVersionNotSupported) r,
    Member (Error UnreachableBackends) r,
    Member E.ExternalAccess r,
    Member (E.FederationAPIAccess FederatorClient) r,
    Member NotificationSubsystem r,
    Member Now r,
    Member LegalHoldStore r,
    Member ProposalStore r,
    Member Random r,
    Member TeamStore r,
    Member TinyLog r,
    Member TeamCollaboratorsSubsystem r,
    Member FederationSubsystem r,
    Member TeamSubsystem r,
    Member (Input ConversationSubsystemConfig) r
  ) =>
  Local UserId ->
  ConnId ->
  ConvId ->
  InviteQualified ->
  Sem r (UpdateResult Event)
addQualifiedMembersUnqualified lusr zcon cnv (InviteQualified users role) = do
  let lcnv = qualifyAs lusr cnv
  getUpdateResult . fmap lcuEvent $
    updateLocalConversationJoin lcnv (tUntagged lusr) (Just zcon) $
      ConversationJoin users role def

-- | Replace conversation members by computing the difference between desired and
-- current members, then executing removals followed by additions within a commit
-- lock.
replaceMembers ::
  forall r.
  ( Member BackendNotificationQueueAccess r,
    Member BrigAPIAccess r,
    Member ConversationStore r,
    Member (ErrorS ('ActionDenied 'AddConversationMember)) r,
    Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r,
    Member (ErrorS ('ActionDenied 'ModifyOtherConversationMember)) r,
    Member (ErrorS 'ConvAccessDenied) r,
    Member (ErrorS 'ConvNotFound) r,
    Member (ErrorS 'InvalidOperation) r,
    Member (ErrorS 'NotConnected) r,
    Member (ErrorS 'NotATeamMember) r,
    Member (ErrorS 'TooManyMembers) r,
    Member (ErrorS 'MissingLegalholdConsent) r,
    Member (ErrorS 'GroupIdVersionNotSupported) r,
    Member (ErrorS 'ConvMemberNotFound) r,
    Member (Error AdminlessConversation) r,
    Member (Error FederationError) r,
    Member (Error UnreachableBackends) r,
    Member E.ExternalAccess r,
    Member (E.FederationAPIAccess FederatorClient) r,
    Member NotificationSubsystem r,
    Member Now r,
    Member LegalHoldStore r,
    Member ProposalStore r,
    Member Random r,
    Member TeamStore r,
    Member TinyLog r,
    Member TeamCollaboratorsSubsystem r,
    Member UserGroupStore r,
    Member FederationSubsystem r,
    Member FeaturesConfigSubsystem r,
    Member TeamSubsystem r,
    Member JobSubsystem r,
    Member (Input ConversationSubsystemConfig) r
  ) =>
  RemoveMemberResponseMode ->
  Local UserId ->
  ConnId ->
  Qualified ConvId ->
  InviteQualified ->
  Sem r ()
replaceMembers responseMode lusr zcon qcnv (InviteQualified invitedUsers role) = do
  lcnv <- ensureLocal lusr qcnv
  conv <- getConversationWithError lcnv

  -- Check team permissions for desired members
  when (null conv.metadata.cnvmParent) $
    mapErrorS @OperationDenied @('ActionDenied 'AddConversationMember) $
      forM_ conv.metadata.cnvmTeam $ \tid -> do
        forM_ invitedUsers $ \u -> do
          mTeamMembership <- TeamSubsystem.internalGetTeamMember (qUnqualified u) tid
          forM_ (mTeamMembership >>= permissionsRole . Wire.API.Team.Member.getPermissions) $
            permissionCheck JoinRegularConversations . Just

  ugs <- getUserGroupsForConv conv.id_
  -- Removals apply only to local members. Additions must account for remote
  -- members too, because re-inviting an existing remote member does not change
  -- their role and must not be treated as adding an admin.
  let currentLocalMembers = Set.fromList $ map (\m -> Qualified m.id_ (tDomain lcnv)) (toList conv.localMembers)
      currentRemoteMembers = Set.fromList $ map (tUntagged . (.id_)) conv.remoteMembers
      invitedMembersSet = Set.fromList $ toList invitedUsers
      ugMembers = concatMap (fmap (flip Qualified (tDomain lusr)) . V.toList . runIdentity . (.members)) (V.toList ugs)
      -- the invited users plus all user group members should stay
      allUsersThatShouldStay = Set.fromList $ toList $ appendList invitedUsers ugMembers
      toRemove = Set.difference currentLocalMembers allUsersThatShouldStay
      toAdd = Set.difference invitedMembersSet (currentLocalMembers <> currentRemoteMembers)
      addedAdmins = if role == roleNameWireAdmin then toAdd else Set.empty

  -- Apply the same adminless protection as DELETE to the complete removal
  -- set. The guard is a preflight so V16+ cannot partially apply a replacement
  -- before returning AdminlessConversation.
  guardPreventAdminlessGroupsFor responseMode lcnv lusr toRemove addedAdmins toAdd

  -- If both sets are empty, return Unchanged
  unless (Set.null toRemove && Set.null toAdd) $ do
    -- Add members first
    for_ (nonEmpty $ Set.toList toAdd) $ \users -> do
      let joinType = mkJoinType conv
          action = ConversationJoin {..}
      getUpdateResult . fmap lcuEvent $
        updateLocalConversationJoin lcnv (tUntagged lusr) (Just zcon) action

    -- Remove members
    for_ (nonEmpty $ Set.toList toRemove) $ \removeList -> do
      if conv.metadata.cnvmGroupConvType == Just Channel
        then do
          -- For channels, always perform removals via the channel-specific path
          -- to ensure MLS proposals are created and protocol constraints are respected.
          -- FUTUREWORK: implement bulk removal in the generic updateLocalConversation path for both regular conversations and channels
          for_ removeList $ removeMemberQualified RemoveMemberLegacyResponse lusr (Just zcon) qcnv
        else
          void . getUpdateResult . fmap lcuEvent $
            updateLocalConversationRemoveMembers
              lcnv
              (tUntagged lusr)
              (Just zcon)
              (ConversationRemoveMembers removeList EdReasonRemoved)

updateSelfMember ::
  ( Member ConversationStore r,
    Member (ErrorS 'ConvNotFound) r,
    Member E.ExternalAccess r,
    Member NotificationSubsystem r,
    Member Now r
  ) =>
  Local UserId ->
  ConnId ->
  Qualified ConvId ->
  MemberUpdate ->
  Sem r ()
updateSelfMember lusr zcon qcnv update = do
  exists <- foldQualified lusr checkLocalMembership checkRemoteMembership qcnv
  unless exists $ throwS @'ConvNotFound
  E.setSelfMember qcnv lusr update
  now <- Now.get
  let e = Event qcnv Nothing (EventFromUser (tUntagged lusr)) now Nothing (EdMemberUpdate (updateData lusr))
  pushConversationEvent (Just zcon) () e (fmap pure lusr) []
  where
    checkLocalMembership ::
      (Member ConversationStore r) =>
      Local ConvId ->
      Sem r Bool
    checkLocalMembership lcnv =
      isMember (tUnqualified lusr)
        <$> E.getLocalMembers (tUnqualified lcnv)
    checkRemoteMembership ::
      (Member ConversationStore r) =>
      Remote ConvId ->
      Sem r Bool
    checkRemoteMembership rcnv =
      isJust . Map.lookup rcnv
        <$> E.getRemoteConversationStatus (tUnqualified lusr) [rcnv]
    updateData luid =
      MemberUpdateData
        { misTarget = tUntagged luid,
          misOtrMutedStatus = mupOtrMuteStatus update,
          misOtrMutedRef = mupOtrMuteRef update,
          misOtrArchived = mupOtrArchive update,
          misOtrArchivedRef = mupOtrArchiveRef update,
          misHidden = mupHidden update,
          misHiddenRef = mupHiddenRef update,
          misConvRoleName = Nothing
        }

updateOtherMemberLocalConv ::
  ( Member ConversationStore r,
    Member (Error FederationError) r,
    Member (ErrorS ('ActionDenied 'ModifyOtherConversationMember)) r,
    Member (ErrorS 'InvalidTarget) r,
    Member (ErrorS 'InvalidOperation) r,
    Member (ErrorS 'ConvNotFound) r,
    Member (ErrorS 'ConvMemberNotFound) r,
    Member NotificationSubsystem r,
    Member BackendNotificationQueueAccess r,
    Member E.ExternalAccess r,
    Member Now r,
    Member TeamSubsystem r
  ) =>
  Local ConvId ->
  Local UserId ->
  ConnId ->
  Qualified UserId ->
  OtherMemberUpdate ->
  Sem r ()
updateOtherMemberLocalConv lcnv lusr con qvictim update = void . getUpdateResult . fmap lcuEvent $ do
  when (tUntagged lusr == qvictim) $
    throwS @'InvalidTarget
  updateLocalConversationMemberUpdate lcnv (tUntagged lusr) (Just con) $
    ConversationMemberUpdate qvictim update

updateOtherMember ::
  ( Member ConversationStore r,
    Member (Error FederationError) r,
    Member (ErrorS ('ActionDenied 'ModifyOtherConversationMember)) r,
    Member (ErrorS 'InvalidTarget) r,
    Member (ErrorS 'InvalidOperation) r,
    Member (ErrorS 'ConvNotFound) r,
    Member (ErrorS 'ConvMemberNotFound) r,
    Member NotificationSubsystem r,
    Member BackendNotificationQueueAccess r,
    Member E.ExternalAccess r,
    Member Now r,
    Member TeamSubsystem r
  ) =>
  Local UserId ->
  ConnId ->
  Qualified ConvId ->
  Qualified UserId ->
  OtherMemberUpdate ->
  Sem r ()
updateOtherMember lusr zcon qcnv qvictim update = do
  let doUpdate = foldQualified lusr updateOtherMemberLocalConv updateOtherMemberRemoteConv
  doUpdate qcnv lusr zcon qvictim update

updateOtherMemberRemoteConv ::
  (Member (Error FederationError) r) =>
  Remote ConvId ->
  Local UserId ->
  ConnId ->
  Qualified UserId ->
  OtherMemberUpdate ->
  Sem r ()
updateOtherMemberRemoteConv _ _ _ _ _ = throw FederationNotImplemented

removeMemberQualified ::
  ( Member BackendNotificationQueueAccess r,
    Member ConversationStore r,
    Member BrigAPIAccess r,
    Member (Error AdminlessConversation) r,
    Member (Error FederationError) r,
    Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r,
    Member (ErrorS ('ActionDenied 'ModifyOtherConversationMember)) r,
    Member (ErrorS 'ConvNotFound) r,
    Member (ErrorS 'InvalidOperation) r,
    Member (ErrorS 'ConvMemberNotFound) r,
    Member E.ExternalAccess r,
    Member (E.FederationAPIAccess FederatorClient) r,
    Member NotificationSubsystem r,
    Member Now r,
    Member ProposalStore r,
    Member Random r,
    Member TinyLog r,
    Member FeaturesConfigSubsystem r,
    Member TeamSubsystem r,
    Member JobSubsystem r,
    Member (Input ConversationSubsystemConfig) r
  ) =>
  RemoveMemberResponseMode ->
  Local UserId ->
  Maybe ConnId ->
  Qualified ConvId ->
  Qualified UserId ->
  Sem r (Maybe Event)
removeMemberQualified responseMode lusr con qcnv victim =
  mapErrorS @('ActionDenied 'LeaveConversation) @('ActionDenied 'RemoveConversationMember) $
    foldQualified
      lusr
      (\lcnv -> removeMemberFromLocalConv responseMode lcnv lusr con)
      (\rcnv -> removeMemberFromRemoteConv rcnv lusr)
      qcnv
      victim

isAdminlessCheckCandidate :: StoredConversation -> Bool
isAdminlessCheckCandidate conv =
  conv.metadata.cnvmType == RegularConv
    && maybe True (== GroupConversation) conv.metadata.cnvmGroupConvType

setupAdminlessGroupsCleanup ::
  ( Member ConversationStore r,
    Member (ErrorS 'ConvNotFound) r,
    Member (Error FederationError) r,
    Member BrigAPIAccess r,
    Member Random r,
    Member NotificationSubsystem r,
    Member Now r,
    Member E.ExternalAccess r,
    Member BackendNotificationQueueAccess r,
    Member FeaturesConfigSubsystem r,
    Member (Input (Local ())) r,
    Member JobSubsystem r
  ) =>
  Maybe (Local UserId) ->
  TeamId ->
  Sem r ()
setupAdminlessGroupsCleanup mUsr tid = do
  teamConvIds <- E.getTeamConversations tid
  for_ teamConvIds $ \cnv -> do
    lcnv <- qualifyLocal cnv
    adminlessTryAutopromote mUsr lcnv $ \_ feature _ -> scheduleDeletion lcnv mUsr tid feature

guardPreventAdminlessGroups ::
  ( Member ConversationStore r,
    Member (Error AdminlessConversation) r,
    Member (ErrorS 'ConvNotFound) r,
    Member (ErrorS ('ActionDenied 'ModifyOtherConversationMember)) r,
    Member (ErrorS 'InvalidOperation) r,
    Member (ErrorS 'ConvMemberNotFound) r,
    Member (Error FederationError) r,
    Member BrigAPIAccess r,
    Member Random r,
    Member NotificationSubsystem r,
    Member Now r,
    Member E.ExternalAccess r,
    Member BackendNotificationQueueAccess r,
    Member TeamSubsystem r,
    Member FeaturesConfigSubsystem r,
    Member JobSubsystem r
  ) =>
  RemoveMemberResponseMode ->
  Local ConvId ->
  Local UserId ->
  Qualified UserId ->
  Sem r ()
guardPreventAdminlessGroups responseMode lcnv lusr victim =
  guardPreventAdminlessGroupsFor responseMode lcnv lusr (Set.singleton victim) Set.empty Set.empty

guardPreventAdminlessGroupsFor ::
  ( Member ConversationStore r,
    Member (Error AdminlessConversation) r,
    Member (ErrorS 'ConvNotFound) r,
    Member (ErrorS ('ActionDenied 'ModifyOtherConversationMember)) r,
    Member (ErrorS 'InvalidOperation) r,
    Member (ErrorS 'ConvMemberNotFound) r,
    Member (Error FederationError) r,
    Member BrigAPIAccess r,
    Member Random r,
    Member NotificationSubsystem r,
    Member Now r,
    Member E.ExternalAccess r,
    Member BackendNotificationQueueAccess r,
    Member FeaturesConfigSubsystem r,
    Member TeamSubsystem r,
    Member JobSubsystem r
  ) =>
  RemoveMemberResponseMode ->
  Local ConvId ->
  Local UserId ->
  Set (Qualified UserId) ->
  Set (Qualified UserId) ->
  Set (Qualified UserId) ->
  Sem r ()
guardPreventAdminlessGroupsFor responseMode lcnv lusr victims addedAdmins addedMembers = do
  conv <- getConversationWithError lcnv
  when (isAdminlessCheckCandidate conv) $ for_ conv.metadata.cnvmTeam $ \tid -> do
    (feature :: LockableFeature PreventAdminlessGroupsConfig) <- getFeatureForTeam tid
    let victims' = Set.map qUnqualified victims
        removingLastAdmin =
          Set.null addedAdmins
            && any
              (\member -> member.convRoleName == roleNameWireAdmin && Set.member member.id_ victims')
              conv.localMembers
            && not
              ( any
                  (\member -> member.convRoleName == roleNameWireAdmin && Set.notMember member.id_ victims')
                  conv.localMembers
              )
            && not (any (\member -> member.convRoleName == roleNameWireAdmin) conv.remoteMembers)
    when (feature.status == FeatureStatusEnabled && removingLastAdmin) $ do
      let addedMembersForEligibility = case responseMode of
            RemoveMemberLegacyResponse -> Set.empty
            RemoveMemberEligibleMembersResponse -> addedMembers
      eligibleMembers <- eligibleAdminFallbackMembersFor lcnv victims' addedMembersForEligibility conv
      case (responseMode, eligibleMembers) of
        (RemoveMemberLegacyResponse, x : xs) -> do
          seed <- randomWord64
          let autopromotionCandidates = selectAutopromotionCandidate seed feature.config.promotionStrategy (x :| xs)
          for_ autopromotionCandidates $ \candidate ->
            updateLocalConversationMemberUpdate lcnv (tUntagged lusr) Nothing $
              ConversationMemberUpdate candidate (OtherMemberUpdate (Just roleNameWireAdmin))
        (RemoveMemberEligibleMembersResponse, _ : _) ->
          throw $ AdminlessConversation (fmap fst eligibleMembers)
        (RemoveMemberLegacyResponse, []) ->
          scheduleDeletion lcnv (Just lusr) tid feature
        (RemoveMemberEligibleMembersResponse, []) ->
          scheduleDeletion lcnv (Just lusr) tid feature

scheduleDeletion ::
  ( Member Now r,
    Member JobSubsystem r
  ) =>
  Local ConvId ->
  Maybe (Local UserId) ->
  TeamId ->
  LockableFeature PreventAdminlessGroupsConfig ->
  Sem r ()
scheduleDeletion lcnv mlusr tid feature = do
  now <- Now.get
  let deletionTimeout = timeoutToNominalDiffTime feature.config.deletionTimeout
      scheduledFor = addUTCTime deletionTimeout now
      deletionScheduledFor = toUTCTimeMillis scheduledFor
  void $ scheduleAdminlessDeletionJob mlusr tid (qUnqualified (tUntagged lcnv)) scheduledFor
  for_ feature.config.reminderTimeouts $
    scheduleReminder now deletionScheduledFor deletionTimeout
  where
    scheduleReminder now deletionScheduledFor deletionTimeout reminderTimeoutCfg = do
      let reminderTimeout = timeoutToNominalDiffTime reminderTimeoutCfg
      when (reminderTimeout < deletionTimeout) $ do
        let reminderAt = addUTCTime (deletionTimeout - reminderTimeout) now
        void $
          scheduleAdminlessReminderJob
            mlusr
            tid
            (qUnqualified (tUntagged lcnv))
            deletionScheduledFor
            reminderTimeout
            reminderAt

    timeoutToNominalDiffTime :: PreventAdminlessTimeout -> NominalDiffTime
    timeoutToNominalDiffTime =
      realToFrac . duration . durationLiteralValue . preventAdminlessTimeoutLiteral

adminlessTryAutopromote ::
  ( Member ConversationStore r,
    Member (ErrorS 'ConvNotFound) r,
    Member (Error FederationError) r,
    Member BrigAPIAccess r,
    Member Random r,
    Member NotificationSubsystem r,
    Member Now r,
    Member E.ExternalAccess r,
    Member BackendNotificationQueueAccess r,
    Member FeaturesConfigSubsystem r
  ) =>
  Maybe (Local UserId) ->
  Local ConvId ->
  (StoredConversation -> LockableFeature PreventAdminlessGroupsConfig -> [(Qualified UserId, User.Name)] -> Sem r ()) ->
  Sem r ()
adminlessTryAutopromote mlusr lcnv altAction = do
  conv <- getConversationWithError lcnv
  when (isAdminlessCheckCandidate conv) $ for_ conv.metadata.cnvmTeam $ \tid -> do
    (feature :: LockableFeature PreventAdminlessGroupsConfig) <- getFeatureForTeam tid
    let adminExists = any (\member -> member.convRoleName == roleNameWireAdmin) conv.localMembers || any (\member -> member.convRoleName == roleNameWireAdmin) conv.remoteMembers
    when (feature.status == FeatureStatusEnabled && not adminExists) $ do
      eligibleMembers <- eligibleAdminFallbackMembers lcnv Nothing conv
      case eligibleMembers of
        x : xs -> do
          seed <- randomWord64
          let autopromotionCandidates = selectAutopromotionCandidate seed feature.config.promotionStrategy (x :| xs)
              update = OtherMemberUpdate (Just roleNameWireAdmin)
          for_ autopromotionCandidates $ \candidate -> do
            E.setOtherMember lcnv candidate update
            case mlusr of
              Just lusr ->
                void $
                  sendConversationActionNotifications
                    (sing @'ConversationMemberUpdateTag)
                    (tUntagged lusr)
                    False
                    Nothing
                    (qualifyAs lcnv conv)
                    (convBotsAndMembers conv)
                    (ConversationMemberUpdate candidate update)
                    def
              Nothing -> do
                now <- Now.get
                Notify.pushSystemEvent
                  Nothing
                  ( SystemEvent
                      (tUntagged lcnv)
                      Nothing
                      now
                      conv.metadata.cnvmTeam
                      (EdSystemMemberUpdate (memberUpdateData candidate update))
                  )
                  (Set.fromList (map (.id_) conv.localMembers))
        [] -> altAction conv feature eligibleMembers
  where
    memberUpdateData candidate memberUpdate' =
      MemberUpdateData
        { misTarget = candidate,
          misOtrMutedStatus = Nothing,
          misOtrMutedRef = Nothing,
          misOtrArchived = Nothing,
          misOtrArchivedRef = Nothing,
          misHidden = Nothing,
          misHiddenRef = Nothing,
          misConvRoleName = omuConvRoleName memberUpdate'
        }

adminlessAutopromoteOrDelete ::
  ( Member ConversationStore r,
    Member (ErrorS 'ConvNotFound) r,
    Member (Error FederationError) r,
    Member BrigAPIAccess r,
    Member Random r,
    Member NotificationSubsystem r,
    Member Now r,
    Member E.ExternalAccess r,
    Member BackendNotificationQueueAccess r,
    Member FeaturesConfigSubsystem r,
    Member ProposalStore r,
    Member CodeStore r
  ) =>
  Maybe (Local UserId) ->
  Local ConvId ->
  Sem r ()
adminlessAutopromoteOrDelete mlusr lcnv = adminlessTryAutopromote mlusr lcnv orAlternativelyDeleteConv
  where
    orAlternativelyDeleteConv conv _ _ = do
      removeConversation (qualifyAs lcnv conv)
      case mlusr of
        Just lusr ->
          void $
            sendConversationActionNotifications
              (sing @'ConversationDeleteTag)
              (tUntagged lusr)
              False
              Nothing
              (qualifyAs lcnv conv)
              (convBotsAndMembers conv)
              ()
              def
        Nothing -> do
          now <- Now.get
          Notify.pushSystemEvent
            Nothing
            (SystemEvent (tUntagged lcnv) Nothing now conv.metadata.cnvmTeam EdSystemConvDelete)
            (Set.fromList (map (.id_) conv.localMembers))

adminlessAutopromoteOrSendReminder ::
  ( Member ConversationStore r,
    Member (ErrorS 'ConvNotFound) r,
    Member (Error FederationError) r,
    Member BrigAPIAccess r,
    Member Random r,
    Member NotificationSubsystem r,
    Member Now r,
    Member E.ExternalAccess r,
    Member BackendNotificationQueueAccess r,
    Member FeaturesConfigSubsystem r
  ) =>
  Maybe (Local UserId) ->
  Local ConvId ->
  UTCTimeMillis ->
  Sem r ()
adminlessAutopromoteOrSendReminder mlusr lcnv deletionScheduledFor = adminlessTryAutopromote mlusr lcnv orAlternativelySendReminder
  where
    orAlternativelySendReminder conv _ _ = do
      now <- Now.get
      case mlusr of
        Just lusr -> do
          let event =
                Event
                  (tUntagged lcnv)
                  Nothing
                  (EventFromUser (tUntagged lusr))
                  now
                  (conv.metadata.cnvmTeam)
                  (EdAdminlessReminder (AdminlessReminder deletionScheduledFor))
          pushConversationEvent Nothing conv event (qualifyAs lcnv (map (.id_) conv.localMembers)) []
        Nothing ->
          Notify.pushSystemEvent
            Nothing
            ( SystemEvent
                (tUntagged lcnv)
                Nothing
                now
                conv.metadata.cnvmTeam
                (EdSystemAdminlessReminder (AdminlessReminder deletionScheduledFor))
            )
            (Set.fromList (map (.id_) conv.localMembers))

-- Use eight random bytes and fold them into a big-endian Word64. This keeps
-- the helper small, deterministic under tests, and free of extra Random API.
randomWord64 :: (Member Random r) => Sem r Word64
randomWord64 = BS.foldl' step 0 <$> Random.bytes 8
  where
    step acc byte = shiftL acc 8 .|. fromIntegral byte

isLeavingLastConversationAdmin :: UserId -> StoredConversation -> Bool
isLeavingLastConversationAdmin leavingUser conv =
  leavingMemberIsAdmin && not remainingAdminExists
  where
    leavingMemberIsAdmin =
      any (\member -> member.id_ == leavingUser && member.convRoleName == roleNameWireAdmin) conv.localMembers
    remainingAdminExists =
      any (\member -> member.id_ /= leavingUser && member.convRoleName == roleNameWireAdmin) conv.localMembers
        || any (\member -> member.convRoleName == roleNameWireAdmin) conv.remoteMembers

eligibleAdminFallbackMembers ::
  (Member BrigAPIAccess r) =>
  Local ConvId ->
  Maybe UserId ->
  StoredConversation ->
  Sem r [(Qualified UserId, User.Name)]
eligibleAdminFallbackMembers lcnv mLeavingUser conv = do
  eligibleAdminFallbackMembersFor lcnv (maybe Set.empty Set.singleton mLeavingUser) Set.empty conv

eligibleAdminFallbackMembersFor ::
  (Member BrigAPIAccess r) =>
  Local ConvId ->
  Set UserId ->
  Set (Qualified UserId) ->
  StoredConversation ->
  Sem r [(Qualified UserId, User.Name)]
eligibleAdminFallbackMembersFor lcnv leavingUsers addedUsers conv = do
  let existingCandidates =
        [ (Qualified member.id_ (tDomain lcnv), member.id_)
        | member <- conv.localMembers,
          Set.notMember member.id_ leavingUsers
        ]
      addedLocalCandidates =
        [ (user, qUnqualified user)
        | user <- Set.toList addedUsers,
          qDomain user == tDomain lcnv
        ]
      candidates = existingCandidates <> addedLocalCandidates
      candidateIds = Set.toList (Set.fromList (map snd candidates))
  users <- Brig.getUsers candidateIds
  let usersById = Map.fromList [(User.userId u, u) | u <- users]
  pure
    [ (qualifiedId, u.userDisplayName)
    | (qualifiedId, candidateId) <- candidates,
      Just u <- [Map.lookup candidateId usersById],
      isEligibleUser u
    ]
  where
    isEligibleUser u =
      u.userType == User.UserTypeRegular
        && u.userStatus == User.Active
        && isNothing u.userService

deleteUserFromTeamConversationsImpl ::
  ( Member BackendNotificationQueueAccess r,
    Member BrigAPIAccess r,
    Member (Error AdminlessConversation) r,
    Member (ErrorS ('ActionDenied 'ModifyOtherConversationMember)) r,
    Member (ErrorS 'ConvNotFound) r,
    Member (ErrorS 'InvalidOperation) r,
    Member (ErrorS 'ConvMemberNotFound) r,
    Member (Error FederationError) r,
    Member E.ConversationStore r,
    Member FeaturesConfigSubsystem r,
    Member NotificationSubsystem r,
    Member E.ExternalAccess r,
    Member Now r,
    Member Random r,
    Member TeamSubsystem r,
    Member JobSubsystem r
  ) =>
  Local UserId ->
  Maybe ConnId ->
  TeamId ->
  UserId ->
  Sem r ()
deleteUserFromTeamConversationsImpl lusr conn tid remove = do
  cc <- E.getTeamConversations tid
  for_ cc $
    ( E.getConversation
        >=> ( \case
                Nothing -> pure ()
                Just dc
                  | remove `isMember` dc.localMembers ->
                      do
                        case dc.metadata.cnvmType of
                          One2OneConv -> E.deleteConversation dc.id_
                          _ -> do
                            guardPreventAdminlessGroups
                              RemoveMemberLegacyResponse
                              (qualifyAs lusr dc.id_)
                              lusr
                              (tUntagged (qualifyAs lusr remove))
                            E.deleteMembers dc.id_ (UserList [remove] [])
                            let (bots, allLocUsers) = localBotsAndUsers dc.localMembers
                                targets =
                                  BotsAndMembers
                                    (Set.fromList $ (.id_) <$> allLocUsers)
                                    (Set.fromList $ (.id_) <$> dc.remoteMembers)
                                    (Set.fromList bots)
                            void $
                              sendConversationActionNotifications
                                SConversationRemoveMembersTag
                                (tUntagged lusr)
                                True
                                conn
                                (qualifyAs lusr dc)
                                targets
                                ( ConversationRemoveMembers
                                    (pure . tUntagged . qualifyAs lusr $ remove)
                                    EdReasonDeleted
                                )
                                def
                  | otherwise -> pure ()
            )
    )

-- | if the public member leave api was called, we can assume that
--   it was called by a user
pattern EdMembersLeaveRemoved :: QualifiedUserIdList -> EventData
pattern EdMembersLeaveRemoved l = EdMembersLeave EdReasonRemoved l

removeMemberFromRemoteConv ::
  ( Member (E.FederationAPIAccess FederatorClient) r,
    Member (Error FederationError) r,
    Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r,
    Member (ErrorS 'ConvNotFound) r,
    Member Now r
  ) =>
  Remote ConvId ->
  Local UserId ->
  Qualified UserId ->
  Sem r (Maybe Event)
removeMemberFromRemoteConv cnv lusr victim
  | tUntagged lusr == victim = do
      let lc = LeaveConversationRequest (tUnqualified cnv) (qUnqualified victim)
      let rpc = fedClient @'Galley @"leave-conversation" lc
      E.runFederated cnv rpc
        >>= either handleError handleSuccess . void . (.response)
  | otherwise = throwS @('ActionDenied 'RemoveConversationMember)
  where
    handleError ::
      ( Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r,
        Member (ErrorS 'ConvNotFound) r
      ) =>
      RemoveFromConversationError ->
      Sem r (Maybe Event)
    handleError RemoveFromConversationErrorRemovalNotAllowed =
      throwS @('ActionDenied 'RemoveConversationMember)
    handleError RemoveFromConversationErrorNotFound = throwS @'ConvNotFound
    handleError RemoveFromConversationErrorUnchanged = pure Nothing

    handleSuccess :: (Member Now r) => () -> Sem r (Maybe Event)
    handleSuccess _ = do
      t <- Now.get
      pure . Just $
        Event (tUntagged cnv) Nothing (EventFromUser (tUntagged lusr)) t Nothing $
          EdMembersLeaveRemoved (QualifiedUserIdList [victim])

-- | Remove a member from a local conversation.
removeMemberFromLocalConv ::
  ( Member BackendNotificationQueueAccess r,
    Member ConversationStore r,
    Member (Error FederationError) r,
    Member (ErrorS ('ActionDenied 'LeaveConversation)) r,
    Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r,
    Member (ErrorS 'ConvNotFound) r,
    Member (ErrorS 'InvalidOperation) r,
    Member E.ExternalAccess r,
    Member NotificationSubsystem r,
    Member Now r,
    Member ProposalStore r,
    Member Random r,
    Member TinyLog r,
    Member TeamSubsystem r,
    Member (Input ConversationSubsystemConfig) r,
    Member (ErrorS (ActionDenied ModifyOtherConversationMember)) r,
    Member (ErrorS ConvMemberNotFound) r,
    Member (Error AdminlessConversation) r,
    Member FeaturesConfigSubsystem r,
    Member BrigAPIAccess r,
    Member JobSubsystem r
  ) =>
  RemoveMemberResponseMode ->
  Local ConvId ->
  Local UserId ->
  Maybe ConnId ->
  Qualified UserId ->
  Sem r (Maybe Event)
removeMemberFromLocalConv responseMode lcnv lusr con victim
  | tUntagged lusr == victim = do
      guardPreventAdminlessGroups responseMode lcnv lusr victim
      fmap (fmap lcuEvent . hush) $
        runError @NoChanges $
          updateLocalConversationLeave lcnv (tUntagged lusr) con
  | otherwise = do
      conv <- getConversationWithError lcnv
      let lconv = qualifyAs lusr conv
      if conv.metadata.cnvmGroupConvType == Just Channel
        then
          -- For channels, always use the channel-specific removal path so that
          -- MLS proposals are generated and the protocol flow is respected.
          -- FUTUREWORK: implement the removal via the generic updateLocalConversation path
          fmap (const Nothing) . runError @NoChanges $ removeMemberFromChannel (tUntagged lusr) lconv victim
        else
          fmap (fmap lcuEvent . hush)
            . runError @NoChanges
            $ updateLocalConversationRemoveMembers
              lcnv
              (tUntagged lusr)
              con
              (ConversationRemoveMembers (pure victim) EdReasonRemoved)

removeMemberFromChannel ::
  forall r.
  ( Member (ErrorS 'ConvNotFound) r,
    Member ProposalStore r,
    Member Now r,
    Member E.ExternalAccess r,
    Member NotificationSubsystem r,
    Member Random r,
    Member TinyLog r,
    Member (Error FederationError) r,
    Member BackendNotificationQueueAccess r,
    Member ConversationStore r,
    Member TeamSubsystem r,
    Member (Input ConversationSubsystemConfig) r,
    Member (Error NoChanges) r
  ) =>
  Qualified UserId ->
  Local StoredConversation ->
  Qualified UserId ->
  Sem r ()
removeMemberFromChannel qusr lconv victim = do
  let conv = tUnqualified lconv
  teamMember <- foldQualified lconv (getTeamMembership conv) (const $ pure Nothing) qusr >>= noteS @'ConvNotFound
  let action = ConversationRemoveMembers {crmTargets = pure victim, crmReason = EdReasonRemoved}
  let actorContext = ActorContext (Nothing :: Maybe LocalMember) (Just teamMember)
  ensureAllowed @'ConversationRemoveMembersTag lconv action conv actorContext
  let notificationTargets = convBotsAndMembers conv
  kickMember qusr lconv notificationTargets victim
  where
    getTeamMembership :: StoredConversation -> Local UserId -> Sem r (Maybe TeamMember)
    getTeamMembership conv luid = maybe (pure Nothing) (TeamSubsystem.internalGetTeamMember (tUnqualified luid)) conv.metadata.cnvmTeam

-- OTR

postProteusMessage ::
  ( Member BrigAPIAccess r,
    Member E.UserClientIndexStore r,
    Member ConversationStore r,
    Member (E.FederationAPIAccess FederatorClient) r,
    Member (Error FederationError) r,
    Member BackendNotificationQueueAccess r,
    Member NotificationSubsystem r,
    Member E.ExternalAccess r,
    Member (Input FeatureFlags) r,
    Member (Input IntraListing) r,
    Member Now r,
    Member TinyLog r,
    Member TeamSubsystem r
  ) =>
  Local UserId ->
  ConnId ->
  Qualified ConvId ->
  RawProto QualifiedNewOtrMessage ->
  Sem r (PostOtrResponse MessageSendingStatus)
postProteusMessage sender zcon conv msg = runLocalInput sender $ do
  foldQualified
    sender
    (\c -> postQualifiedOtrMessage User (tUntagged sender) (Just zcon) c (rpValue msg))
    (\c -> postRemoteOtrMessage sender c (rpRaw msg))
    conv

postProteusBroadcast ::
  ( Member BrigAPIAccess r,
    Member (ErrorS 'TeamNotFound) r,
    Member (ErrorS 'NonBindingTeam) r,
    Member (ErrorS 'BroadcastLimitExceeded) r,
    Member NotificationSubsystem r,
    Member E.ExternalAccess r,
    Member (Input FeatureFlags) r,
    Member Now r,
    Member TeamStore r,
    Member TinyLog r,
    Member (Input FanoutLimit) r,
    Member TeamSubsystem r,
    Member (Input ConversationSubsystemConfig) r,
    Member E.UserClientIndexStore r
  ) =>
  Local UserId ->
  ConnId ->
  QualifiedNewOtrMessage ->
  Sem r (PostOtrResponse MessageSendingStatus)
postProteusBroadcast sender zcon = postBroadcast sender (Just zcon)

unqualifyEndpoint ::
  (Functor f) =>
  Local x ->
  (QualifiedNewOtrMessage -> f (PostOtrResponse MessageSendingStatus)) ->
  Maybe IgnoreMissing ->
  Maybe ReportMissing ->
  NewOtrMessage ->
  f (PostOtrResponse ClientMismatch)
unqualifyEndpoint loc f ignoreMissing reportMissing message = do
  let qualifiedRecipients =
        QualifiedOtrRecipients
          . QualifiedUserClientMap
          . Map.singleton (tDomain loc)
          . userClientMap
          . fmap fromBase64TextLenient
          . otrRecipientsMap
          . newOtrRecipients
          $ message
      clientMismatchStrategy = legacyClientMismatchStrategy (tDomain loc) (newOtrReportMissing message) ignoreMissing reportMissing
      qualifiedMessage =
        QualifiedNewOtrMessage
          { qualifiedNewOtrSender = newOtrSender message,
            qualifiedNewOtrRecipients = qualifiedRecipients,
            qualifiedNewOtrNativePush = newOtrNativePush message,
            qualifiedNewOtrTransient = newOtrTransient message,
            qualifiedNewOtrNativePriority = newOtrNativePriority message,
            qualifiedNewOtrData = foldMap fromBase64TextLenient (newOtrData message),
            qualifiedNewOtrClientMismatchStrategy = clientMismatchStrategy
          }
  unqualify (tDomain loc) <$> f qualifiedMessage

postBotMessageUnqualified ::
  ( Member BrigAPIAccess r,
    Member E.UserClientIndexStore r,
    Member ConversationStore r,
    Member E.ExternalAccess r,
    Member (E.FederationAPIAccess FederatorClient) r,
    Member BackendNotificationQueueAccess r,
    Member NotificationSubsystem r,
    Member (Input (Local ())) r,
    Member (Input FeatureFlags) r,
    Member (Input IntraListing) r,
    Member TinyLog r,
    Member Now r,
    Member TeamSubsystem r
  ) =>
  BotId ->
  ConvId ->
  Maybe IgnoreMissing ->
  Maybe ReportMissing ->
  NewOtrMessage ->
  Sem r (PostOtrResponse ClientMismatch)
postBotMessageUnqualified sender cnv ignoreMissing reportMissing message = do
  lusr <- qualifyLocal (botUserId sender)
  lcnv <- qualifyLocal cnv
  unqualifyEndpoint
    lusr
    (runLocalInput lusr . postQualifiedOtrMessage Bot (tUntagged lusr) Nothing lcnv)
    ignoreMissing
    reportMissing
    message

postOtrBroadcastUnqualified ::
  ( Member BrigAPIAccess r,
    Member (ErrorS 'TeamNotFound) r,
    Member (ErrorS 'NonBindingTeam) r,
    Member (ErrorS 'BroadcastLimitExceeded) r,
    Member NotificationSubsystem r,
    Member E.ExternalAccess r,
    Member (Input FeatureFlags) r,
    Member Now r,
    Member TeamStore r,
    Member TinyLog r,
    Member (Input FanoutLimit) r,
    Member TeamSubsystem r,
    Member (Input ConversationSubsystemConfig) r,
    Member E.UserClientIndexStore r
  ) =>
  Local UserId ->
  ConnId ->
  Maybe IgnoreMissing ->
  Maybe ReportMissing ->
  NewOtrMessage ->
  Sem r (PostOtrResponse ClientMismatch)
postOtrBroadcastUnqualified sender zcon =
  unqualifyEndpoint
    sender
    (postBroadcast sender (Just zcon))

postOtrMessageUnqualified ::
  ( Member BrigAPIAccess r,
    Member E.UserClientIndexStore r,
    Member ConversationStore r,
    Member (E.FederationAPIAccess FederatorClient) r,
    Member BackendNotificationQueueAccess r,
    Member E.ExternalAccess r,
    Member NotificationSubsystem r,
    Member (Input FeatureFlags) r,
    Member (Input IntraListing) r,
    Member Now r,
    Member TinyLog r,
    Member TeamSubsystem r
  ) =>
  Local UserId ->
  ConnId ->
  ConvId ->
  Maybe IgnoreMissing ->
  Maybe ReportMissing ->
  NewOtrMessage ->
  Sem r (PostOtrResponse ClientMismatch)
postOtrMessageUnqualified sender zcon cnv =
  let lcnv = qualifyAs sender cnv
   in unqualifyEndpoint
        sender
        (runLocalInput sender . postQualifiedOtrMessage User (tUntagged sender) (Just zcon) lcnv)

updateConversationName ::
  ( Member ConversationStore r,
    Member (Error FederationError) r,
    Member (Error InvalidInput) r,
    Member (ErrorS ('ActionDenied 'ModifyConversationName)) r,
    Member (ErrorS 'ConvNotFound) r,
    Member (ErrorS 'InvalidOperation) r,
    Member E.ExternalAccess r,
    Member Now r,
    Member BackendNotificationQueueAccess r,
    Member NotificationSubsystem r,
    Member TeamSubsystem r
  ) =>
  Local UserId ->
  ConnId ->
  Qualified ConvId ->
  ConversationRename ->
  Sem r (UpdateResult Event)
updateConversationName lusr zcon qcnv convRename = do
  foldQualified
    lusr
    (updateLocalConversationName lusr zcon)
    (\_ _ -> throw FederationNotImplemented)
    qcnv
    convRename

updateLocalConversationName ::
  ( Member ConversationStore r,
    Member (Error FederationError) r,
    Member (Error InvalidInput) r,
    Member (ErrorS ('ActionDenied 'ModifyConversationName)) r,
    Member (ErrorS 'ConvNotFound) r,
    Member (ErrorS 'InvalidOperation) r,
    Member E.ExternalAccess r,
    Member Now r,
    Member BackendNotificationQueueAccess r,
    Member NotificationSubsystem r,
    Member TeamSubsystem r
  ) =>
  Local UserId ->
  ConnId ->
  Local ConvId ->
  ConversationRename ->
  Sem r (UpdateResult Event)
updateLocalConversationName lusr zcon lcnv rename =
  getUpdateResult . fmap lcuEvent $
    updateLocalConversationRename lcnv (tUntagged lusr) (Just zcon) rename

memberTyping ::
  ( Member NotificationSubsystem r,
    Member (ErrorS 'ConvNotFound) r,
    Member (Input (Local ())) r,
    Member Now r,
    Member ConversationStore r,
    Member (E.FederationAPIAccess FederatorClient) r,
    Member (Error FederationError) r,
    Member TeamSubsystem r
  ) =>
  Local UserId ->
  ConnId ->
  Qualified ConvId ->
  TypingStatus ->
  Sem r ()
memberTyping lusr zcon qcnv ts = do
  foldQualified
    lusr
    ( \lcnv -> do
        conv <- maskConvAccessDenied $ getConversationAsMember (tUntagged lusr) lcnv
        void $ notifyTypingIndicator conv (tUntagged lusr) (Just zcon) ts
    )
    ( \rcnv -> do
        isMemberRemoteConv <- E.checkLocalMemberRemoteConv (tUnqualified lusr) rcnv
        unless isMemberRemoteConv $ throwS @'ConvNotFound
        let rpc =
              TypingDataUpdateRequest
                { typingStatus = ts,
                  userId = tUnqualified lusr,
                  convId = tUnqualified rcnv
                }
        res <- E.runFederated rcnv (fedClient @'Galley @"update-typing-indicator" rpc)
        case res of
          TypingDataUpdateSuccess (TypingDataUpdated {..}) -> do
            pushTypingIndicatorEvents origUserId time usersInConv (Just zcon) qcnv typingStatus
          TypingDataUpdateError _ -> pure ()
    )
    qcnv

addBot ::
  forall r.
  ( Member E.UserClientIndexStore r,
    Member ConversationStore r,
    Member (ErrorS ('ActionDenied 'AddConversationMember)) r,
    Member (ErrorS 'ConvNotFound) r,
    Member (ErrorS 'InvalidOperation) r,
    Member (ErrorS 'TooManyMembers) r,
    Member E.ExternalAccess r,
    Member NotificationSubsystem r,
    Member (Input ConversationSubsystemConfig) r,
    Member Now r
  ) =>
  Local UserId ->
  ConnId ->
  AddBot ->
  Sem r Event
addBot lusr zcon b = do
  c <-
    E.getConversation (b ^. addBotConv) >>= noteS @'ConvNotFound
  -- Check some preconditions on adding bots to a conversation
  (bots, users) <- regularConvChecks c
  t <- Now.get
  E.createClient (botUserId (b ^. addBotId)) (b ^. addBotClient)
  bm <- E.createBotMember (b ^. addBotService) (b ^. addBotId) (b ^. addBotConv)
  let e =
        Event
          (tUntagged (qualifyAs lusr (b ^. addBotConv)))
          Nothing
          (EventFromUser (tUntagged lusr))
          t
          Nothing
          ( EdMembersJoin
              ( MembersJoin
                  [ SimpleMember
                      (tUntagged (qualifyAs lusr (botUserId (botMemId bm))))
                      roleNameWireAdmin
                  ]
                  InternalAdd
              )
          )
  pushNotifications
    [ def
        { origin = Just (tUnqualified lusr),
          json = toJSONObject e,
          recipients = map localMemberToRecipient users,
          isCellsEvent = shouldPushToCells c.metadata e,
          conn = Just zcon
        }
    ]
  E.deliverAsync (map (,e) (bm : bots))
  pure e
  where
    regularConvChecks c = do
      let (bots, users) = localBotsAndUsers c.localMembers
      unless (tUnqualified lusr `isMember` users) $ throwS @'ConvNotFound
      ensureGroupConversation c
      self <- getSelfMemberFromLocals (tUnqualified lusr) users
      -- Note that in brig from where this internal handler is called, we additionally check for conversation admin role.
      -- Remember to change this if we ever want to allow non admins to add bots.
      -- Also note, that in the future we want to allow channel admins (who are not necessarily conversation admins) to add bots.
      -- Currently bots cannot be added to channels because channels are always MLS conversations which do not support bots.
      ensureActionAllowed SAddConversationMember self
      unless (any ((== b ^. addBotId) . botMemId) bots) $ do
        let botId = qualifyAs lusr (botUserId (b ^. addBotId))
        ensureMemberLimit (convProtocolTag c) (toList c.localMembers) [tUntagged botId]
      pure (bots, users)

rmBot ::
  ( Member E.UserClientIndexStore r,
    Member ConversationStore r,
    Member (ErrorS 'ConvNotFound) r,
    Member E.ExternalAccess r,
    Member NotificationSubsystem r,
    Member Now r,
    Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r
  ) =>
  Local UserId ->
  Maybe ConnId ->
  RemoveBot ->
  Sem r (UpdateResult Event)
rmBot lusr zcon b = do
  c <-
    E.getConversation (b ^. rmBotConv) >>= noteS @'ConvNotFound
  let (bots, users) = localBotsAndUsers c.localMembers
  unless (tUnqualified lusr `isMember` c.localMembers) $
    throwS @'ConvNotFound
  -- A bot can remove itself (which will internally be triggered when a service is deleted),
  -- otherwise we have to check for the correct permissions
  unless (botUserId (b ^. rmBotId) == tUnqualified lusr) $ do
    -- Note that in brig from where this internal handler is called, we additionally check for conversation admin role.
    -- Remember to change this if we ever want to allow non admins to remove bots.
    self <- getSelfMemberFromLocals (tUnqualified lusr) users
    -- Similarly to addBot we currently only allow admins to remove bots. Once bots support MLS we, also need to check for channel admin permissions here.
    ensureActionAllowed SRemoveConversationMember self
  let lcnv = qualifyAs lusr c.id_
  if not (any ((== b ^. rmBotId) . botMemId) bots)
    then pure Unchanged
    else do
      t <- Now.get
      do
        let evd = EdMembersLeaveRemoved (QualifiedUserIdList [tUntagged (qualifyAs lusr (botUserId (b ^. rmBotId)))])
        let e = Event (tUntagged lcnv) Nothing (EventFromUser (tUntagged lusr)) t Nothing evd
        pushNotifications
          [ def
              { origin = Just (tUnqualified lusr),
                json = toJSONObject e,
                recipients = map localMemberToRecipient users,
                isCellsEvent = shouldPushToCells c.metadata e,
                conn = zcon
              }
          ]
        E.deleteMembers c.id_ (UserList [botUserId (b ^. rmBotId)] [])
        E.deleteClients (botUserId (b ^. rmBotId))
        E.deliverAsync (map (,e) bots)
        pure $ Updated e

updateCellsState ::
  ( Member ConversationStore r,
    Member (ErrorS ConvNotFound) r,
    Member (ErrorS InvalidOperation) r,
    Member FeaturesConfigSubsystem r
  ) =>
  ConvId ->
  CellsState ->
  Sem r ()
updateCellsState cnv state = do
  when (state /= CellsDisabled) $ do
    conv <- E.getConversation cnv >>= noteS @ConvNotFound
    tid <- noteS @InvalidOperation conv.metadata.cnvmTeam
    (feat :: LockableFeature CellsConfig) <- getFeatureForTeam tid
    noteS @InvalidOperation $ guard (feat.status == FeatureStatusEnabled)
  E.setConversationCellsState cnv state

-------------------------------------------------------------------------------
-- Helpers

ensureConvMember :: (Member (ErrorS 'ConvNotFound) r) => [LocalMember] -> UserId -> Sem r ()
ensureConvMember users usr =
  unless (usr `isMember` users) $ throwS @'ConvNotFound

getConversationCodeURI ::
  ( Member (ErrorS 'ConvAccessDenied) r,
    Member CodeStore r
  ) =>
  Maybe ZHostValue ->
  Sem r HttpsUrl
getConversationCodeURI mbZHost = do
  mbURI <- E.getConversationCodeURI mbZHost
  case mbURI of
    Just uri -> pure uri
    Nothing -> throwS @'ConvAccessDenied
