{-# LANGUAGE TemplateHaskell #-}

-- 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
  ( module Wire.ConversationSubsystem,
    Util.BotsAndMembers (..),
    Util.canDeleteMember,
    Util.isMember,
    Util.userLHEnabled,
    MLSRemoval.RemoveUserIncludeMain (..),
    LegalholdConflicts.guardLegalholdPolicyConflicts,
  )
where

import Data.Code qualified as Code
import Data.CommaSeparatedList (CommaSeparatedList)
import Data.Domain
import Data.Id
import Data.Json.Util (UTCTimeMillis)
import Data.Misc (IpAddr)
import Data.Qualified
import Data.Range
import Data.Singletons (Sing)
import Imports
import Polysemy
import Wire.API.Bot (AddBot, RemoveBot)
import Wire.API.Conversation hiding (Member)
import Wire.API.Conversation qualified as Public
import Wire.API.Conversation.Action
import Wire.API.Conversation.CellsState (CellsState)
import Wire.API.Conversation.Code (ConversationCodeInfo, CreateConversationCodeRequest, JoinConversationByCode)
import Wire.API.Conversation.Pagination (ConversationPage)
import Wire.API.Conversation.Protocol
import Wire.API.Conversation.Role (ConversationRolesList)
import Wire.API.Conversation.Typing
import Wire.API.Event.Conversation
import Wire.API.Federation.API.Common
import Wire.API.Federation.API.Galley
import Wire.API.MLS.CommitBundle
import Wire.API.MLS.GroupInfo (GroupInfoData)
import Wire.API.MLS.Keys (MLSKeys, MLSKeysByPurpose, MLSPublicKey, MLSPublicKeyFormat, SomeKey)
import Wire.API.MLS.Message
import Wire.API.MLS.OutOfSync (EnableOutOfSyncCheck)
import Wire.API.MLS.Serialisation
import Wire.API.MLS.SubConversation (ConvOrSubConvId, PublicSubConversation, SubConvId)
import Wire.API.Message (ClientMismatch, IgnoreMissing, MessageSendingStatus, NewOtrMessage, QualifiedNewOtrMessage, ReportMissing)
import Wire.API.Pagination (PageSize, SortOrder)
import Wire.API.Provider.Bot qualified as Public (BotConvView)
import Wire.API.Routes.Internal.Galley.ConversationsIntra (UpsertOne2OneConversationRequest)
import Wire.API.Routes.Public (ZHostValue)
import Wire.API.Routes.Public.Galley.Conversation
import Wire.API.Routes.Public.Galley.MLS (MLSReset)
import Wire.API.Routes.Public.Galley.Messaging (MessageNotSent, PostOtrResponse)
import Wire.API.Routes.Public.Util (UpdateResult)
import Wire.API.Routes.Version
import Wire.API.ServantProto (RawProto (..))
import Wire.API.Team.Feature (GuestLinksConfig, LockableFeature)
import Wire.ConversationStore.MLS.Types (ListGlobalSelfConvs)
import Wire.ConversationSubsystem.LegalholdConflicts qualified as LegalholdConflicts
import Wire.ConversationSubsystem.MLS.IncomingMessage (IncomingBundle, IncomingMessage)
import Wire.ConversationSubsystem.MLS.Removal qualified as MLSRemoval
import Wire.ConversationSubsystem.Util qualified as Util
import Wire.NotificationSubsystem (LocalConversationUpdate)
import Wire.StoredConversation

data RemoveMemberResponseMode
  = RemoveMemberLegacyResponse
  | RemoveMemberEligibleMembersResponse

data ConversationSubsystem m a where
  NotifyConversationAction ::
    Sing tag ->
    EventFrom ->
    Bool ->
    Maybe ConnId ->
    Local StoredConversation ->
    Set UserId ->
    Set (Remote UserId) ->
    Set BotMember ->
    ConversationAction (tag :: ConversationActionTag) ->
    ExtraConversationData ->
    ConversationSubsystem r LocalConversationUpdate
  InternalCreateGroupConversation ::
    Local UserId ->
    Maybe ConnId ->
    NewConv ->
    ConversationSubsystem m StoredConversation
  CreateLegacyGroupConversation ::
    Local UserId ->
    Maybe ConnId ->
    NewConv ->
    ConversationSubsystem m (ConversationResponse (Public.OwnConversation GroupConvType))
  CreateGroupOwnConversation ::
    Local UserId ->
    Maybe ConnId ->
    NewConv ->
    ConversationSubsystem m (CreateGroupConversationResponseV9 GroupConvType)
  CreateGroupConversation ::
    Local UserId ->
    Maybe ConnId ->
    NewConv ->
    ConversationSubsystem m (CreateGroupConversation GroupConvType)
  CreateProteusSelfConversation ::
    Local UserId ->
    ConversationSubsystem m (ConversationResponse (Public.OwnConversation GroupConvType))
  CreateOne2OneConversation ::
    Local UserId ->
    ConnId ->
    NewOne2OneConv ->
    ConversationSubsystem m (ConversationResponse (Public.OwnConversation GroupConvType))
  CreateConnectConversation ::
    Local UserId ->
    Maybe ConnId ->
    Connect ->
    ConversationSubsystem m (ConversationResponse (Public.OwnConversation GroupConvType))
  GetConversations ::
    [ConvId] ->
    ConversationSubsystem m [StoredConversation]
  GetConversationIds ::
    Local UserId ->
    Range 1 1000 Int32 ->
    Maybe ConversationPagingState ->
    ConversationSubsystem r ConvIdsPage
  InternalGetLocalMember ::
    ConvId ->
    UserId ->
    ConversationSubsystem m (Maybe LocalMember)
  InternalGetMember ::
    Qualified ConvId ->
    UserId ->
    ConversationSubsystem m (Maybe Public.Member)
  GetConversationMeta ::
    ConvId ->
    ConversationSubsystem m (ConversationMetadata GroupConvType)
  GetMLSOne2OneConversationInternal ::
    Local UserId ->
    Qualified UserId ->
    ConversationSubsystem m (Public.OwnConversation GroupConvType)
  IsMLSOne2OneEstablished ::
    Local UserId ->
    Qualified UserId ->
    ConversationSubsystem m Bool
  GetLocalConversationInternal ::
    ConvId ->
    ConversationSubsystem m (Conversation GroupConvType)
  RemoveClient ::
    UserId ->
    ClientId ->
    ConversationSubsystem m ()
  AddBot ::
    Local UserId ->
    ConnId ->
    AddBot ->
    ConversationSubsystem m Event
  RmBot ::
    Local UserId ->
    Maybe ConnId ->
    RemoveBot ->
    ConversationSubsystem m (UpdateResult Event)
  UpdateCellsState ::
    ConvId ->
    CellsState ->
    ConversationSubsystem m ()
  RemoveUser ::
    -- MLS removal proposals for a single conversation; does not touch the conversation store.
    Local StoredConversation ->
    MLSRemoval.RemoveUserIncludeMain ->
    Qualified UserId ->
    ConversationSubsystem m ()
  PostMLSCommitBundle ::
    Local x ->
    Qualified UserId ->
    ClientId ->
    ConvType ->
    Qualified ConvOrSubConvId ->
    Maybe ConnId ->
    EnableOutOfSyncCheck ->
    IncomingBundle ->
    ConversationSubsystem m [LocalConversationUpdate]
  PostMLSCommitBundleFromLocalUser ::
    Version ->
    Local UserId ->
    ClientId ->
    ConnId ->
    RawMLS CommitBundle ->
    ConversationSubsystem m MLSMessageSendingStatus
  PostMLSMessage ::
    Local x ->
    Qualified UserId ->
    ClientId ->
    ConvType ->
    Qualified ConvOrSubConvId ->
    Maybe ConnId ->
    EnableOutOfSyncCheck ->
    IncomingMessage ->
    ConversationSubsystem m [LocalConversationUpdate]
  PostMLSMessageFromLocalUser ::
    Version ->
    Local UserId ->
    ClientId ->
    ConnId ->
    RawMLS Message ->
    ConversationSubsystem m MLSMessageSendingStatus
  IsMLSEnabled :: ConversationSubsystem m Bool
  GetConversationsInternal ::
    Local UserId ->
    Maybe (Range 1 32 (CommaSeparatedList ConvId)) ->
    Maybe ConvId ->
    Maybe (Range 1 500 Int32) ->
    ConversationSubsystem m (Public.ConversationList StoredConversation)
  FederationOnConversationCreated ::
    Domain ->
    ConversationCreated ConvId ->
    ConversationSubsystem m EmptyResponse
  FederationGetConversations ::
    Domain ->
    GetConversationsRequest ->
    ConversationSubsystem m GetRemoteConversationViewsResponse
  FederationLeaveConversation ::
    Domain ->
    LeaveConversationRequest ->
    ConversationSubsystem m LeaveConversationResponse
  FederationSendMessage ::
    Domain ->
    ProteusMessageSendRequest ->
    ConversationSubsystem m MessageSendResponse
  FederationUpdateConversation ::
    Domain ->
    ConversationUpdateRequest ->
    ConversationSubsystem m ConversationUpdateResponse
  FederationMlsSendWelcome ::
    Domain ->
    MLSWelcomeRequest ->
    ConversationSubsystem m MLSWelcomeResponse
  FederationSendMLSMessage ::
    Domain ->
    MLSMessageSendRequest ->
    ConversationSubsystem m MLSMessageResponse
  FederationSendMLSCommitBundle ::
    Domain ->
    MLSMessageSendRequest ->
    ConversationSubsystem m MLSMessageResponse
  FederationQueryGroupInfo ::
    Domain ->
    GetGroupInfoRequest ->
    ConversationSubsystem m GetGroupInfoResponse
  FederationUpdateTypingIndicator ::
    Domain ->
    TypingDataUpdateRequest ->
    ConversationSubsystem m TypingDataUpdateResponse
  FederationOnTypingIndicatorUpdated ::
    Domain ->
    TypingDataUpdated ->
    ConversationSubsystem m EmptyResponse
  FederationGetSubConversationForRemoteUser ::
    Domain ->
    GetSubConversationsRequest ->
    ConversationSubsystem m GetSubConversationsResponse
  FederationDeleteSubConversationForRemoteUser ::
    Domain ->
    DeleteSubConversationFedRequest ->
    ConversationSubsystem m DeleteSubConversationResponse
  FederationLeaveSubConversation ::
    Domain ->
    LeaveSubConversationRequest ->
    ConversationSubsystem m LeaveSubConversationResponse
  FederationGetLegacyOne2OneConversation ::
    Domain ->
    GetOne2OneConversationRequest ->
    ConversationSubsystem m GetOne2OneConversationResponse
  FederationGetOne2OneConversation ::
    Domain ->
    GetOne2OneConversationRequest ->
    ConversationSubsystem m GetOne2OneConversationResponseV2
  FederationOnClientRemoved ::
    Domain ->
    ClientRemovedRequest ->
    ConversationSubsystem m EmptyResponse
  FederationOnMessageSent ::
    Domain ->
    RemoteMessage ConvId ->
    ConversationSubsystem m EmptyResponse
  FederationOnMLSMessageSent ::
    Domain ->
    RemoteMLSMessage ->
    ConversationSubsystem m EmptyResponse
  FederationOnConversationUpdated ::
    Domain ->
    ConversationUpdate ->
    ConversationSubsystem m EmptyResponse
  FederationOnUserDeleted ::
    Domain ->
    UserDeletedConversationsNotification ->
    ConversationSubsystem m EmptyResponse
  PostOtrMessageUnqualified ::
    Local UserId ->
    ConnId ->
    ConvId ->
    Maybe IgnoreMissing ->
    Maybe ReportMissing ->
    NewOtrMessage ->
    ConversationSubsystem m (PostOtrResponse ClientMismatch)
  PostOtrBroadcastUnqualified ::
    Local UserId ->
    ConnId ->
    Maybe IgnoreMissing ->
    Maybe ReportMissing ->
    NewOtrMessage ->
    ConversationSubsystem m (PostOtrResponse ClientMismatch)
  PostProteusMessage ::
    Local UserId ->
    ConnId ->
    Qualified ConvId ->
    RawProto QualifiedNewOtrMessage ->
    ConversationSubsystem m (PostOtrResponse MessageSendingStatus)
  PostProteusBroadcast ::
    Local UserId ->
    ConnId ->
    QualifiedNewOtrMessage ->
    ConversationSubsystem m (PostOtrResponse MessageSendingStatus)
  DeleteLocalConversation ::
    Local UserId ->
    ConnId ->
    Local ConvId ->
    ConversationSubsystem m (UpdateResult Event)
  InternalDeleteLocalConversation ::
    Local ConvId ->
    ConversationSubsystem m ()
  InternalDeleteLocalAdminlessGroup ::
    Maybe (Local UserId) ->
    Local ConvId ->
    ConversationSubsystem m ()
  InternalNotifyAdminlessReminder ::
    Maybe (Local UserId) ->
    Local ConvId ->
    UTCTimeMillis ->
    ConversationSubsystem m ()
  SetupAdminlessGroupsCleanup ::
    Maybe (Local UserId) ->
    TeamId ->
    ConversationSubsystem m ()
  GetMLSPublicKeys ::
    Maybe MLSPublicKeyFormat ->
    ConversationSubsystem m (MLSKeysByPurpose (MLSKeys SomeKey))
  ResetMLSConversation ::
    Local UserId ->
    MLSReset ->
    ConversationSubsystem m ()
  GetSubConversation ::
    Local UserId ->
    Qualified ConvId ->
    SubConvId ->
    ConversationSubsystem m PublicSubConversation
  GetBotConversation ::
    BotId ->
    ConvId ->
    ConversationSubsystem m Public.BotConvView
  -- Query functions
  GetUnqualifiedOwnConversation ::
    Local UserId ->
    ConvId ->
    ConversationSubsystem m (Public.OwnConversation GroupConvType)
  GetOwnConversation ::
    Local UserId ->
    Qualified ConvId ->
    ConversationSubsystem m (Public.OwnConversation GroupConvType)
  GetPaginatedConversations ::
    Local UserId ->
    Maybe (Range 1 32 (CommaSeparatedList ConvId)) ->
    Maybe ConvId ->
    Maybe (Range 1 500 Int32) ->
    ConversationSubsystem m (Public.ConversationList (Public.OwnConversation GroupConvType))
  GetConversation ::
    Local UserId ->
    Qualified ConvId ->
    ConversationSubsystem m (Public.Conversation GroupConvType)
  InternalGetConversation ::
    ConvId ->
    ConversationSubsystem m (Maybe StoredConversation)
  GetConversationRoles ::
    Local UserId ->
    ConvId ->
    ConversationSubsystem m ConversationRolesList
  SearchChannels ::
    Local UserId ->
    TeamId ->
    Maybe Text ->
    Maybe SortOrder ->
    Maybe PageSize ->
    Maybe Text ->
    Maybe ConvId ->
    Bool ->
    ConversationSubsystem m ConversationPage
  GetGroupInfo ::
    Local UserId ->
    Qualified ConvId ->
    ConversationSubsystem m GroupInfoData
  ConversationIdsPageFromUnqualified ::
    Local UserId ->
    Maybe ConvId ->
    Maybe (Range 1 1000 Int32) ->
    ConversationSubsystem m (ConversationList ConvId)
  ConversationIdsPaginated ::
    ListGlobalSelfConvs ->
    Local UserId ->
    Public.GetPaginatedConversationIds ->
    ConversationSubsystem m Public.ConvIdsPage
  ConversationIdsPageFrom ::
    Local UserId ->
    Public.GetPaginatedConversationIds ->
    ConversationSubsystem m Public.ConvIdsPage
  ListConversations ::
    Local UserId ->
    Public.ListConversations ->
    ConversationSubsystem m (ConversationsResponse GroupConvType)
  GetConversationByReusableCode ::
    Local UserId ->
    Code.Key ->
    Code.Value ->
    ConversationSubsystem m ConversationCoverView
  GetMLSSelfConversationWithError ::
    Local UserId ->
    ConversationSubsystem m (Public.OwnConversation GroupConvType)
  GetMLSOne2OneOwnConversation ::
    Local UserId ->
    Qualified UserId ->
    ConversationSubsystem m (Public.OwnConversation GroupConvType)
  GetMLSOne2OneMLSConversation ::
    Local UserId ->
    Qualified UserId ->
    ConversationSubsystem m (MLSOne2OneConversation MLSPublicKey GroupConvType)
  GetMLSOne2OneConversation ::
    Local UserId ->
    Qualified UserId ->
    Maybe MLSPublicKeyFormat ->
    ConversationSubsystem m (MLSOne2OneConversation SomeKey GroupConvType)
  GetLocalSelf ::
    Local UserId ->
    ConvId ->
    ConversationSubsystem m (Maybe Public.Member)
  GetSelfMember ::
    Local UserId ->
    Qualified ConvId ->
    ConversationSubsystem m (Maybe Public.Member)
  GetConversationGuestLinksStatus ::
    UserId ->
    ConvId ->
    ConversationSubsystem m (LockableFeature GuestLinksConfig)
  GetCode ::
    Maybe ZHostValue ->
    Local UserId ->
    ConvId ->
    ConversationSubsystem m ConversationCodeInfo
  -- Update functions

  AddQualifiedMembersUnqualified ::
    Local UserId ->
    ConnId ->
    ConvId ->
    InviteQualified ->
    ConversationSubsystem m (UpdateResult Event)
  AddMembers ::
    Local UserId ->
    ConnId ->
    Qualified ConvId ->
    InviteQualified ->
    ConversationSubsystem m (UpdateResult Event)
  ReplaceMembers ::
    RemoveMemberResponseMode ->
    Local UserId ->
    ConnId ->
    Qualified ConvId ->
    InviteQualified ->
    ConversationSubsystem m ()
  JoinConversationById ::
    Local UserId ->
    ConnId ->
    ConvId ->
    ConversationSubsystem m (UpdateResult Event)
  JoinConversationByReusableCode ::
    Local UserId ->
    ConnId ->
    JoinConversationByCode ->
    ConversationSubsystem m (UpdateResult Event)
  CheckReusableCode ::
    IpAddr ->
    ConversationCode ->
    ConversationSubsystem m ()
  AddCodeUnqualified ::
    Maybe CreateConversationCodeRequest ->
    UserId ->
    Maybe ZHostValue ->
    Maybe ConnId ->
    ConvId ->
    ConversationSubsystem m AddCodeResult
  RmCodeUnqualified ::
    Local UserId ->
    ConnId ->
    ConvId ->
    ConversationSubsystem m Event
  MemberTyping ::
    Local UserId ->
    ConnId ->
    Qualified ConvId ->
    TypingStatus ->
    ConversationSubsystem m ()
  RemoveMemberQualified ::
    RemoveMemberResponseMode ->
    Local UserId ->
    Maybe ConnId ->
    Qualified ConvId ->
    Qualified UserId ->
    ConversationSubsystem m (Maybe Event)
  DeleteUserFromTeamConversations ::
    Local UserId ->
    Maybe ConnId ->
    TeamId ->
    UserId ->
    ConversationSubsystem m ()
  UpdateOtherMember ::
    Local UserId ->
    ConnId ->
    Qualified ConvId ->
    Qualified UserId ->
    OtherMemberUpdate ->
    ConversationSubsystem m ()
  UpdateConversationName ::
    Local UserId ->
    ConnId ->
    Qualified ConvId ->
    ConversationRename ->
    ConversationSubsystem m (UpdateResult Event)
  UpdateConversationMessageTimer ::
    Local UserId ->
    ConnId ->
    Qualified ConvId ->
    ConversationMessageTimerUpdate ->
    ConversationSubsystem m (UpdateResult Event)
  UpdateConversationReceiptMode ::
    Local UserId ->
    ConnId ->
    Qualified ConvId ->
    ConversationReceiptModeUpdate ->
    ConversationSubsystem m (UpdateResult Event)
  UpdateConversationAccess ::
    Local UserId ->
    ConnId ->
    Qualified ConvId ->
    ConversationAccessData ->
    ConversationSubsystem m (UpdateResult Event)
  UpdateConversationHistory ::
    Local UserId ->
    ConnId ->
    Qualified ConvId ->
    ConversationHistoryUpdate ->
    ConversationSubsystem m (UpdateResult Event)
  UpdateSelfMember ::
    Local UserId ->
    ConnId ->
    Qualified ConvId ->
    MemberUpdate ->
    ConversationSubsystem m ()
  UpdateConversationProtocolWithLocalUser ::
    Local UserId ->
    ConnId ->
    Qualified ConvId ->
    ProtocolUpdate ->
    ConversationSubsystem m (UpdateResult Event)
  UpdateChannelAddPermission ::
    Local UserId ->
    ConnId ->
    Qualified ConvId ->
    AddPermissionUpdate ->
    ConversationSubsystem m (UpdateResult Event)
  PostBotMessageUnqualified ::
    BotId ->
    ConvId ->
    Maybe IgnoreMissing ->
    Maybe ReportMissing ->
    NewOtrMessage ->
    ConversationSubsystem m (Either (MessageNotSent ClientMismatch) ClientMismatch)
  -- Sub-conversation functions
  DeleteSubConversation ::
    Local UserId ->
    Qualified ConvId ->
    SubConvId ->
    MLSReset ->
    ConversationSubsystem m ()
  GetSubConversationGroupInfo ::
    Local UserId ->
    Qualified ConvId ->
    SubConvId ->
    ConversationSubsystem m GroupInfoData
  LeaveSubConversation ::
    Local UserId ->
    ClientId ->
    Qualified ConvId ->
    SubConvId ->
    ConversationSubsystem m ()
  SendConversationActionNotifications ::
    forall tag m.
    Sing tag ->
    Qualified UserId ->
    Bool ->
    Maybe ConnId ->
    Local StoredConversation ->
    Util.BotsAndMembers ->
    ConversationAction (tag :: ConversationActionTag) ->
    ExtraConversationData ->
    ConversationSubsystem m LocalConversationUpdate
  InternalUpsertOne2OneConversation ::
    UpsertOne2OneConversationRequest ->
    ConversationSubsystem m ()
  AcceptConv ::
    QualifiedWithTag QLocal UserId ->
    Maybe ConnId ->
    ConvId ->
    ConversationSubsystem m (OwnConversation GroupConvType)
  BlockConv ::
    QualifiedWithTag QLocal UserId ->
    Qualified ConvId ->
    ConversationSubsystem m ()
  UnblockConv ::
    QualifiedWithTag QLocal UserId ->
    Maybe ConnId ->
    Qualified ConvId ->
    ConversationSubsystem m ()

makeSem ''ConversationSubsystem

iterateConversations ::
  (Member ConversationSubsystem r) =>
  Local UserId ->
  Range 1 500 Int32 ->
  ([StoredConversation] -> Sem r a) ->
  Sem r [a]
iterateConversations luid pageSize handleConvs = go Nothing
  where
    go mbConv = do
      convResult <- getConversationsInternal luid Nothing mbConv (Just pageSize)
      resultHead <- handleConvs (convList convResult)
      resultTail <- case convList convResult of
        (conv : rest) ->
          if convHasMore convResult
            then go (Just (maximum ((.id_) <$> (conv : rest))))
            else pure []
        _ -> pure []
      pure $ resultHead : resultTail
