package ch.threema.app.processors.incomingcspmessage.conversation

import ch.threema.app.eventbus.GlobalEventBuses
import ch.threema.app.eventbus.events.MessageEvent
import ch.threema.app.managers.ServiceManager
import ch.threema.app.processors.incomingcspmessage.IncomingCspMessageSubTask
import ch.threema.app.processors.incomingcspmessage.ReceiveStepsResult
import ch.threema.app.processors.incomingcspmessage.groupcontrol.runCommonGroupReceiveSteps
import ch.threema.app.utils.MimeUtil
import ch.threema.base.utils.getThreemaLogger
import ch.threema.data.datatypes.ConversationVisibility
import ch.threema.data.models.GroupModel
import ch.threema.domain.protocol.csp.messages.file.FileData
import ch.threema.domain.protocol.csp.messages.file.GroupFileMessage
import ch.threema.domain.taskmanager.ActiveTaskCodec
import ch.threema.domain.taskmanager.TriggerSource
import ch.threema.storage.models.MessageType
import ch.threema.storage.models.data.media.FileDataModel
import ch.threema.storage.models.group.GroupMessageModel
import java.time.Instant
import java.util.UUID
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject

private val logger = getThreemaLogger("IncomingGroupFileMessageTask")

class IncomingGroupFileMessageTask(
    groupFileMessage: GroupFileMessage,
    triggerSource: TriggerSource,
    serviceManager: ServiceManager,
) : IncomingCspMessageSubTask<GroupFileMessage>(
    groupFileMessage,
    triggerSource,
    serviceManager,
),
    KoinComponent {
    private val globalEventBuses: GlobalEventBuses by inject()
    private val messageService = serviceManager.messageService
    private val groupService = serviceManager.groupService
    private val groupModelRepository by lazy { serviceManager.modelRepositories.groups }

    override suspend fun executeMessageStepsFromRemote(handle: ActiveTaskCodec): ReceiveStepsResult {
        val groupModel: GroupModel = runCommonGroupReceiveSteps(message, handle, serviceManager)
            ?: run {
                logger.warn("Discarding message ${message.messageId}: Could not find group for incoming group file message")
                return ReceiveStepsResult.DISCARD
            }
        return processIncomingMessage(
            preCheckedGroupModel = groupModel,
            triggerSource = TriggerSource.REMOTE,
        )
    }

    override suspend fun executeMessageStepsFromSync() = processIncomingMessage(
        preCheckedGroupModel = null,
        triggerSource = TriggerSource.SYNC,
    )

    private fun processIncomingMessage(
        preCheckedGroupModel: GroupModel?,
        triggerSource: TriggerSource,
    ): ReceiveStepsResult {
        // 0: Group model must exist locally at this point
        val groupModel: GroupModel = preCheckedGroupModel
            ?: groupModelRepository.getByCreatorIdentityAndId(
                message.groupCreator,
                message.apiGroupId,
            ) ?: run {
            logger.error("Discarding message ${message.messageId}: Could not find group model with api id ${message.apiGroupId}")
            return ReceiveStepsResult.DISCARD
        }

        // 1: Check if the group message already exists locally (from previous run(s) of this task).
        //    If so, cancel and accept that the download for the content(s) might not be complete.
        messageService.getGroupMessageModel(
            message.messageId,
            message.groupCreator,
            message.apiGroupId,
        )?.run {
            // In this case the message has been processed earlier. Therefore we consider this as success. This causes the message to be reflected.
            logger.info("Message model already exists. Aborting successfully.")
            return ReceiveStepsResult.SUCCESS
        }

        val fileData: FileData = message.fileData ?: run {
            logger.error("Discarding message ${message.messageId}: Missing file data")
            return ReceiveStepsResult.DISCARD
        }

        // 2. Map the FileData object to a FileDataModel instance (field "downloaded" is false)
        val fileDataModel: FileDataModel = FileDataModel.fromIncomingFileData(fileData)

        // 3. Create the actual AbstractMessageModel containing the file and sender information
        val messageModel: GroupMessageModel = createGroupMessageModelFromFileMessage(
            groupFileMessage = message,
            fileDataModel = fileDataModel,
            fileData = fileData,
            groupModel = groupModel,
        )

        val oldGroupModel = groupService.getByGroupMessage(message) ?: run {
            logger.error("Old group model is null")
            return ReceiveStepsResult.DISCARD
        }

        // 4. Un-archive group if currently archived
        if (triggerSource == TriggerSource.REMOTE) {
            groupModel.data?.let { groupModelData ->
                if (groupModelData.conversationVisibility == ConversationVisibility.ARCHIVED) {
                    groupModel.setConversationVisibilityFromLocalOrRemote(ConversationVisibility.NORMAL)
                }
            }
        }

        // 5. Bump last updated timestamp if necessary to move conversation up in list
        if (message.bumpLastUpdate()) {
            groupService.bumpLastUpdate(oldGroupModel)
        }

        // 6. Save message model and inform event bus about new message
        messageService.save(messageModel)
        globalEventBuses.messages.emit(MessageEvent.NewMessage(messageModel))

        // 7. Download thumbnail and content blob (if auto download enabled)
        //    We still return SUCCESS even if the blobs could net be downloaded
        processMediaContent(fileData, messageModel)

        return ReceiveStepsResult.SUCCESS
    }

    /**
     *  @return A new instance of `AbstractMessageModel` with type [MessageType.FILE] containing
     *  the `body` and `dataObject` from the passed file information.
     */
    private fun createGroupMessageModelFromFileMessage(
        groupFileMessage: GroupFileMessage,
        fileDataModel: FileDataModel,
        fileData: FileData,
        groupModel: GroupModel,
    ): GroupMessageModel {
        return GroupMessageModel().apply {
            uid = UUID.randomUUID().toString()
            messageId = message.messageId

            identity = groupFileMessage.fromIdentity
            groupId = groupModel.getDatabaseId().toInt()

            this.fileData = fileDataModel
            messageContentsType = MimeUtil.getContentTypeFromFileData(fileDataModel)

            postedAt = message.timestamp
            createdAt = Instant.now()

            messageFlags = message.messageFlags

            isOutbox = false
            isSaved = true

            correlationId = fileData.correlationId
            forwardSecurityMode = message.forwardSecurityMode
        }
    }

    /**
     *  **Synchronously**
     *
     *  Attempt to download the thumbnail and the actual media content. Even if
     *  the thumbnail download failed, we try to download the actual blob contents.
     */
    private fun processMediaContent(fileData: FileData, messageModel: GroupMessageModel) {
        runCatching {
            messageService.downloadThumbnailIfPresent(fileData, messageModel)
        }.onSuccess { thumbnailWasDownloaded: Boolean ->
            if (thumbnailWasDownloaded) {
                globalEventBuses.messages.emit(MessageEvent.MessagesUpdated(messageModel))
            }
        }.onFailure { throwable ->
            logger.error("Unable to download thumbnail blob", throwable)
        }
        if (messageService.shouldAutoDownload(messageModel)) {
            runCatching {
                messageService.downloadMediaMessage(messageModel, null)
            }.onFailure { throwable ->
                // a failed blob auto-download should not be considered a failure as the user can try again manually
                logger.error("Unable to auto-download blob", throwable)
            }
        }
    }
}
