package ch.threema.storage.factories;

import android.content.ContentValues;
import android.database.Cursor;

import org.json.JSONObject;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import ch.threema.app.services.MessageService;
import ch.threema.data.datatypes.ConversationVisibility;
import ch.threema.domain.models.MessageId;
import ch.threema.storage.ChunkedSequence;
import ch.threema.storage.CursorHelper;
import ch.threema.storage.DatabaseCreationProvider;
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.group.GroupMessageModel;
import ch.threema.data.models.GroupModel;
import ch.threema.storage.models.MessageState;
import ch.threema.storage.models.MessageType;
import ch.threema.storage.models.group.GroupModelOld;
import kotlin.ranges.LongProgression;

import static ch.threema.common.JsonExtensionsKt.parseJsonObjectAsStringMap;
import static ch.threema.storage.models.data.DisplayTag.DISPLAY_TAG_STARRED;

public class GroupMessageModelFactory extends AbstractMessageModelFactory {
    public GroupMessageModelFactory(DatabaseProvider databaseProvider) {
        super(databaseProvider, GroupMessageModel.TABLE);
    }

    public List<GroupMessageModel> getAll() {
        return convertList(getReadableDatabase().query(this.getTableName(),
            null,
            null,
            null,
            null,
            null,
            null));
    }

    public GroupMessageModel getByApiMessageIdAndIdentity(MessageId apiMessageId, String identity) {
        return getFirst(
            GroupMessageModel.COLUMN_API_MESSAGE_ID + "=?" +
                " AND " + GroupMessageModel.COLUMN_IDENTITY + "=?",
            new String[]{
                apiMessageId.toString(),
                identity
            });
    }

    public GroupMessageModel getByApiMessageIdAndGroupId(@NonNull MessageId apiMessageId, int groupId) {
        return getFirst(
            GroupMessageModel.COLUMN_API_MESSAGE_ID + "=?" +
                " AND " + GroupMessageModel.COLUMN_GROUP_ID + "=?",
            new String[]{
                apiMessageId.toString(),
                String.valueOf(groupId),
            });
    }

    public GroupMessageModel getById(int id) {
        return getFirst(
            GroupMessageModel.COLUMN_ID + "=?",
            new String[]{
                String.valueOf(id)
            });
    }

    public GroupMessageModel getByUid(String uid) {
        return getFirst(
            GroupMessageModel.COLUMN_UID + "=?",
            new String[]{
                uid
            });
    }

    public List<GroupMessageModel> getAllRejectedMessagesInGroup(@NonNull GroupModel group) {
        return convertList(
            getReadableDatabase().query(
                getTableName(),
                null,
                GroupMessageModel.COLUMN_GROUP_ID + "=? AND " + AbstractMessageModel.COLUMN_STATE + "=?",
                new String[]{String.valueOf(group.getDatabaseId()), MessageState.FS_KEY_MISMATCH.toString()},
                null,
                null,
                null
            )
        );
    }

    public List<AbstractMessageModel> getMessagesByText(@Nullable String text, boolean includeArchived, boolean starredOnly, boolean sortAscending) {
        String displayClause, sortClause;
        if (starredOnly) {
            displayClause = " AND (" + GroupMessageModel.COLUMN_DISPLAY_TAGS + " & " + DISPLAY_TAG_STARRED + ") > 0 ";
        } else {
            displayClause = "";
        }

        if (sortAscending) {
            sortClause = " ASC ";
        } else {
            sortClause = " DESC ";
        }

        if (includeArchived) {
            if (text == null) {
                return convertAbstractList(getReadableDatabase().rawQuery(
                    "SELECT * FROM " + GroupMessageModel.TABLE +
                        " WHERE " + GroupMessageModel.COLUMN_IS_STATUS_MESSAGE + " = 0" +
                        displayClause +
                        " ORDER BY " + GroupMessageModel.COLUMN_CREATED_AT + sortClause +
                        "LIMIT 200",
                    new String[]{}));
            }

            return convertAbstractList(getReadableDatabase().rawQuery(
                "SELECT * FROM " + GroupMessageModel.TABLE +
                    " WHERE ( ( " + GroupMessageModel.COLUMN_BODY + " LIKE ? " +
                    " AND " + GroupMessageModel.COLUMN_TYPE + " IN (" +
                    MessageType.TEXT.serializedValue + "," +
                    MessageType.LOCATION.serializedValue + "," +
                    MessageType.POLL.serializedValue + ") )" +
                    " OR ( " + GroupMessageModel.COLUMN_CAPTION + " LIKE ? " +
                    " AND " + GroupMessageModel.COLUMN_TYPE + " = " +
                    MessageType.FILE.serializedValue + " ) )" +
                    " AND " + GroupMessageModel.COLUMN_IS_STATUS_MESSAGE + " = 0" +
                    displayClause +
                    " ORDER BY " + GroupMessageModel.COLUMN_CREATED_AT + sortClause +
                    "LIMIT 200",
                new String[]{
                    "%" + text + "%",
                    "%" + text + "%"
                }));
        } else {
            if (text == null) {
                return convertAbstractList(getReadableDatabase().rawQuery(
                    "SELECT * FROM " + GroupMessageModel.TABLE + " m" +
                        " INNER JOIN " + GroupModelOld.TABLE + " g ON g." + GroupModelOld.COLUMN_ID + " = m." + GroupMessageModel.COLUMN_GROUP_ID +
                        " WHERE g." + GroupModelOld.COLUMN_CONVERSATION_VISIBILITY + " != " + ConversationVisibility.ARCHIVED.getSerializedValue() +
                        " AND m." + GroupMessageModel.COLUMN_IS_STATUS_MESSAGE + " = 0" +
                        displayClause +
                        " ORDER BY m." + GroupMessageModel.COLUMN_CREATED_AT + sortClause +
                        "LIMIT 200",
                    new String[]{}));
            }

            return convertAbstractList(getReadableDatabase().rawQuery(
                "SELECT * FROM " + GroupMessageModel.TABLE + " m" +
                    " INNER JOIN " + GroupModelOld.TABLE + " g ON g." + GroupModelOld.COLUMN_ID + " = m." + GroupMessageModel.COLUMN_GROUP_ID +
                    " WHERE g." + GroupModelOld.COLUMN_CONVERSATION_VISIBILITY + " != " + ConversationVisibility.ARCHIVED.getSerializedValue() +
                    " AND ( ( m." + GroupMessageModel.COLUMN_BODY + " LIKE ? " +
                    " AND m." + GroupMessageModel.COLUMN_TYPE + " IN (" +
                    MessageType.TEXT.serializedValue + "," +
                    MessageType.LOCATION.serializedValue + "," +
                    MessageType.POLL.serializedValue + ") )" +
                    " OR ( m." + GroupMessageModel.COLUMN_CAPTION + " LIKE ? " +
                    " AND m." + GroupMessageModel.COLUMN_TYPE + " = " +
                    MessageType.FILE.serializedValue + " ) )" +
                    " AND m." + GroupMessageModel.COLUMN_IS_STATUS_MESSAGE + " = 0" +
                    displayClause +
                    " ORDER BY m." + GroupMessageModel.COLUMN_CREATED_AT + sortClause +
                    "LIMIT 200",
                new String[]{
                    "%" + text + "%",
                    "%" + text + "%"
                }));
        }
    }

    /**
     * Convert a cursor's rows to a list of {@link AbstractMessageModel}s.
     * Note that the cursor will be closed after conversion.
     */
    private List<AbstractMessageModel> convertAbstractList(Cursor cursor) {
        List<AbstractMessageModel> result = new ArrayList<>();
        if (cursor != null) {
            try (cursor) {
                while (cursor.moveToNext()) {
                    result.add(convert(cursor));
                }
            }
        }
        return result;
    }

    /**
     * Convert a cursor's rows to a list of {@link GroupMessageModel}s.
     * Note that the cursor will be closed after conversion.
     */
    private List<GroupMessageModel> convertList(Cursor cursor) {
        List<GroupMessageModel> result = new ArrayList<>();
        if (cursor != null) {
            try (cursor) {
                while (cursor.moveToNext()) {
                    result.add(convert(cursor));
                }
            }
        }
        return result;
    }

    private GroupMessageModel convert(Cursor cursor) {
        if (cursor != null && cursor.getPosition() >= 0) {
            final GroupMessageModel groupMessageModel = new GroupMessageModel();

            //convert default
            super.convert(groupMessageModel, new CursorHelper(cursor, getColumnIndexCache()).current((CursorHelper.Callback) cursorHelper -> {
                int groupId = Objects.requireNonNull(cursorHelper.getInt(GroupMessageModel.COLUMN_GROUP_ID));
                groupMessageModel.setGroupId(groupId);
                String messageStates = cursorHelper.getString(GroupMessageModel.COLUMN_GROUP_MESSAGE_STATES);
                if (messageStates != null) {
                    try {
                        Map<String, String> messageStatesMap = parseJsonObjectAsStringMap(messageStates);
                        groupMessageModel.setGroupMessageStates(messageStatesMap);
                    } catch (Exception ignored) {
                        // map may not be available or empty
                        groupMessageModel.setGroupMessageStates(null);
                    }
                }
                return false;
            }));

            return groupMessageModel;
        }

        return null;
    }

    public long countMessages(int groupId) {
        return DatabaseUtil.count(getReadableDatabase().rawQuery(
            "SELECT COUNT(*) FROM " + this.getTableName()
                + " WHERE " + GroupMessageModel.COLUMN_GROUP_ID + "=?",
            new String[]{
                String.valueOf(groupId)
            }
        ));
    }

    public long countUnreadMessages(int groupId) {
        return DatabaseUtil.count(getReadableDatabase().rawQuery(
            "SELECT COUNT(*) FROM " + this.getTableName()
                + " WHERE " + GroupMessageModel.COLUMN_GROUP_ID + "=?"
                + " AND " + GroupMessageModel.COLUMN_OUTBOX + "=0"
                + " AND " + GroupMessageModel.COLUMN_IS_SAVED + "=1"
                + " AND " + GroupMessageModel.COLUMN_IS_READ + "=0"
                + " AND " + GroupMessageModel.COLUMN_IS_STATUS_MESSAGE + "=0",
            new String[]{
                String.valueOf(groupId)
            }
        ));
    }

    public List<GroupMessageModel> getUnreadMessages(int groupId) {
        return convertList(getReadableDatabase().query(this.getTableName(),
            null,
            GroupMessageModel.COLUMN_GROUP_ID + "=?"
                + " AND " + GroupMessageModel.COLUMN_OUTBOX + "=0"
                + " AND " + GroupMessageModel.COLUMN_IS_SAVED + "=1"
                + " AND " + GroupMessageModel.COLUMN_IS_READ + "=0"
                + " AND " + GroupMessageModel.COLUMN_IS_STATUS_MESSAGE + "=0",
            new String[]{
                String.valueOf(groupId)
            },
            null,
            null,
            null));
    }

    public long countByTypes(MessageType[] messageTypes) {
        String[] args = new String[messageTypes.length];
        for (int n = 0; n < messageTypes.length; n++) {
            args[n] = String.valueOf(messageTypes[n].serializedValue);
        }

        Cursor c = getReadableDatabase().rawQuery(
            "SELECT COUNT(*) FROM " + this.getTableName()
                + " WHERE " + GroupMessageModel.COLUMN_TYPE + " IN (" + DatabaseUtil.makePlaceholders(args.length) + ")",
            args
        );
        return DatabaseUtil.count(c);
    }

    public boolean createOrUpdate(GroupMessageModel groupMessageModel) {
        boolean insert = true;
        if (groupMessageModel.getId() > 0) {
            Cursor cursor = getReadableDatabase().query(
                this.getTableName(),
                null,
                GroupMessageModel.COLUMN_ID + "=?",
                new String[]{
                    String.valueOf(groupMessageModel.getId())
                },
                null,
                null,
                null
            );

            if (cursor != null) {
                try (cursor) {
                    insert = !cursor.moveToNext();
                }
            }
        }

        if (insert) {
            return create(groupMessageModel);
        } else {
            return update(groupMessageModel);
        }
    }

    public boolean create(GroupMessageModel groupMessageModel) {
        ContentValues contentValues = this.buildContentValues(groupMessageModel);
        contentValues.put(GroupMessageModel.COLUMN_GROUP_ID, groupMessageModel.getGroupId());
        addGroupMessageStates(contentValues, groupMessageModel);
        long newId = getWritableDatabase().insertOrThrow(this.getTableName(), null, contentValues);
        if (newId > 0) {
            groupMessageModel.setId((int) newId);
            return true;
        }
        return false;
    }

    public boolean update(GroupMessageModel groupMessageModel) {
        ContentValues contentValues = this.buildContentValues(groupMessageModel);
        addGroupMessageStates(contentValues, groupMessageModel);
        getWritableDatabase().update(this.getTableName(),
            contentValues,
            GroupMessageModel.COLUMN_ID + "=?",
            new String[]{
                String.valueOf(groupMessageModel.getId()),
            });
        return true;
    }

    public List<GroupMessageModel> find(int groupId, MessageService.MessageFilter filter) {
        QueryBuilder queryBuilder = new QueryBuilder();

        //sort by id!
        String orderBy = GroupMessageModel.COLUMN_ID + " DESC";
        List<String> placeholders = new ArrayList<>();

        queryBuilder.appendWhere(GroupMessageModel.COLUMN_GROUP_ID + "=?");
        placeholders.add(String.valueOf(groupId));

        //default filters
        this.appendFilter(queryBuilder, filter, placeholders);

        queryBuilder.setTables(this.getTableName());
        List<GroupMessageModel> messageModels = convertList(queryBuilder.query(
            getReadableDatabase(),
            null,
            null,
            placeholders.toArray(new String[0]),
            null,
            null,
            orderBy,
            this.limitFilter(filter)));

        this.postFilter(messageModels, filter);

        return messageModels;
    }

    public ChunkedSequence<GroupMessageModel> getByGroupId(long groupId) {
        long count;
        try (Cursor cursor = getReadableDatabase().query(
            "SELECT COUNT(*) FROM `" + getTableName() + "` WHERE `" + GroupMessageModel.COLUMN_GROUP_ID + "` = ?",
            new String[]{String.valueOf(groupId)}
        )) {
            if (cursor.moveToNext()) {
                count = cursor.getLong(0);
            } else {
                count = 0;
            }
        }

        return new ChunkedSequence<>(
            LongProgression.Companion.fromClosedRange(0, count, 10000),
            (from, size) ->
                getReadableDatabase().query(
                    "SELECT * FROM `" + getTableName() + "` WHERE `" + GroupMessageModel.COLUMN_GROUP_ID + "` = ? LIMIT ? OFFSET ?",
                    new String[]{String.valueOf(groupId), String.valueOf(size), String.valueOf(from)}
                ),
            this::convert
        );
    }

    public int delete(GroupMessageModel groupMessageModel) {
        return getWritableDatabase().delete(this.getTableName(),
            GroupMessageModel.COLUMN_ID + "=?",
            new String[]{
                String.valueOf(groupMessageModel.getId())
            });
    }

    public int deleteByGroupId(long groupId) {
        return getWritableDatabase().delete(this.getTableName(),
            GroupMessageModel.COLUMN_GROUP_ID + "=?",
            new String[]{
                String.valueOf(groupId)
            });
    }

    private GroupMessageModel getFirst(String selection, String[] selectionArgs) {
        Cursor cursor = getReadableDatabase().query(
            this.getTableName(),
            null,
            selection,
            selectionArgs,
            null,
            null,
            null
        );

        if (cursor != null) {
            try (cursor) {
                if (cursor.moveToFirst()) {
                    return convert(cursor);
                }
            }
        }

        return null;
    }

    private void addGroupMessageStates(@NonNull ContentValues contentValues, @NonNull GroupMessageModel groupMessageModel) {
        String groupMessageStates = null;
        if (groupMessageModel.getGroupMessageStates() != null) {
            groupMessageStates = new JSONObject(groupMessageModel.getGroupMessageStates()).toString();
        }

        contentValues.put(GroupMessageModel.COLUMN_GROUP_MESSAGE_STATES, groupMessageStates);
    }

    public static class Creator implements DatabaseCreationProvider {
        @Override
        @NonNull
        public String [] getCreationStatements() {
            return new String[]{
                "CREATE TABLE `" + GroupMessageModel.TABLE + "`" +
                    "(" +
                    "`" + GroupMessageModel.COLUMN_ID + "` INTEGER PRIMARY KEY AUTOINCREMENT , " +
                    "`" + GroupMessageModel.COLUMN_UID + "` VARCHAR , " +
                    "`" + GroupMessageModel.COLUMN_API_MESSAGE_ID + "` VARCHAR , " +
                    "`" + GroupMessageModel.COLUMN_GROUP_ID + "` INTEGER NOT NULL , " +
                    "`" + GroupMessageModel.COLUMN_IDENTITY + "` VARCHAR , " +
                    "`" + GroupMessageModel.COLUMN_OUTBOX + "` SMALLINT , " +
                    "`" + GroupMessageModel.COLUMN_TYPE + "` INTEGER ," +
                    "`" + GroupMessageModel.COLUMN_CORRELATION_ID + "` VARCHAR ," +
                    "`" + GroupMessageModel.COLUMN_BODY + "` VARCHAR ," +
                    "`" + GroupMessageModel.COLUMN_CAPTION + "` VARCHAR ," +
                    "`" + GroupMessageModel.COLUMN_IS_READ + "` SMALLINT ," +
                    "`" + GroupMessageModel.COLUMN_IS_SAVED + "` SMALLINT ," +
                    "`" + GroupMessageModel.COLUMN_IS_QUEUED + "` TINYINT ," +
                    "`" + GroupMessageModel.COLUMN_STATE + "` VARCHAR , " +
                    "`" + GroupMessageModel.COLUMN_POSTED_AT + "` BIGINT , " +
                    "`" + GroupMessageModel.COLUMN_CREATED_AT + "` BIGINT , " +
                    "`" + GroupMessageModel.COLUMN_MODIFIED_AT + "` BIGINT , " +
                    "`" + GroupMessageModel.COLUMN_IS_STATUS_MESSAGE + "` SMALLINT ," +
                    "`" + GroupMessageModel.COLUMN_QUOTED_MESSAGE_API_MESSAGE_ID + "` VARCHAR ," +
                    "`" + GroupMessageModel.COLUMN_MESSAGE_CONTENTS_TYPE + "` TINYINT ," +
                    "`" + GroupMessageModel.COLUMN_MESSAGE_FLAGS + "` INT ," +
                    "`" + GroupMessageModel.COLUMN_DELIVERED_AT + "` DATETIME ," +
                    "`" + GroupMessageModel.COLUMN_READ_AT + "` DATETIME ," +
                    "`" + GroupMessageModel.COLUMN_FORWARD_SECURITY_MODE + "` TINYINT DEFAULT 0 ," +
                    "`" + GroupMessageModel.COLUMN_GROUP_MESSAGE_STATES + "` VARCHAR ," +
                    "`" + GroupMessageModel.COLUMN_DISPLAY_TAGS + "` TINYINT DEFAULT 0 ," +
                    "`" + GroupMessageModel.COLUMN_EDITED_AT + "` DATETIME ," +
                    "`" + GroupMessageModel.COLUMN_DELETED_AT + "` DATETIME );",

                // indices
                "CREATE INDEX `group_message_uid_idx` ON `" + GroupMessageModel.TABLE + "` ( `" + GroupMessageModel.COLUMN_UID + "` )",
                "CREATE INDEX `m_group_message_outbox_idx` ON `" + GroupMessageModel.TABLE + "` ( `" + GroupMessageModel.COLUMN_OUTBOX + "` );",
                "CREATE INDEX `m_group_message_identity_idx` ON `" + GroupMessageModel.TABLE + "` ( `" + GroupMessageModel.COLUMN_IDENTITY + "` );",
                "CREATE INDEX `m_group_message_groupId_idx` ON `" + GroupMessageModel.TABLE + "` ( `" + GroupMessageModel.COLUMN_GROUP_ID + "` );",
                "CREATE INDEX `groupMessageApiMessageIdIdx` ON `" + GroupMessageModel.TABLE + "` ( `" + GroupMessageModel.COLUMN_API_MESSAGE_ID + "` );",
                "CREATE INDEX `groupMessageCorrelationIdIdx` ON `" + GroupMessageModel.TABLE + "` ( `" + GroupMessageModel.COLUMN_CORRELATION_ID + "` );",
                "CREATE INDEX `group_message_state_idx` ON `" + GroupMessageModel.TABLE
                    + "`(`" + AbstractMessageModel.COLUMN_TYPE
                    + "`, `" + AbstractMessageModel.COLUMN_STATE
                    + "`, `" + AbstractMessageModel.COLUMN_OUTBOX
                    + "`)",
            };
        }
    }
}
