package ch.threema.storage.factories;

import static ch.threema.common.JavaCompat.isNullOrEmpty;
import static ch.threema.storage.models.data.DisplayTag.DISPLAY_TAG_STARRED;

import android.content.ContentValues;
import android.database.sqlite.SQLiteException;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;

import net.zetetic.database.sqlcipher.SQLiteStatement;

import org.slf4j.Logger;

import java.util.Iterator;
import java.util.List;

import ch.threema.app.services.MessageService;
import static ch.threema.base.utils.LoggingKt.getThreemaLogger;
import ch.threema.domain.protocol.csp.messages.fs.ForwardSecurityMode;
import ch.threema.storage.CursorHelper;
import ch.threema.storage.DatabaseProvider;
import ch.threema.storage.DatabaseUtil;
import ch.threema.storage.QueryBuilder;
import ch.threema.storage.models.AbstractMessageModel;
import ch.threema.storage.models.MessageState;
import ch.threema.storage.models.MessageType;
import ch.threema.storage.models.data.DisplayTag;
import ch.threema.storage.models.data.MessageContentsType;
import ch.threema.storage.models.data.media.FileDataModel;

abstract class AbstractMessageModelFactory extends ModelFactory {
    private static final Logger logger = getThreemaLogger("AbstractMessageModelFactory");

    AbstractMessageModelFactory(DatabaseProvider databaseProvider, String tableName) {
        super(databaseProvider, tableName);
    }

    void convert(final AbstractMessageModel messageModel, CursorHelper cursorFactory) {
        cursorFactory.current(new CursorHelper.Callback() {
            @Override
            public boolean next(CursorHelper cursorFactory) {
                ForwardSecurityMode forwardSecurityMode = ForwardSecurityMode.NONE;
                Integer fsmValue = cursorFactory.getInt(AbstractMessageModel.COLUMN_FORWARD_SECURITY_MODE);
                if (fsmValue != null) {
                    forwardSecurityMode = ForwardSecurityMode.getByValue(fsmValue);
                }

                messageModel.setId(cursorFactory.getInt(AbstractMessageModel.COLUMN_ID));
                messageModel.setUid(cursorFactory.getString(AbstractMessageModel.COLUMN_UID));
                messageModel.setApiMessageId(cursorFactory.getString(AbstractMessageModel.COLUMN_API_MESSAGE_ID));
                messageModel.setIdentity(cursorFactory.getString(AbstractMessageModel.COLUMN_IDENTITY));
                messageModel.setOutbox(cursorFactory.getBoolean(AbstractMessageModel.COLUMN_OUTBOX));
                messageModel.setCorrelationId(cursorFactory.getString(AbstractMessageModel.COLUMN_CORRELATION_ID));
                messageModel.setBody(cursorFactory.getString(AbstractMessageModel.COLUMN_BODY));
                messageModel.setRead(cursorFactory.getBoolean(AbstractMessageModel.COLUMN_IS_READ));
                messageModel.setSaved(cursorFactory.getBoolean(AbstractMessageModel.COLUMN_IS_SAVED));
                messageModel.setPostedAt(cursorFactory.getInstant(AbstractMessageModel.COLUMN_POSTED_AT));
                messageModel.setCreatedAt(cursorFactory.getInstant(AbstractMessageModel.COLUMN_CREATED_AT));
                messageModel.setModifiedAt(cursorFactory.getInstant(AbstractMessageModel.COLUMN_MODIFIED_AT));
                messageModel.setStatusMessage(cursorFactory.getBoolean(AbstractMessageModel.COLUMN_IS_STATUS_MESSAGE));
                messageModel.setCaption(cursorFactory.getString(AbstractMessageModel.COLUMN_CAPTION));
                messageModel.setQuotedMessageId(cursorFactory.getString(AbstractMessageModel.COLUMN_QUOTED_MESSAGE_API_MESSAGE_ID));
                messageModel.setMessageContentsType(cursorFactory.getInt(AbstractMessageModel.COLUMN_MESSAGE_CONTENTS_TYPE));
                messageModel.setMessageFlags(cursorFactory.getInt(AbstractMessageModel.COLUMN_MESSAGE_FLAGS));
                messageModel.setDeliveredAt(cursorFactory.getInstant(AbstractMessageModel.COLUMN_DELIVERED_AT));
                messageModel.setReadAt(cursorFactory.getInstant(AbstractMessageModel.COLUMN_READ_AT));
                messageModel.setEditedAt(cursorFactory.getInstant(AbstractMessageModel.COLUMN_EDITED_AT));
                messageModel.setDeletedAt(cursorFactory.getInstant(AbstractMessageModel.COLUMN_DELETED_AT));
                messageModel.setForwardSecurityMode(forwardSecurityMode);
                messageModel.setDisplayTags(cursorFactory.getInt(AbstractMessageModel.COLUMN_DISPLAY_TAGS));

                String stateString = cursorFactory.getString(AbstractMessageModel.COLUMN_STATE);
                if (!isNullOrEmpty(stateString)) {
                    try {
                        messageModel.setState(MessageState.valueOf(stateString));
                    } catch (IllegalArgumentException e) {
                        logger.error("Invalid message state {} - ignore", stateString, e);
                    }
                }

                int type = cursorFactory.getInt(AbstractMessageModel.COLUMN_TYPE);
                messageModel.setType(MessageType.deserialize(type));
                return false;
            }
        });
    }

    ContentValues buildContentValues(AbstractMessageModel messageModel) {
        ensureMessageIdIsSetIfRequiredByType(messageModel);
        ContentValues contentValues = new ContentValues();
        contentValues.put(AbstractMessageModel.COLUMN_UID, messageModel.getUid());
        contentValues.put(AbstractMessageModel.COLUMN_API_MESSAGE_ID, messageModel.getApiMessageId());
        contentValues.put(AbstractMessageModel.COLUMN_IDENTITY, messageModel.getIdentity());
        contentValues.put(AbstractMessageModel.COLUMN_OUTBOX, messageModel.isOutbox());
        contentValues.put(AbstractMessageModel.COLUMN_TYPE, messageModel.getType() != null ? messageModel.getType().serializedValue : null);
        contentValues.put(AbstractMessageModel.COLUMN_CORRELATION_ID, messageModel.getCorrelationId());
        contentValues.put(AbstractMessageModel.COLUMN_BODY, messageModel.getBody());
        contentValues.put(AbstractMessageModel.COLUMN_IS_READ, messageModel.isRead());
        contentValues.put(AbstractMessageModel.COLUMN_IS_SAVED, messageModel.isSaved());
        contentValues.put(AbstractMessageModel.COLUMN_STATE, messageModel.getState() != null ? messageModel.getState().toString() : null);
        contentValues.put(AbstractMessageModel.COLUMN_POSTED_AT, DatabaseUtil.getDateTimeContentValue(messageModel.getPostedAt()));
        contentValues.put(AbstractMessageModel.COLUMN_CREATED_AT, DatabaseUtil.getDateTimeContentValue(messageModel.getCreatedAt()));
        contentValues.put(AbstractMessageModel.COLUMN_MODIFIED_AT, DatabaseUtil.getDateTimeContentValue(messageModel.getModifiedAt()));
        contentValues.put(AbstractMessageModel.COLUMN_IS_STATUS_MESSAGE, messageModel.isStatusMessage());
        contentValues.put(AbstractMessageModel.COLUMN_CAPTION, messageModel.getCaption());
        contentValues.put(AbstractMessageModel.COLUMN_QUOTED_MESSAGE_API_MESSAGE_ID, messageModel.getQuotedMessageId());
        contentValues.put(AbstractMessageModel.COLUMN_MESSAGE_CONTENTS_TYPE, messageModel.getMessageContentsType());
        contentValues.put(AbstractMessageModel.COLUMN_MESSAGE_FLAGS, messageModel.getMessageFlags());
        contentValues.put(AbstractMessageModel.COLUMN_DELIVERED_AT, DatabaseUtil.getDateTimeContentValue(messageModel.getDeliveredAt()));
        contentValues.put(AbstractMessageModel.COLUMN_READ_AT, DatabaseUtil.getDateTimeContentValue(messageModel.getReadAt()));
        contentValues.put(AbstractMessageModel.COLUMN_EDITED_AT, DatabaseUtil.getDateTimeContentValue(messageModel.getEditedAt()));
        contentValues.put(AbstractMessageModel.COLUMN_DELETED_AT, DatabaseUtil.getDateTimeContentValue(messageModel.getDeletedAt()));
        contentValues.put(AbstractMessageModel.COLUMN_FORWARD_SECURITY_MODE, messageModel.getForwardSecurityMode() != null ? messageModel.getForwardSecurityMode().getValue() : null);
        contentValues.put(AbstractMessageModel.COLUMN_DISPLAY_TAGS, messageModel.getDisplayTags());

        return contentValues;
    }

    private void ensureMessageIdIsSetIfRequiredByType(@NonNull AbstractMessageModel message) {
        var type = message.getType();
        if (type != null && type.getRequiresMessageId() && message.getApiMessageId() == null) {
            throw new IllegalArgumentException("Message of type " + type + " requires a message id");
        }
    }

    void appendFilter(QueryBuilder queryBuilder, @Nullable MessageService.MessageFilter filter, List<String> placeholders) {
        if (filter != null) {
            if (!filter.withStatusMessages()) {
                queryBuilder.appendWhere(AbstractMessageModel.COLUMN_IS_STATUS_MESSAGE + "=0");
            }
            if (filter.onlyUnread()) {
                queryBuilder.appendWhere(AbstractMessageModel.COLUMN_OUTBOX + "=0");
                queryBuilder.appendWhere(AbstractMessageModel.COLUMN_IS_READ + "=0");
                queryBuilder.appendWhere(AbstractMessageModel.COLUMN_IS_STATUS_MESSAGE + "=0");
            }

            if (!filter.withUnsaved()) {
                queryBuilder.appendWhere(AbstractMessageModel.COLUMN_IS_SAVED + "=1");
            }

            if (filter.types() != null && filter.types().length > 0) {
                queryBuilder.appendWhere(AbstractMessageModel.COLUMN_TYPE + " IN (" + DatabaseUtil.makePlaceholders(filter.types().length) + ")");
                for (MessageType f : filter.types()) {
                    placeholders.add(String.valueOf(f.serializedValue));
                }
            }

            if (filter.contentTypes() != null && filter.contentTypes().length > 0) {
                queryBuilder.appendWhere(AbstractMessageModel.COLUMN_MESSAGE_CONTENTS_TYPE + " IN (" + DatabaseUtil.makePlaceholders(filter.contentTypes().length) + ")");
                for (@MessageContentsType int f : filter.contentTypes()) {
                    placeholders.add(String.valueOf(f));
                }
            }

            if (filter.getPageReferenceId() != null && filter.getPageReferenceId() > 0) {
                queryBuilder.appendWhere(AbstractMessageModel.COLUMN_ID + "<?");
                placeholders.add(String.valueOf(filter.getPageReferenceId()));
            }

            if (filter.displayTags() != null && filter.displayTags().length > 0) {
                for (@DisplayTag int f : filter.displayTags()) {
                    queryBuilder.appendWhere("(" + AbstractMessageModel.COLUMN_DISPLAY_TAGS + " & " + f + ") > 0");
                }
            }
        }
    }

    <T> void postFilter(List<T> input, @Nullable MessageService.MessageFilter filter) {
        if (filter != null && filter.onlyDownloaded()) {
            Iterator<T> i = input.iterator();
            while (i.hasNext()) {
                AbstractMessageModel m = (AbstractMessageModel) i.next();
                boolean remove = false;
                if (m.getType() == MessageType.FILE) {
                    FileDataModel d = m.getFileData();
                    remove = (d == null || !d.isDownloaded());
                }

                if (remove) {
                    i.remove();
                }
            }
        }
    }

    String limitFilter(@Nullable MessageService.MessageFilter filter) {
        if (filter != null && filter.getPageSize() > 0) {
            return "" + filter.getPageSize();
        }
        return null;
    }

    /**
     * Mark file messages that have not been uploaded completely yet as failed so that users can
     * retry sending them. This affects file messages with state pending (upload did not start yet)
     * and uploading (upload may have been started already). File messages with state sending do not
     * need to be set to failed as they have been uploaded completely and a persistent task has been
     * scheduled. Therefore, these files will get sent as soon as there is a chat server connection.
     */
    public void markUnscheduledFileMessagesAsFailed() {
        ContentValues values = new ContentValues();
        values.put(AbstractMessageModel.COLUMN_STATE, MessageState.SENDFAILED.toString());

        try {
            int updated = getWritableDatabase().update(
                this.getTableName(),
                values,
                AbstractMessageModel.COLUMN_TYPE + " =?"
                    + " AND " + AbstractMessageModel.COLUMN_STATE + " IN (?, ?)"
                    + " AND " + AbstractMessageModel.COLUMN_OUTBOX + " = 1",
                new String[]{
                    String.valueOf(MessageType.FILE.serializedValue),
                    MessageState.PENDING.toString(),
                    MessageState.UPLOADING.toString(),
                }
            );

            if (updated > 0) {
                logger.info("{} messages in sending status were updated to sendfailed.", updated);
            }
        } catch (Exception e) {
            logger.error("Exception", e);
        }
    }

    @WorkerThread
    public int unstarAllMessages() {
        String query =
            "UPDATE " + this.getTableName() +
                " SET " + AbstractMessageModel.COLUMN_DISPLAY_TAGS +
                " = (" + AbstractMessageModel.COLUMN_DISPLAY_TAGS +
                " & ~" + DISPLAY_TAG_STARRED +
                ") WHERE (" + AbstractMessageModel.COLUMN_DISPLAY_TAGS + " & " + DISPLAY_TAG_STARRED + ") > 0";

        SQLiteStatement statement = getWritableDatabase().compileStatement(query);
        return statement.executeUpdateDelete();
    }

    @WorkerThread
    public long countStarredMessages() throws SQLiteException {
        return DatabaseUtil.count(getReadableDatabase().rawQuery(
            "SELECT COUNT(*) FROM " + this.getTableName()
                + " WHERE (" + AbstractMessageModel.COLUMN_DISPLAY_TAGS + " & " + DISPLAY_TAG_STARRED + ") > 0",
            null
        ));
    }
}
