package ch.threema.storage.models

import ch.threema.app.utils.MimeUtil
import ch.threema.app.utils.QuoteUtil
import ch.threema.base.utils.getThreemaLogger
import ch.threema.domain.models.MessageId
import ch.threema.domain.protocol.blob.BlobScope
import ch.threema.domain.protocol.csp.messages.file.FileData
import ch.threema.domain.protocol.csp.messages.fs.ForwardSecurityMode
import ch.threema.domain.types.IdentityString
import ch.threema.domain.types.MessageIdString
import ch.threema.domain.types.MessageUid
import ch.threema.storage.models.data.DisplayTag
import ch.threema.storage.models.data.LocationDataModel
import ch.threema.storage.models.data.MessageContentsType
import ch.threema.storage.models.data.MessageDataInterface
import ch.threema.storage.models.data.media.FileDataModel
import ch.threema.storage.models.data.media.PollDataModel
import ch.threema.storage.models.data.status.ForwardSecurityStatusDataModel
import ch.threema.storage.models.data.status.GroupCallStatusDataModel
import ch.threema.storage.models.data.status.GroupStatusDataModel
import ch.threema.storage.models.data.status.StatusDataModel
import ch.threema.storage.models.data.status.VoipStatusDataModel
import ch.threema.storage.models.group.GroupMessageModel
import java.time.Instant

private val logger = getThreemaLogger("AbstractMessageModel")

abstract class AbstractMessageModel
@JvmOverloads
internal constructor(
    open var isStatusMessage: Boolean = false,
) {
    /**
     * The message's id, unique per message type.
     */
    open var id: Int = 0

    /**
     * The message's uid, globally unique.
     */
    open var uid: MessageUid? = null

    /**
     * The chat protocol message id assigned by the sender, represented as a string.
     * Use [messageId] instead if possible.
     */
    open var apiMessageId: MessageIdString? = null

    /**
     * The chat protocol message id assigned by the sender.
     */
    var messageId: MessageId?
        get() = apiMessageId?.let(MessageId::fromString)
        set(value) {
            apiMessageId = value?.toString()
        }

    /**
     * The associated identity: Either the recipient
     * (for outgoing messages) or the sender (for incoming messages).
     */
    open var identity: IdentityString? = null

    /**
     * Whether the message is an outgoing one, i.e., sent by the user
     */
    open var isOutbox: Boolean = false
        set(value) {
            field = value
            if (value) {
                // Outgoing messages can't be unread
                isRead = true
            }
        }

    open var type: MessageType? = null

    open var correlationId: String? = null

    open var body: String? = null

    open var isRead: Boolean = false
        get() = field || isOutbox

    open var isSaved: Boolean = false

    open var state: MessageState? = null

    var rawPostedAt: Instant? = null
        private set

    open var postedAt: Instant?
        get() = rawPostedAt ?: createdAt
        set(value) {
            rawPostedAt = value
        }

    open var createdAt: Instant? = null

    open var deliveredAt: Instant? = null

    open var readAt: Instant? = null

    open var modifiedAt: Instant? = null

    open var editedAt: Instant? = null

    open var deletedAt: Instant? = null

    var caption: String? = null
        get() = when (type) {
            MessageType.FILE -> fileData.caption
            MessageType.LOCATION -> locationData.poi?.getCaptionOrNull()
            else -> field
        }

    var quotedMessageId: String? = null

    @MessageContentsType
    var messageContentsType: Int = 0

    var messageFlags: Int = 0

    var forwardSecurityMode: ForwardSecurityMode? = null

    @DisplayTag
    var displayTags: Int = 0

    var bodyAndQuotedMessageId: String?
        get() = body
            ?.let { body ->
                quotedMessageId?.let { quotedMessageId ->
                    QuoteUtil.quote(body, quotedMessageId)
                }
                    ?: body
            }
        set(value) {
            if (QuoteUtil.isQuoteV2(value)) {
                QuoteUtil.addBodyAndQuotedMessageId(this, value)
            } else {
                body = value
                quotedMessageId = null
            }
        }

    val isEdited: Boolean
        get() = editedAt != null

    val isDeleted: Boolean
        get() = deletedAt != null

    protected var dataObject: MessageDataInterface? = null

    var locationData: LocationDataModel
        get() = getOrSetDataModel(MessageType.LOCATION, LocationDataModel::fromStringOrDefault)
            ?: emptyLocationDataModel
        set(value) {
            setDataModel(MessageType.LOCATION, value)
        }

    var voipStatusData: VoipStatusDataModel?
        get() = getOrSetDataModel(
            expectedType = MessageType.VOIP_STATUS,
            convert = { body ->
                StatusDataModel.deserialize(body) as VoipStatusDataModel?
            },
        )
        set(value) {
            setDataModel(MessageType.VOIP_STATUS, value, StatusDataModel::serialize)
        }

    var groupCallStatusData: GroupCallStatusDataModel?
        get() = getOrSetDataModel(
            expectedType = MessageType.GROUP_CALL_STATUS,
            convert = { body ->
                StatusDataModel.deserialize(body) as GroupCallStatusDataModel?
            },
        )
        set(value) {
            setDataModel(MessageType.GROUP_CALL_STATUS, value, StatusDataModel::serialize)
        }

    var forwardSecurityStatusData: ForwardSecurityStatusDataModel?
        get() = getOrSetDataModel(
            expectedType = MessageType.FORWARD_SECURITY_STATUS,
            convert = { body ->
                StatusDataModel.deserialize(body) as ForwardSecurityStatusDataModel?
            },
        )
        set(value) {
            setDataModel(MessageType.FORWARD_SECURITY_STATUS, value, StatusDataModel::serialize)
        }

    var groupStatusData: GroupStatusDataModel?
        get() = getOrSetDataModel(
            expectedType = MessageType.GROUP_STATUS,
            convert = { body ->
                StatusDataModel.deserialize(body) as GroupStatusDataModel?
            },
        )
        set(value) {
            setDataModel(MessageType.GROUP_STATUS, value, StatusDataModel::serialize)
        }

    var pollData: PollDataModel
        get() = getOrSetDataModel(MessageType.POLL, PollDataModel::deserialize)
            ?: emptyPollDataModel
        set(value) {
            setDataModel(MessageType.POLL, value)
        }

    var fileData: FileDataModel
        get() = getOrSetDataModel(MessageType.FILE, FileDataModel::create)
            ?: emptyFileDataModel
        set(value) {
            setDataModel(MessageType.FILE, value)
        }

    private inline fun <reified T : MessageDataInterface> getOrSetDataModel(
        expectedType: MessageType,
        convert: (String) -> T?,
    ): T? {
        if (dataObject == null || dataObject !is T) {
            if (checkBodyAndTypeForDataObjectAccess(expectedType)) {
                dataObject = body?.let(convert)
                if (dataObject !is T) {
                    logger.warn("Message data object is of wrong type for expected message type {}", expectedType)
                }
            }
        }
        return (dataObject as? T)
    }

    private fun checkBodyAndTypeForDataObjectAccess(expectedType: MessageType): Boolean {
        if (body == null) {
            return false
        }
        if (type == null) {
            logger.warn("Message type was expected to be {} but was null when trying to get data model", expectedType)
            return true
        }
        if (type != expectedType) {
            logger.warn("Message type was expected to be {} but was {} when trying to get data model", expectedType, type)
            return false
        }
        return true
    }

    private fun <T : MessageDataInterface> setDataModel(
        type: MessageType,
        dataObject: T?,
        convertToString: T.() -> String = { this.toString() },
    ) {
        this.type = type
        this.body = dataObject?.convertToString()
        this.dataObject = dataObject
    }

    /**
     * Call this to update the body field with the data model stuff
     */
    fun writeDataModelToBody() {
        dataObject?.let { dataObject ->
            body = dataObject.toString()
        }
    }

    val isAvailable: Boolean
        get() = when (type) {
            MessageType.FILE -> isOutbox || fileData.isDownloaded
            else -> true
        }

    val isStarred: Boolean
        get() = (displayTags and DisplayTag.DISPLAY_TAG_STARRED) == DisplayTag.DISPLAY_TAG_STARRED

    /**
     * This only makes sense (finds its use) when multi device is active.
     * The api call to download a blob without multi-device does not require a scope.
     *
     * @return The `BlobScope` to use when downloading the blob from the mirror server.
     * If the message if outgoing (`isOutbox()`) we use the local scope to download the
     * blob, as we know we received a reflected message. For every kind of incoming message we
     * need to use the public scope, as the blob might only be present on the usual blob
     * server (not mirror)
     */
    val blobScopeForDownload: BlobScope
        get() = if (isOutbox) BlobScope.Local else BlobScope.Public

    /**
     * This only makes sense (finds its use) when multi device is active.
     * The api call to mark a blob as done without multi-device does not require a scope.
     *
     * @return The `BlobScope` to use when marking the blob as "done" on the blob mirror server.
     * We only use the public scope here if the message is incoming and its not a group message. If
     * the message in outgoing, always use local scope (because we got the reflection only). If the
     * message is incoming but in a group, we use local scope (so that the blob is retained for other group members).
     */
    val blobScopeForMarkAsDone: BlobScope
        get() = if (!isOutbox && this !is GroupMessageModel) BlobScope.Public else BlobScope.Local

    fun isDownloadedVoiceMessage(): Boolean =
        type == MessageType.FILE &&
            MimeUtil.isAudioFile(fileData.getMimeType()) &&
            fileData.renderingType == FileData.RENDERING_MEDIA &&
            fileData.isDownloaded

    companion object {
        /**
         * The message id, unique per type.
         */
        const val COLUMN_ID = "id"

        /**
         * The message uid, unique globally.
         */
        const val COLUMN_UID = "uid"

        /**
         * The chat protocol message id assigned by the sender.
         */
        const val COLUMN_API_MESSAGE_ID = "apiMessageId"

        /**
         * Identity of the conversation partner.
         */
        const val COLUMN_IDENTITY = "identity"

        /**
         * Message direction. true = outgoing, false = incoming.
         */
        const val COLUMN_OUTBOX = "outbox"

        /**
         * Message type.
         */
        const val COLUMN_TYPE = "type"

        /**
         * Correlation ID.
         */
        const val COLUMN_CORRELATION_ID = "correlationId"

        /**
         * Message body.
         */
        const val COLUMN_BODY = "body"

        /**
         * Message caption.
         */
        const val COLUMN_CAPTION = "caption"

        /**
         * Whether this message has been read by the receiver.
         */
        const val COLUMN_IS_READ = "isRead"

        /**
         * Whether this message has been saved to the internal database.
         */
        const val COLUMN_IS_SAVED = "isSaved"

        /**
         * The message state.
         */
        const val COLUMN_STATE = "state"

        /**
         * When the message was created.
         */
        const val COLUMN_CREATED_AT = "createdAtUtc"

        /**
         * When the message was accepted by the server.
         */
        const val COLUMN_POSTED_AT = "postedAtUtc"

        /**
         * When the message was last modified.
         */
        const val COLUMN_MODIFIED_AT = "modifiedAtUtc"

        /**
         * Whether this message is a status message.
         */
        const val COLUMN_IS_STATUS_MESSAGE = "isStatusMessage"

        /**
         * This was used to indicate whether the message was saved to the message queue. Note that this
         * column is not used anymore. We just keep it in the database to prevent performing a risky
         * database migration since the message tables potentially contain many rows.
         */
        const val COLUMN_IS_QUEUED = "isQueued"

        /**
         * API message id of quoted message, if any.
         */
        const val COLUMN_QUOTED_MESSAGE_API_MESSAGE_ID = "quotedMessageId"

        /**
         * contents type of message - may be different from type
         */
        const val COLUMN_MESSAGE_CONTENTS_TYPE = "messageContentsType"

        /**
         * message flags that affect delivery receipt behavior etc. - carried over from AbstractMessage
         */
        const val COLUMN_MESSAGE_FLAGS = "messageFlags"

        /**
         * When the message was delivered.
         */
        const val COLUMN_DELIVERED_AT = "deliveredAtUtc"

        /**
         * When the message was read.
         */
        const val COLUMN_READ_AT = "readAtUtc"

        /**
         * The forward security mode in which the message was received/sent.
         */
        const val COLUMN_FORWARD_SECURITY_MODE = "forwardSecurityMode"

        /**
         * Display tags. Used e.g. for starred or pinned messages
         */
        const val COLUMN_DISPLAY_TAGS = "displayTags"

        /**
         * When the message was edited
         */
        const val COLUMN_EDITED_AT = "editedAtUtc"

        /**
         * When the message was deleted
         */
        const val COLUMN_DELETED_AT = "deletedAtUtc"

        // These models are used as workaround for the case where the data model is accessed
        // through a getter, but the stored body is missing or of the wrong type. We can't return null
        // so instead we return these dummy models that contain no data.
        private val emptyLocationDataModel by lazy(LazyThreadSafetyMode.NONE) {
            LocationDataModel.createEmpty()
        }
        private val emptyPollDataModel by lazy(LazyThreadSafetyMode.NONE) {
            PollDataModel.createEmpty()
        }
        private val emptyFileDataModel by lazy(LazyThreadSafetyMode.NONE) {
            FileDataModel.createEmpty()
        }
    }
}
