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.utils.MimeUtil
import ch.threema.base.utils.getThreemaLogger
import ch.threema.domain.models.AcquaintanceLevel
import ch.threema.domain.protocol.csp.messages.file.FileData
import ch.threema.domain.protocol.csp.messages.file.FileMessage
import ch.threema.domain.taskmanager.ActiveTaskCodec
import ch.threema.domain.taskmanager.TriggerSource
import ch.threema.storage.models.MessageModel
import ch.threema.storage.models.MessageType
import ch.threema.storage.models.data.media.FileDataModel
import java.time.Instant
import java.util.UUID
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject

private val logger = getThreemaLogger("IncomingContactFileMessageTask")

class IncomingContactFileMessageTask(
    fileMessage: FileMessage,
    triggerSource: TriggerSource,
    serviceManager: ServiceManager,
) : IncomingCspMessageSubTask<FileMessage>(
    fileMessage,
    triggerSource,
    serviceManager,
),
    KoinComponent {
    private val globalEventBuses: GlobalEventBuses by inject()
    private val messageService = serviceManager.messageService
    private val contactService = serviceManager.contactService
    private val contactRepository = serviceManager.modelRepositories.contacts

    override suspend fun executeMessageStepsFromRemote(handle: ActiveTaskCodec) =
        processIncomingMessage(
            triggerSource = TriggerSource.REMOTE,
        )

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

    private fun processIncomingMessage(triggerSource: TriggerSource): ReceiveStepsResult {
        // 0: Contact must exist locally at this point
        if (!contactRepository.existsByIdentity(message.fromIdentity)) {
            logger.error("Discarding message ${message.messageId}: Sender contact with identity ${message.fromIdentity} does not exist locally.")
            return ReceiveStepsResult.DISCARD
        }

        // 1: Check if the 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.getContactMessageModel(
            message.messageId,
            message.fromIdentity,
        )?.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: MessageModel = createMessageModelFromFileMessage(
            fileMessage = message,
            fileDataModel = fileDataModel,
            fileData = fileData,
        )

        // 4. Un-archive the contact and set the acquaintance level to "direct" because it is a 1:1 chat now
        if (triggerSource == TriggerSource.REMOTE) {
            contactService.unarchive(message.fromIdentity, triggerSource)
            contactService.setAcquaintanceLevel(
                message.fromIdentity,
                AcquaintanceLevel.DIRECT,
            )
        }

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

        // 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 createMessageModelFromFileMessage(
        fileMessage: FileMessage,
        fileDataModel: FileDataModel,
        fileData: FileData,
    ): MessageModel {
        return MessageModel().apply {
            uid = UUID.randomUUID().toString()
            messageId = message.messageId

            identity = fileMessage.fromIdentity

            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: MessageModel) {
        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)
            }
        }
    }
}
