package ch.threema.app.tasks

import ch.threema.app.groupflows.GroupChanges
import ch.threema.app.profilepicture.GroupProfilePictureUploader.GroupProfilePictureUploadResult
import ch.threema.app.services.ConversationCategoryService
import ch.threema.app.services.FileService
import ch.threema.base.crypto.NonceFactory
import ch.threema.base.utils.getThreemaLogger
import ch.threema.common.contentEquals
import ch.threema.data.datatypes.ConversationVisibility
import ch.threema.data.datatypes.GroupConversationId
import ch.threema.data.datatypes.GroupIdentity
import ch.threema.data.datatypes.GroupNotificationTriggerPolicyOverride
import ch.threema.data.datatypes.GroupNotificationTriggerPolicyOverridePolicy
import ch.threema.data.models.GroupModel
import ch.threema.data.models.GroupModelData
import ch.threema.data.repositories.ContactModelRepository
import ch.threema.data.repositories.GroupModelRepository
import ch.threema.domain.protocol.csp.ProtocolDefines
import ch.threema.domain.taskmanager.ActiveTask
import ch.threema.domain.taskmanager.ActiveTaskCodec
import ch.threema.domain.taskmanager.Task
import ch.threema.domain.taskmanager.TaskCodec
import ch.threema.domain.taskmanager.TransactionScope
import ch.threema.domain.taskmanager.getEncryptedGroupSyncUpdate
import ch.threema.domain.types.IdentityString
import ch.threema.protobuf.common.Image
import ch.threema.protobuf.common.blob
import ch.threema.protobuf.common.deltaImage
import ch.threema.protobuf.common.identities
import ch.threema.protobuf.common.image
import ch.threema.protobuf.common.unit
import ch.threema.protobuf.d2d.GroupSync.Update.MemberStateChange
import ch.threema.protobuf.d2d.sync.ConversationCategory
import ch.threema.protobuf.d2d.sync.ConversationVisibility as ProtocolsConversationVisibility
import ch.threema.protobuf.d2d.sync.Group
import ch.threema.protobuf.d2d.sync.GroupKt
import ch.threema.protobuf.d2d.sync.group
import ch.threema.protobuf.toProtobuf
import com.google.protobuf.kotlin.toByteString
import java.time.Instant
import kotlinx.serialization.Serializable
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject

private val logger = getThreemaLogger("ReflectGroupSyncUpdateTask")

abstract class ReflectGroupSyncUpdateBaseTask<TransactionResult, TaskResult>(
    protected val groupIdentity: GroupIdentity,
) : ReflectGroupSyncTask<TransactionResult, TaskResult>(), KoinComponent {
    private val nonceFactory: NonceFactory by inject()
    protected val fileService: FileService by inject()
    protected val groupModelRepository: GroupModelRepository by inject()

    protected val groupModel: GroupModel by lazy {
        groupModelRepository.getByGroupIdentity(groupIdentity)
            ?: throw IllegalStateException("Group has been deleted and this update task is not necessary anymore")
    }

    /**
     * The task type. This is just used for debugging.
     */
    protected abstract val type: String

    /**
     * This method is called as part of the transaction's precondition. Specific subclasses may
     * define their own precondition based on the current group model data by implementing this
     * method.
     */
    protected abstract fun checkInPrecondition(currentData: GroupModelData): Boolean

    /**
     * Get the group sync that contains the delta updates.
     */
    abstract fun getGroupSync(): Group

    /**
     * Get the member state changes.
     */
    abstract fun getMemberStateChanges(): Map<String, MemberStateChange>

    /**
     * As a precondition for an update task, the group with the given group identity must exist and
     * the changed data must differ from the current data.
     */
    final override val runPrecondition: () -> Boolean = {
        // The group must exist and the task specific check must be successful
        groupModel.data?.let {
            checkInPrecondition(it)
        } ?: false
    }

    protected suspend fun reflectGroupSync(handle: ActiveTaskCodec) {
        logger.info("Reflecting group sync update of type {}", type)

        val encryptedEnvelopeResult = getEncryptedGroupSyncUpdate(
            group = getGroupSync().also { groupSync: Group ->
                check(
                    groupSync.groupIdentity.creatorIdentity == groupModel.groupIdentity.creatorIdentity &&
                        groupSync.groupIdentity.groupId == groupModel.groupIdentity.groupId,
                ) { "Group identity must match" }
            },
            memberStateChanges = getMemberStateChanges(),
            multiDeviceProperties = mdProperties,
        )

        handle.reflectAndAwaitAck(
            encryptedEnvelopeResult = encryptedEnvelopeResult,
            storeD2dNonce = true,
            nonceFactory = nonceFactory,
        )
    }
}

/**
 * This task must be run *before* the changes have been persisted. It cannot be run using the task
 * manager as it must be run immediately inside another task.
 */
abstract class ReflectGroupSyncUpdateImmediateTask(
    groupIdentity: GroupIdentity,
) : ReflectGroupSyncUpdateBaseTask<Unit, Unit>(
    groupIdentity = groupIdentity,
) {
    suspend fun reflect(handle: ActiveTaskCodec): ReflectionResult<Unit> {
        return runTransaction(handle)
    }

    /**
     * This method is run inside the transaction before the sync messages are sent.
     */
    protected abstract fun checkForDataRaces(currentData: GroupModelData)

    /**
     * The default precondition for immediate tasks is checking that the group is not deleted and
     * the user state is member.
     */
    override fun checkInPrecondition(currentData: GroupModelData): Boolean {
        return currentData.isMember
    }

    override val runInsideTransaction: suspend (handle: ActiveTaskCodec) -> Unit = { handle ->
        val groupModelData = groupModel.data
        check(groupModelData != null) { "Group model data cannot be null at this point" }
        checkForDataRaces(groupModelData)

        reflectGroupSync(handle)
    }

    override fun getMemberStateChanges(): Map<String, MemberStateChange> = emptyMap()

    override val runAfterSuccessfulTransaction: suspend (transactionResult: Unit) -> Unit = {
        // Nothing to do
    }

    /**
     * This reflection task can be used to reflect that the user has been kicked.
     */
    class ReflectUserKicked(
        groupIdentity: GroupIdentity,
    ) : ReflectGroupSyncUpdateImmediateTask(
        groupIdentity = groupIdentity,
    ) {
        override val type = "ReflectUserKicked"

        override fun checkForDataRaces(currentData: GroupModelData) {
            // Nothing to check here as this check is already done in the precondition
        }

        override fun getGroupSync(): Group = group {
            groupIdentity = groupModel.groupIdentity.toProtobuf()
            userState = Group.UserState.KICKED
        }
    }

    class ReflectMemberLeft(
        private val leftMemberIdentity: IdentityString,
        groupIdentity: GroupIdentity,
    ) : ReflectGroupSyncUpdateImmediateTask(
        groupIdentity = groupIdentity,
    ) {
        override val type = "ReflectMemberLeft"

        override fun checkForDataRaces(currentData: GroupModelData) {
            if (!currentData.otherMembers.contains(leftMemberIdentity)) {
                logger.warn("Group race occurred: Left member has already been removed from data")
            }
        }

        override fun getGroupSync(): Group {
            val groupModelData = groupModel.data
                ?: throw IllegalStateException("Group model data is null")

            val updatedMembers = groupModelData.otherMembers.filter { it != leftMemberIdentity }

            return group {
                groupIdentity = groupModel.groupIdentity.toProtobuf()
                memberIdentities = identities {
                    this.identities.clear()
                    this.identities.addAll(updatedMembers)
                }
            }
        }

        override fun getMemberStateChanges(): Map<String, MemberStateChange> {
            return mapOf(leftMemberIdentity to MemberStateChange.LEFT)
        }
    }

    class ReflectGroupName(
        private val newGroupName: String,
        groupIdentity: GroupIdentity,
    ) : ReflectGroupSyncUpdateImmediateTask(
        groupIdentity = groupIdentity,
    ) {
        override val type = "ReflectGroupName"

        override fun checkForDataRaces(currentData: GroupModelData) {
            if (newGroupName == currentData.name) {
                logger.warn("Group race occurred: The name did not change")
            }
        }

        override fun getGroupSync(): Group = group {
            groupIdentity = this@ReflectGroupName.groupModel.groupIdentity.toProtobuf()
            name = newGroupName
        }
    }

    class ReflectGroupSetProfilePicture(
        private val blobId: ByteArray,
        private val encryptionKey: ByteArray,
        private val blobNonce: ByteArray,
        groupIdentity: GroupIdentity,
        private val profilePictureBlob: ByteArray,
    ) : ReflectGroupSyncUpdateImmediateTask(
        groupIdentity = groupIdentity,
    ) {
        override val type = "ReflectGroupDeleteProfilePicture"

        override fun checkForDataRaces(currentData: GroupModelData) {
            if (fileService.getGroupProfilePictureStream(groupModel.getDatabaseId()).contentEquals(profilePictureBlob)) {
                logger.warn("Group race occurred: The profile picture did not change")
            }
        }

        override fun getGroupSync(): Group = group {
            groupIdentity = this@ReflectGroupSetProfilePicture.groupModel.groupIdentity.toProtobuf()
            profilePicture = deltaImage {
                updated = image {
                    blob = blob {
                        id = blobId.toByteString()
                        key = encryptionKey.toByteString()
                        nonce = blobNonce.toByteString()
                    }
                }
            }
        }
    }

    class ReflectGroupDeleteProfilePicture(
        groupIdentity: GroupIdentity,
    ) : ReflectGroupSyncUpdateImmediateTask(
        groupIdentity = groupIdentity,
    ) {
        override val type = "ReflectGroupDeleteProfilePicture"

        override fun checkForDataRaces(currentData: GroupModelData) {
            if (!fileService.hasGroupProfilePicture(groupModel.getDatabaseId())) {
                logger.warn("Group race occurred: There is no group profile picture for this group")
            }
        }

        override fun getGroupSync(): Group = group {
            groupIdentity =
                this@ReflectGroupDeleteProfilePicture.groupModel.groupIdentity.toProtobuf()
            profilePicture = deltaImage {
                removed = unit { }
            }
        }
    }
}

abstract class ReflectGroupSyncUpdateTask(
    groupIdentity: GroupIdentity,
) : ReflectGroupSyncUpdateBaseTask<Unit, Unit>(
    groupIdentity = groupIdentity,
),
    ActiveTask<Unit>,
    PersistableTask {
    /**
     * Return true if the change that should be reflected still matches the current data. Note that
     * if a task performs several changes, then *all* of the new values must be equal to
     * [currentData].
     */
    protected abstract fun isChangeValid(currentData: GroupModelData): Boolean

    final override fun checkInPrecondition(currentData: GroupModelData): Boolean = isChangeValid(currentData)

    /**
     *  Not changing any member states in [ReflectGroupSyncUpdateTask].
     *  Use the tasks of [ReflectGroupSyncUpdateImmediateTask] for that.
     */
    final override fun getMemberStateChanges(): Map<String, MemberStateChange> = emptyMap()

    final override fun getGroupSync(): Group = group {
        this.groupIdentity = groupModel.groupIdentity.toProtobuf()
        this.buildGroupSyncChanges()
    }

    protected abstract val buildGroupSyncChanges: GroupKt.Dsl.() -> Unit

    override val runInsideTransaction: suspend (handle: ActiveTaskCodec) -> Unit = { handle ->
        reflectGroupSync(handle)
    }

    override val runAfterSuccessfulTransaction: suspend (transactionResult: Unit) -> Unit = {
        // Nothing to do
    }

    final override suspend fun invoke(handle: ActiveTaskCodec) {
        if (!multiDeviceManager.isMultiDeviceActive) {
            logger.warn("Cannot reflect group sync update of type {} if md is not active", type)
            return
        }

        runTransaction(handle)
    }

    class ReflectNotificationTriggerPolicyOverrideUpdate(
        private val newNotificationTriggerPolicyOverride: GroupNotificationTriggerPolicyOverride?,
        groupIdentity: GroupIdentity,
    ) : ReflectGroupSyncUpdateTask(
        groupIdentity = groupIdentity,
    ) {
        override val type: String = "ReflectNotificationTriggerPolicyOverrideUpdate"

        override fun isChangeValid(currentData: GroupModelData): Boolean =
            currentData.notificationTriggerPolicyOverride == newNotificationTriggerPolicyOverride

        override val buildGroupSyncChanges: GroupKt.Dsl.() -> Unit = {
            this.notificationTriggerPolicyOverride = newNotificationTriggerPolicyOverride.toProtobuf()
        }

        override fun serialize(): SerializableTaskData =
            ReflectNotificationTriggerPolicyOverrideUpdateDataV2(
                notificationTriggerPolicyOverridePolicy = newNotificationTriggerPolicyOverride?.policy?.serializedValue,
                notificationTriggerPolicyOverrideExpiresAt = newNotificationTriggerPolicyOverride?.expiresAt?.toEpochMilli(),
                groupIdentity = groupModel.groupIdentity,
            )

        /**
         * Note that this is only used for backwards compatibility as we used to persist this task data. We still need to be able to restore this task
         * data as some clients may have persisted it.
         */
        @Deprecated("Use ReflectNotificationTriggerPolicyOverrideUpdateDataV2 instead")
        @Serializable
        data class ReflectNotificationTriggerPolicyOverrideUpdateData(
            private val newNotificationTriggerPolicyOverride: Long?,
            private val groupIdentity: GroupIdentity,
        ) : SerializableTaskData {
            override fun createTask(): Task<*, TaskCodec> =
                ReflectNotificationTriggerPolicyOverrideUpdate(
                    newNotificationTriggerPolicyOverride = deserializeGroupNotificationTriggerPolicyOverride(),
                    groupIdentity = groupIdentity,
                )

            private fun deserializeGroupNotificationTriggerPolicyOverride(): GroupNotificationTriggerPolicyOverride? {
                if (newNotificationTriggerPolicyOverride == null) {
                    return null
                }

                return when {
                    // In case the value is -1, the group is indefinitely muted
                    newNotificationTriggerPolicyOverride == -1L -> GroupNotificationTriggerPolicyOverride(
                        policy = GroupNotificationTriggerPolicyOverridePolicy.NEVER,
                        expiresAt = null,
                    )

                    // In case the value is -2, the group is indefinitely muted except mentioned
                    newNotificationTriggerPolicyOverride == -2L -> GroupNotificationTriggerPolicyOverride(
                        policy = GroupNotificationTriggerPolicyOverridePolicy.MENTIONED,
                        expiresAt = null,
                    )

                    // In case the value is greater than 0, it denotes the timestamp until when the group is muted
                    newNotificationTriggerPolicyOverride > 0 -> GroupNotificationTriggerPolicyOverride(
                        policy = GroupNotificationTriggerPolicyOverridePolicy.NEVER,
                        expiresAt = Instant.ofEpochMilli(newNotificationTriggerPolicyOverride),
                    )

                    // In case of 0 or any negative value other than -1 and -2, there is no notification trigger policy override set
                    else -> null
                }
            }
        }

        @Serializable
        data class ReflectNotificationTriggerPolicyOverrideUpdateDataV2(
            private val notificationTriggerPolicyOverridePolicy: Int?,
            private val notificationTriggerPolicyOverrideExpiresAt: Long?,
            private val groupIdentity: GroupIdentity,
        ) : SerializableTaskData {
            override fun createTask(): Task<*, TaskCodec> =
                ReflectNotificationTriggerPolicyOverrideUpdate(
                    newNotificationTriggerPolicyOverride = deserializeGroupNotificationTriggerPolicyOverride(),
                    groupIdentity = groupIdentity,
                )

            private fun deserializeGroupNotificationTriggerPolicyOverride(): GroupNotificationTriggerPolicyOverride? {
                if (notificationTriggerPolicyOverridePolicy == null) {
                    return null
                }

                val policy = GroupNotificationTriggerPolicyOverridePolicy.deserialize(notificationTriggerPolicyOverridePolicy)
                    ?: error("Could not deserialize group notification trigger policy with value $notificationTriggerPolicyOverridePolicy")
                val expiresAt = notificationTriggerPolicyOverrideExpiresAt?.let { expiresAt -> Instant.ofEpochMilli(expiresAt) }

                return GroupNotificationTriggerPolicyOverride(
                    policy = policy,
                    expiresAt = expiresAt,
                )
            }
        }
    }

    /**
     * Note that this task currently just reflects the current conversation category state of the group as the conversation category is not part of
     * the group model.
     */
    class ReflectGroupConversationCategoryUpdateTask(
        groupIdentity: GroupIdentity,
        private val isPrivateChat: Boolean,
    ) : ReflectGroupSyncUpdateTask(
        groupIdentity = groupIdentity,
    ),
        KoinComponent {
        private val conversationCategoryService: ConversationCategoryService by inject()

        override val type: String = "ReflectGroupConversationCategoryUpdateTask"

        override fun isChangeValid(currentData: GroupModelData): Boolean {
            // The change must be correct, otherwise we must not reflect that change
            return conversationCategoryService.isMarkedAsPrivate(
                conversationId = GroupConversationId(groupDatabaseId = groupModel.getDatabaseId()),
            ) == isPrivateChat
        }

        override val buildGroupSyncChanges: GroupKt.Dsl.() -> Unit = {
            conversationCategory = if (isPrivateChat) {
                ConversationCategory.PROTECTED
            } else {
                ConversationCategory.DEFAULT
            }
        }

        override fun serialize(): SerializableTaskData {
            return ReflectGroupConversationCategoryData(
                groupIdentity = groupModel.groupIdentity,
                isPrivateChat = isPrivateChat,
            )
        }

        @Serializable
        data class ReflectGroupConversationCategoryData(
            private val groupIdentity: GroupIdentity,
            private val isPrivateChat: Boolean,
        ) : SerializableTaskData {
            override fun createTask(): Task<*, TaskCodec> {
                return ReflectGroupConversationCategoryUpdateTask(
                    groupIdentity = groupIdentity,
                    isPrivateChat = isPrivateChat,
                )
            }
        }
    }

    /**
     * Reflect the conversation visibility.
     */
    class ReflectGroupConversationVisibilityUpdate(
        private val conversationVisibility: ConversationVisibility,
        groupIdentity: GroupIdentity,
    ) : ReflectGroupSyncUpdateTask(
        groupIdentity = groupIdentity,
    ) {
        override val type = "ReflectGroupConversationVisibilityUpdate"

        override fun isChangeValid(currentData: GroupModelData): Boolean =
            currentData.conversationVisibility == conversationVisibility

        override val buildGroupSyncChanges: GroupKt.Dsl.() -> Unit = {
            this.conversationVisibility = when (this@ReflectGroupConversationVisibilityUpdate.conversationVisibility) {
                ConversationVisibility.NORMAL -> ProtocolsConversationVisibility.NORMAL
                ConversationVisibility.ARCHIVED -> ProtocolsConversationVisibility.ARCHIVED
                ConversationVisibility.PINNED -> ProtocolsConversationVisibility.PINNED
            }
        }

        override fun serialize(): SerializableTaskData = ReflectGroupConversationVisibilityUpdateData(
            conversationVisibilitySerialized = conversationVisibility.serializedValue,
            groupIdentity = groupIdentity,
        )

        @Serializable
        data class ReflectGroupConversationVisibilityUpdateData(
            private val conversationVisibilitySerialized: Int,
            private val groupIdentity: GroupIdentity,
        ) : SerializableTaskData {
            override fun createTask(): Task<*, TaskCodec> =
                ReflectGroupConversationVisibilityUpdate(
                    conversationVisibility = requireConversationVisibility(),
                    groupIdentity = groupIdentity,
                )

            private fun requireConversationVisibility() = ConversationVisibility
                .deserialize(conversationVisibilitySerialized)
                ?: error("Could not deserialize conversation visibility from value $conversationVisibilitySerialized")
        }
    }

    /**
     * Note: This is only used to create the correct fully qualified name for the contained task data.
     */
    @Suppress("unused")
    object ReflectGroupConversationVisibilityArchiveUpdate {
        /**
         * There used to be a separate task for reflecting the information whether a group was archived or not. This task does not exist anymore and
         * is replaced by [ReflectGroupConversationVisibilityUpdate]. This task data may still be persisted on some devices. In that case we create
         * now a [ReflectGroupConversationVisibilityUpdate] from this task.
         *
         * TODO(ANDR-4974): Remove this.
         */
        @Deprecated("This data should not be used anymore. It just exists in case it has been persisted.")
        @Serializable
        data class ReflectGroupConversationVisibilityArchiveUpdateData(
            private val isArchived: Boolean,
            private val groupIdentity: GroupIdentity,
        ) : SerializableTaskData {
            override fun createTask(): Task<*, TaskCodec> {
                val conversationVisibility = when (isArchived) {
                    true -> ConversationVisibility.ARCHIVED
                    false -> ConversationVisibility.NORMAL
                }
                return ReflectGroupConversationVisibilityUpdate(
                    conversationVisibility = conversationVisibility,
                    groupIdentity = groupIdentity,
                )
            }
        }
    }

    /**
     * Note: This is only used to create the correct fully qualified name for the contained task data.
     */
    @Suppress("unused")
    object ReflectGroupConversationVisibilityPinnedUpdate {

        /**
         * There used to be a separate task for reflecting the information whether a group was archived or not. This task does not exist anymore and
         * is replaced by [ReflectGroupConversationVisibilityUpdate]. This task data may still be persisted on some devices. In that case we create
         * now a [ReflectGroupConversationVisibilityUpdate] from this task.
         *
         * TODO(ANDR-4974): Remove this.
         */
        @Deprecated("This data should not be used anymore. It just exists in case it has been persisted.")
        @Serializable
        data class ReflectGroupConversationVisibilityPinnedUpdateData(
            private val isPinned: Boolean,
            private val groupIdentity: GroupIdentity,
        ) : SerializableTaskData {
            override fun createTask(): Task<*, TaskCodec> {
                val conversationVisibility = if (isPinned) {
                    ConversationVisibility.PINNED
                } else {
                    ConversationVisibility.NORMAL
                }
                return ReflectGroupConversationVisibilityUpdate(
                    conversationVisibility = conversationVisibility,
                    groupIdentity = groupIdentity,
                )
            }
        }
    }
}

abstract class ReflectGroupSyncUpdateFromLocal<T, U>(
    groupIdentity: GroupIdentity,
) : ReflectGroupSyncUpdateBaseTask<T, ReflectionResult<U>>(
    groupIdentity = groupIdentity,
),
    ActiveTask<ReflectionResult<U>> {
    override suspend fun invoke(handle: ActiveTaskCodec): ReflectionResult<U> {
        if (!multiDeviceManager.isMultiDeviceActive) {
            logger.warn("Cannot reflect group sync update from local of type {} when md is not active", type)
            return ReflectionResult.MultiDeviceNotActive()
        }

        try {
            return reflectSync(handle)
        } catch (e: TransactionScope.TransactionException) {
            logger.error("Could not reflect as the precondition failed", e)
            return ReflectionResult.PreconditionFailed(e)
        }
    }
}

class ReflectLocalGroupUpdate(
    private val updatedName: String?,
    private val addMembers: Set<String>,
    private val removeMembers: Set<String>,
    private val profilePictureChange: GroupChanges.ProfilePictureChange,
    private val uploadGroupProfilePicture: (GroupChanges.ProfilePictureChange) -> GroupProfilePictureUploadResult?,
    groupIdentity: GroupIdentity,
) : ReflectGroupSyncUpdateFromLocal<GroupProfilePictureUploadResult?, GroupProfilePictureUploadResult?>(
    groupIdentity = groupIdentity,
),
    KoinComponent {
    private val contactModelRepository: ContactModelRepository by inject()

    override val type = "ReflectGroupLocalUpdate"

    private var groupProfilePictureUploadResultSuccess: GroupProfilePictureUploadResult.Success? = null

    override fun checkInPrecondition(currentData: GroupModelData): Boolean {
        return addMembers.all { contactModelRepository.getByIdentity(it) != null } &&
            removeMembers.all { contactModelRepository.getByIdentity(it) != null }
    }

    override fun getGroupSync(): Group {
        val groupModelData = groupModel.data ?: run {
            throw IllegalStateException("Group model data cannot be null at this point")
        }

        val updatedMembers = groupModelData.otherMembers + addMembers - removeMembers

        return group {
            groupIdentity = this@ReflectLocalGroupUpdate.groupModel.groupIdentity.toProtobuf()
            updatedName?.let {
                name = it
            }
            memberIdentities = identities {
                identities.addAll(updatedMembers)
            }
            when (profilePictureChange) {
                is GroupChanges.ProfilePictureChange.Set -> {
                    groupProfilePictureUploadResultSuccess?.let {
                        profilePicture = deltaImage {
                            updated = image {
                                type = Image.Type.JPEG
                                blob = blob {
                                    id = it.blobId.toByteString()
                                    nonce = ProtocolDefines.GROUP_PHOTO_NONCE.toByteString()
                                    key = it.encryptionKey.toByteString()
                                }
                            }
                        }
                    } ?: {
                        // Note: This should never happen as the reflection is only performed when the upload was successful
                        error("Not reflecting group picture as upload failed")
                    }
                }

                GroupChanges.ProfilePictureChange.Remove -> {
                    profilePicture = deltaImage {
                        removed = unit { }
                    }
                }

                GroupChanges.ProfilePictureChange.NoChange -> Unit
            }
        }
    }

    override val runInsideTransaction: suspend (handle: ActiveTaskCodec) -> GroupProfilePictureUploadResult? = { handle ->
        val uploadResult = uploadGroupProfilePicture(profilePictureChange)

        if (uploadResult is GroupProfilePictureUploadResult.Success || uploadResult == null) {
            groupProfilePictureUploadResultSuccess = uploadResult
            reflectGroupSync(handle)
        }

        uploadResult
    }

    override val runAfterSuccessfulTransaction: suspend (GroupProfilePictureUploadResult?) -> ReflectionResult<GroupProfilePictureUploadResult?> = {
        ReflectionResult.Success(it)
    }

    override fun getMemberStateChanges(): Map<String, MemberStateChange> {
        val memberStateChanges: MutableMap<String, MemberStateChange> = mutableMapOf()

        addMembers.forEach { memberStateChanges[it] = MemberStateChange.ADDED }
        removeMembers.forEach { memberStateChanges[it] = MemberStateChange.KICKED }

        return memberStateChanges
    }
}

class ReflectLocalGroupLeaveOrDisband(
    groupIdentity: GroupIdentity,
) : ReflectGroupSyncUpdateFromLocal<Unit, Unit>(
    groupIdentity = groupIdentity,
) {
    override val type = "ReflectLocalGroupLeave"

    override fun checkInPrecondition(currentData: GroupModelData) = currentData.isMember

    override fun getGroupSync() = group {
        groupIdentity = groupModel.groupIdentity.toProtobuf()
        userState = Group.UserState.LEFT
    }

    override val runInsideTransaction: suspend (handle: ActiveTaskCodec) -> Unit = { handle ->
        reflectGroupSync(handle)
    }

    override val runAfterSuccessfulTransaction: suspend (transactionResult: Unit) -> ReflectionResult<Unit> = {
        ReflectionResult.Success(Unit)
    }

    override fun getMemberStateChanges(): Map<String, MemberStateChange> = emptyMap()
}
