package ch.threema.app.webclient.services.instance.message.receiver;

import org.msgpack.core.MessagePackException;
import org.msgpack.value.Value;
import org.slf4j.Logger;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Map;

import androidx.annotation.AnyThread;
import androidx.annotation.NonNull;
import androidx.annotation.StringDef;
import androidx.annotation.WorkerThread;
import ch.threema.app.groupflows.GroupDisbandIntent;
import ch.threema.app.groupflows.GroupFlowResult;
import ch.threema.app.groupflows.GroupLeaveIntent;
import ch.threema.app.services.GroupFlowDispatcher;
import ch.threema.app.utils.RuntimeUtil;
import ch.threema.app.webclient.Protocol;
import ch.threema.app.webclient.services.instance.MessageDispatcher;
import ch.threema.app.webclient.services.instance.MessageReceiver;
import ch.threema.base.utils.CoroutinesExtensionKt;
import static ch.threema.base.utils.LoggingKt.getThreemaLogger;
import ch.threema.data.models.GroupModel;
import ch.threema.data.models.GroupModelData;
import ch.threema.data.repositories.GroupModelRepository;
import kotlin.Unit;
import kotlinx.coroutines.Deferred;

@WorkerThread
public class DeleteGroupHandler extends MessageReceiver {
    private static final Logger logger = getThreemaLogger("DeleteGroupHandler");

    private static final String TYPE_LEAVE = "leave";
    private static final String TYPE_DELETE = "delete";

    // Error codes
    @Retention(RetentionPolicy.SOURCE)
    @StringDef({
        Protocol.ERROR_INVALID_GROUP,
        Protocol.ERROR_ALREADY_LEFT,
        Protocol.ERROR_NOT_ALLOWED,
        Protocol.ERROR_BAD_REQUEST,
    })
    private @interface ErrorCode {
    }

    private @NonNull
    final MessageDispatcher responseDispatcher;
    private @NonNull
    final GroupFlowDispatcher groupFlowDispatcher;
    private @NonNull
    final GroupModelRepository groupModelRepository;
    private @NonNull
    final String myIdentity;

    @AnyThread
    public DeleteGroupHandler(
        @NonNull MessageDispatcher responseDispatcher,
        @NonNull GroupFlowDispatcher groupFlowDispatcher,
        @NonNull GroupModelRepository groupModelRepository,
        @NonNull String myIdentity
    ) {
        super(Protocol.SUB_TYPE_GROUP);
        this.responseDispatcher = responseDispatcher;
        this.groupFlowDispatcher = groupFlowDispatcher;
        this.groupModelRepository = groupModelRepository;
        this.myIdentity = myIdentity;
    }

    @Override
    protected void receive(Map<String, Value> message) throws MessagePackException {
        logger.debug("Received delete request");
        final Map<String, Value> args = this.getArguments(message, false, new String[]{
            Protocol.ARGUMENT_RECEIVER_ID,
            Protocol.ARGUMENT_DELETE_TYPE,
            Protocol.ARGUMENT_TEMPORARY_ID,
        });

        // Get temporary ID
        final String temporaryId = args.get(Protocol.ARGUMENT_TEMPORARY_ID).asStringValue().toString();

        // Load group
        final int groupId = Integer.parseInt(args.get(Protocol.ARGUMENT_RECEIVER_ID).asStringValue().toString());
        final GroupModel group = groupModelRepository.getByGroupDatabaseId(groupId);
        if (group == null) {
            logger.error("invalid group, aborting");
            this.failed(temporaryId, Protocol.ERROR_INVALID_GROUP);
            return;
        }

        final GroupModelData groupModelData = group.getData();
        if (groupModelData == null) {
            logger.error("Group model data is null");
            this.failed(temporaryId, Protocol.ERROR_INVALID_GROUP);
            return;
        }

        final boolean isLeft = !groupModelData.isMember();
        final boolean isCreator =
            groupModelData.groupIdentity.getCreatorIdentity().equals(myIdentity);

        // There are two delete types: Either we want to delete the group, or just leave it.
        switch (args.get(Protocol.ARGUMENT_DELETE_TYPE).asStringValue().toString()) {
            case TYPE_LEAVE:
                if (isLeft) {
                    logger.error("group already left");
                    this.failed(temporaryId, Protocol.ERROR_ALREADY_LEFT);
                    return;
                }
                if (isCreator) {
                    // If the group is not left and the user is the creator, then dissolve the group first
                    disbandGroup(group, temporaryId);
                } else {
                    // If the group is not left and the user is a member, then leave the group first
                    leaveGroup(group, temporaryId);
                }
                return;
            case TYPE_DELETE:
                if (!isLeft) {
                    if (isCreator) {
                        // If the group is not left and the user is the creator, then dissolve the group first
                        disbandAndRemoveGroup(group, temporaryId);
                    } else {
                        // If the group is not left and the user is a member, then leave the group first
                        leaveAndRemoveGroup(group, temporaryId);
                    }
                } else {
                    removeGroup(group, temporaryId);
                }
                break;
            default:
                logger.error("invalid delete type argument");
                this.failed(temporaryId, Protocol.ERROR_BAD_REQUEST);
        }
    }

    private void leaveGroup(@NonNull GroupModel groupModel, @NonNull String temporaryId) {
        Deferred<GroupFlowResult> result = groupFlowDispatcher.runLeaveGroupFlow(
            GroupLeaveIntent.LEAVE,
            groupModel
        );
        handleGroupFlowResult(result, temporaryId);
    }

    private void disbandGroup(@NonNull GroupModel groupModel, @NonNull String temporaryId) {
        Deferred<GroupFlowResult> result = groupFlowDispatcher.runDisbandGroupFlow(
            GroupDisbandIntent.DISBAND,
            groupModel
        );
        handleGroupFlowResult(result, temporaryId);
    }

    private void leaveAndRemoveGroup(@NonNull GroupModel groupModel, @NonNull String temporaryId) {
        Deferred<GroupFlowResult> result = groupFlowDispatcher.runLeaveGroupFlow(
            GroupLeaveIntent.LEAVE_AND_REMOVE,
            groupModel
        );
        handleGroupFlowResult(result, temporaryId);
    }

    private void disbandAndRemoveGroup(@NonNull GroupModel groupModel, @NonNull String temporaryId) {
        Deferred<GroupFlowResult> result = groupFlowDispatcher.runDisbandGroupFlow(
            GroupDisbandIntent.DISBAND_AND_REMOVE,
            groupModel
        );
        handleGroupFlowResult(result, temporaryId);
    }

    private void removeGroup(@NonNull GroupModel groupModel, @NonNull String temporaryId) {
        Deferred<GroupFlowResult> result = groupFlowDispatcher.runRemoveGroupFlow(
            groupModel
        );
        handleGroupFlowResult(result, temporaryId);
    }

    private void handleGroupFlowResult(
        @NonNull Deferred<GroupFlowResult> groupFlowResultDeferred,
        @NonNull String temporaryId
    ) {
        CoroutinesExtensionKt.onCompleted(
            groupFlowResultDeferred,
            exception -> {
                logger.error("group-flow was completed exceptionally", exception);
                RuntimeUtil.runOnWorkerThread(() -> {
                    failed(temporaryId, Protocol.ERROR_BAD_REQUEST);
                });
                return Unit.INSTANCE;
            },
            result -> {
                RuntimeUtil.runOnWorkerThread(() -> {
                    if (result instanceof GroupFlowResult.Success) {
                        success(temporaryId);
                    } else if (result instanceof GroupFlowResult.Failure) {
                        failed(temporaryId, Protocol.ERROR_BAD_REQUEST);
                    }
                });
                return Unit.INSTANCE;
            }
        );
    }

    /**
     * Respond with success true.
     */
    private void success(String temporaryId) {
        logger.debug("Respond with leave group success");
        this.sendConfirmActionSuccess(this.responseDispatcher, temporaryId);
    }

    /**
     * Respond with an error code.
     */
    private void failed(String temporaryId, @ErrorCode String errorCode) {
        logger.warn("Respond with modify group failed ({})", errorCode);
        this.sendConfirmActionFailure(this.responseDispatcher, temporaryId, errorCode);
    }

    @Override
    protected boolean maybeNeedsConnection() {
        return true;
    }
}
