/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.syncope.core.flowable.impl;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.syncope.common.lib.request.UserCR;
import org.apache.syncope.common.lib.request.UserUR;
import org.apache.syncope.common.lib.to.WorkflowTask;
import org.apache.syncope.common.lib.to.WorkflowTaskExecInput;
import org.apache.syncope.common.lib.types.ResourceOperation;
import org.apache.syncope.core.flowable.api.UserRequestHandler;
import org.apache.syncope.core.flowable.api.WorkflowTaskManager;
import org.apache.syncope.core.flowable.support.DomainProcessEngine;
import org.apache.syncope.core.persistence.api.dao.UserDAO;
import org.apache.syncope.core.persistence.api.entity.EntityFactory;
import org.apache.syncope.core.persistence.api.entity.user.User;
import org.apache.syncope.core.provisioning.api.PropagationByResource;
import org.apache.syncope.core.provisioning.api.UserWorkflowResult;
import org.apache.syncope.core.provisioning.api.data.UserDataBinder;
import org.apache.syncope.core.provisioning.api.event.AnyLifecycleEvent;
import org.apache.syncope.core.spring.security.AuthContextUtils;
import org.apache.syncope.core.workflow.api.WorkflowException;
import org.apache.syncope.core.workflow.java.AbstractUserWorkflowAdapter;
import org.flowable.bpmn.model.FlowElement;
import org.flowable.bpmn.model.Gateway;
import org.flowable.bpmn.model.Process;
import org.flowable.bpmn.model.SequenceFlow;
import org.flowable.common.engine.api.FlowableException;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.task.api.Task;
import org.identityconnectors.framework.common.objects.SyncDeltaType;
import org.springframework.beans.BeanUtils;
import org.springframework.context.ApplicationEventPublisher;

public class FlowableUserWorkflowAdapter extends AbstractUserWorkflowAdapter implements WorkflowTaskManager {

    protected final DomainProcessEngine engine;

    protected final UserRequestHandler userRequestHandler;

    protected final ApplicationEventPublisher publisher;

    public FlowableUserWorkflowAdapter(
            final UserDataBinder dataBinder,
            final UserDAO userDAO,
            final EntityFactory entityFactory,
            final DomainProcessEngine engine,
            final UserRequestHandler userRequestHandler,
            final ApplicationEventPublisher publisher) {

        super(dataBinder, userDAO, entityFactory);
        this.engine = engine;
        this.userRequestHandler = userRequestHandler;
        this.publisher = publisher;
    }

    @Override
    public String getPrefix() {
        return "ACT_";
    }

    @Override
    public <T> T getVariable(final String executionId, final String variableName, final Class<T> variableClass) {
        return engine.getRuntimeService().getVariable(executionId, variableName, variableClass);
    }

    @Override
    public void setVariable(final String executionId, final String variableName, final Object value) {
        engine.getRuntimeService().setVariable(executionId, variableName, value);
    }

    protected User lazyLoad(final User user) {
        // using BeanUtils to access all user's properties and trigger lazy loading - we are about to
        // serialize a User instance for availability within workflow tasks, and this breaks transactions
        BeanUtils.copyProperties(user, entityFactory.newEntity(User.class));
        return user;
    }

    @Override
    protected UserWorkflowResult<Pair<String, Boolean>> doCreate(
            final UserCR userCR,
            final boolean disablePwdPolicyCheck,
            final Boolean enabled,
            final String creator,
            final String context) {

        Map<String, Object> variables = new HashMap<>();
        variables.put(FlowableRuntimeUtils.WF_EXECUTOR, AuthContextUtils.getUsername());
        variables.put(FlowableRuntimeUtils.USER_CR, userCR);
        variables.put(FlowableRuntimeUtils.ENABLED, enabled);

        ProcessInstance procInst = null;
        try {
            procInst = engine.getRuntimeService().
                    startProcessInstanceByKey(FlowableRuntimeUtils.WF_PROCESS_ID, variables);
        } catch (FlowableException e) {
            FlowableRuntimeUtils.throwException(
                    e, "While starting " + FlowableRuntimeUtils.WF_PROCESS_ID + " instance");
        }

        engine.getRuntimeService().removeVariable(
                Objects.requireNonNull(procInst).getProcessInstanceId(), FlowableRuntimeUtils.WF_EXECUTOR);
        engine.getRuntimeService().removeVariable(
                procInst.getProcessInstanceId(), FlowableRuntimeUtils.USER_CR);
        engine.getRuntimeService().removeVariable(
                procInst.getProcessInstanceId(), FlowableRuntimeUtils.USER_TO);

        User user = engine.getRuntimeService().
                getVariable(procInst.getProcessInstanceId(), FlowableRuntimeUtils.USER, User.class);
        engine.getRuntimeService().removeVariable(
                procInst.getProcessInstanceId(), FlowableRuntimeUtils.USER);

        Boolean updatedEnabled = engine.getRuntimeService().
                getVariable(procInst.getProcessInstanceId(), FlowableRuntimeUtils.ENABLED, Boolean.class);
        engine.getRuntimeService().removeVariable(
                procInst.getProcessInstanceId(), FlowableRuntimeUtils.ENABLED);
        if (updatedEnabled != null) {
            user.setSuspended(!updatedEnabled);
        }

        // this will make UserValidator not to consider password policies at all
        if (disablePwdPolicyCheck) {
            user.removeClearPassword();
        }

        metadata(user, creator, context);
        FlowableRuntimeUtils.updateStatus(engine, procInst.getProcessInstanceId(), user);
        User created = userDAO.save(user);

        publisher.publishEvent(
                new AnyLifecycleEvent<>(this, SyncDeltaType.CREATE, created, AuthContextUtils.getDomain()));

        engine.getRuntimeService().updateBusinessKey(
                procInst.getProcessInstanceId(), FlowableRuntimeUtils.getWFProcBusinessKey(created.getKey()));

        Boolean propagateEnable = engine.getRuntimeService().getVariable(
                procInst.getProcessInstanceId(), FlowableRuntimeUtils.PROPAGATE_ENABLE, Boolean.class);
        engine.getRuntimeService().removeVariable(
                procInst.getProcessInstanceId(), FlowableRuntimeUtils.PROPAGATE_ENABLE);
        if (propagateEnable == null) {
            propagateEnable = enabled;
        }

        PropagationByResource<String> propByRes = new PropagationByResource<>();
        propByRes.set(ResourceOperation.CREATE, userDAO.findAllResourceKeys(created.getKey()));

        PropagationByResource<Pair<String, String>> propByLinkedAccount = new PropagationByResource<>();
        user.getLinkedAccounts().forEach(account -> propByLinkedAccount.add(
                ResourceOperation.CREATE,
                Pair.of(account.getResource().getKey(), account.getConnObjectKeyValue())));

        FlowableRuntimeUtils.saveForFormSubmit(
                engine,
                procInst.getProcessInstanceId(),
                dataBinder.getUserTO(created, true),
                userCR.getPassword(),
                enabled,
                propByRes,
                propByLinkedAccount);

        Set<String> tasks = FlowableRuntimeUtils.getPerformedTasks(engine, procInst.getProcessInstanceId());

        return new UserWorkflowResult<>(
                Pair.of(created.getKey(), propagateEnable),
                propByRes,
                propByLinkedAccount,
                tasks);
    }

    protected Set<String> doExecuteNextTask(
            final String procInstID,
            final User user,
            final Map<String, Object> moreVariables) {

        Set<String> preTasks = FlowableRuntimeUtils.getPerformedTasks(engine, procInstID);

        Map<String, Object> variables = new HashMap<>();
        variables.put(FlowableRuntimeUtils.WF_EXECUTOR, AuthContextUtils.getUsername());
        variables.put(FlowableRuntimeUtils.USER, lazyLoad(user));

        if (moreVariables != null && !moreVariables.isEmpty()) {
            variables.putAll(moreVariables);
        }

        List<Task> tasks = engine.getTaskService().createTaskQuery().processInstanceId(procInstID).list();
        String task = null;
        if (tasks.size() == 1) {
            try {
                engine.getTaskService().complete(tasks.get(0).getId(), variables);
                task = tasks.get(0).getTaskDefinitionKey();
            } catch (FlowableException e) {
                FlowableRuntimeUtils.throwException(
                        e, "While completing task '" + tasks.get(0).getName() + "' for " + user);
            }
        } else {
            LOG.warn("Expected a single task, found {}", tasks.size());
        }

        Set<String> postTasks = FlowableRuntimeUtils.getPerformedTasks(engine, procInstID);
        postTasks.removeAll(preTasks);
        if (task != null) {
            postTasks.add(task);
        }

        return postTasks;
    }

    @Override
    protected UserWorkflowResult<String> doActivate(
            final User user, final String token, final String updater, final String context) {

        String procInstID = FlowableRuntimeUtils.getWFProcInstID(engine, user.getKey());

        Map<String, Object> variables = new HashMap<>(2);
        variables.put(FlowableRuntimeUtils.TOKEN, token);
        variables.put(FlowableRuntimeUtils.TASK, "activate");

        Set<String> tasks = doExecuteNextTask(procInstID, user, variables);

        metadata(user, updater, context);
        FlowableRuntimeUtils.updateStatus(engine, procInstID, user);
        User updated = userDAO.save(user);

        publisher.publishEvent(
                new AnyLifecycleEvent<>(this, SyncDeltaType.UPDATE, updated, AuthContextUtils.getDomain()));

        variables.keySet().forEach(key -> engine.getRuntimeService().removeVariable(procInstID, key));
        engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.USER);
        engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.WF_EXECUTOR);

        return new UserWorkflowResult<>(updated.getKey(), null, null, tasks);
    }

    @Override
    protected UserWorkflowResult<Pair<UserUR, Boolean>> doUpdate(
            final User user, final UserUR userUR, final String updater, final String context) {

        String procInstID = FlowableRuntimeUtils.getWFProcInstID(engine, user.getKey());

        // save some existing variable values for later processing, after actual update is made 
        UserUR beforeUpdate = engine.getRuntimeService().
                getVariable(procInstID, FlowableRuntimeUtils.USER_UR, UserUR.class);
        @SuppressWarnings("unchecked")
        PropagationByResource<String> propByResBeforeUpdate = engine.getRuntimeService().getVariable(
                procInstID, FlowableRuntimeUtils.PROP_BY_RESOURCE, PropagationByResource.class);
        @SuppressWarnings("unchecked")
        PropagationByResource<Pair<String, String>> propByLinkedAccountBeforeUpdate = engine.getRuntimeService().
                getVariable(procInstID, FlowableRuntimeUtils.PROP_BY_LINKEDACCOUNT, PropagationByResource.class);

        // whether the initial status is a form task
        boolean inFormTask = FlowableRuntimeUtils.getFormTask(engine, procInstID) != null;

        Map<String, Object> variables = new HashMap<>(2);
        variables.put(FlowableRuntimeUtils.USER_UR, userUR);
        variables.put(FlowableRuntimeUtils.TASK, "update");

        Set<String> tasks = doExecuteNextTask(procInstID, user, variables);

        metadata(user, updater, context);
        FlowableRuntimeUtils.updateStatus(engine, procInstID, user);
        User updated = userDAO.save(user);

        publisher.publishEvent(
                new AnyLifecycleEvent<>(this, SyncDeltaType.UPDATE, updated, AuthContextUtils.getDomain()));

        engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.USER);
        engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.WF_EXECUTOR);
        engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.TASK);

        // if the original status was a form task, restore the patch as before the process started
        if (inFormTask) {
            if (beforeUpdate == null) {
                engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.USER_UR);
            } else {
                engine.getRuntimeService().setVariable(procInstID, FlowableRuntimeUtils.USER_UR, beforeUpdate);
            }
        }

        // whether the after status is a form task
        inFormTask = FlowableRuntimeUtils.getFormTask(engine, procInstID) != null;
        if (!inFormTask) {
            engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.USER_UR);
        }

        @SuppressWarnings("unchecked")
        PropagationByResource<String> propByRes = engine.getRuntimeService().getVariable(
                procInstID, FlowableRuntimeUtils.PROP_BY_RESOURCE, PropagationByResource.class);
        engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.PROP_BY_RESOURCE);

        @SuppressWarnings("unchecked")
        PropagationByResource<Pair<String, String>> propByLinkedAccount = engine.getRuntimeService().getVariable(
                procInstID, FlowableRuntimeUtils.PROP_BY_LINKEDACCOUNT, PropagationByResource.class);
        engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.PROP_BY_LINKEDACCOUNT);

        FlowableRuntimeUtils.saveForFormSubmit(
                engine,
                procInstID,
                dataBinder.getUserTO(updated, true),
                userUR.getPassword() == null ? null : userUR.getPassword().getValue(),
                null,
                Optional.ofNullable(propByResBeforeUpdate).orElse(propByRes),
                Optional.ofNullable(propByLinkedAccountBeforeUpdate).orElse(propByLinkedAccount));

        Boolean propagateEnable = engine.getRuntimeService().getVariable(
                procInstID, FlowableRuntimeUtils.PROPAGATE_ENABLE, Boolean.class);
        engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.PROPAGATE_ENABLE);

        return new UserWorkflowResult<>(Pair.of(userUR, propagateEnable), propByRes, propByLinkedAccount, tasks);
    }

    @Override
    protected UserWorkflowResult<String> doSuspend(final User user, final String updater, final String context) {
        String procInstID = FlowableRuntimeUtils.getWFProcInstID(engine, user.getKey());

        Set<String> performedTasks =
                doExecuteNextTask(procInstID, user, Map.of(FlowableRuntimeUtils.TASK, "suspend"));

        metadata(user, updater, context);
        FlowableRuntimeUtils.updateStatus(engine, procInstID, user);
        User updated = userDAO.save(user);

        publisher.publishEvent(
                new AnyLifecycleEvent<>(this, SyncDeltaType.UPDATE, updated, AuthContextUtils.getDomain()));

        @SuppressWarnings("unchecked")
        PropagationByResource<String> propByRes = engine.getRuntimeService().getVariable(
                procInstID, FlowableRuntimeUtils.PROP_BY_RESOURCE, PropagationByResource.class);
        engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.PROP_BY_RESOURCE);

        @SuppressWarnings("unchecked")
        PropagationByResource<Pair<String, String>> propByLinkedAccount = engine.getRuntimeService().getVariable(
                procInstID, FlowableRuntimeUtils.PROP_BY_LINKEDACCOUNT, PropagationByResource.class);
        engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.PROP_BY_LINKEDACCOUNT);

        engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.TASK);
        engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.USER);
        engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.WF_EXECUTOR);
        engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.PROP_BY_RESOURCE);
        engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.PROP_BY_LINKEDACCOUNT);

        return new UserWorkflowResult<>(updated.getKey(), propByRes, propByLinkedAccount, performedTasks);
    }

    @Override
    protected UserWorkflowResult<String> doReactivate(final User user, final String updater, final String context) {
        String procInstID = FlowableRuntimeUtils.getWFProcInstID(engine, user.getKey());

        Set<String> performedTasks =
                doExecuteNextTask(procInstID, user, Map.of(FlowableRuntimeUtils.TASK, "reactivate"));

        metadata(user, updater, context);
        FlowableRuntimeUtils.updateStatus(engine, procInstID, user);
        User updated = userDAO.save(user);

        publisher.publishEvent(
                new AnyLifecycleEvent<>(this, SyncDeltaType.UPDATE, updated, AuthContextUtils.getDomain()));

        @SuppressWarnings("unchecked")
        PropagationByResource<String> propByRes = engine.getRuntimeService().getVariable(
                procInstID, FlowableRuntimeUtils.PROP_BY_RESOURCE, PropagationByResource.class);
        engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.PROP_BY_RESOURCE);

        @SuppressWarnings("unchecked")
        PropagationByResource<Pair<String, String>> propByLinkedAccount = engine.getRuntimeService().getVariable(
                procInstID, FlowableRuntimeUtils.PROP_BY_LINKEDACCOUNT, PropagationByResource.class);
        engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.PROP_BY_LINKEDACCOUNT);

        engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.TASK);
        engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.USER);
        engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.WF_EXECUTOR);
        engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.PROP_BY_RESOURCE);
        engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.PROP_BY_LINKEDACCOUNT);

        return new UserWorkflowResult<>(updated.getKey(), propByRes, propByLinkedAccount, performedTasks);
    }

    @Override
    protected void doRequestPasswordReset(final User user, final String updater, final String context) {
        Map<String, Object> variables = new HashMap<>(3);
        variables.put(FlowableRuntimeUtils.USER_TO, dataBinder.getUserTO(user, true));
        variables.put(FlowableRuntimeUtils.TASK, "requestPasswordReset");
        variables.put(FlowableRuntimeUtils.EVENT, "requestPasswordReset");

        String procInstID = FlowableRuntimeUtils.getWFProcInstID(engine, user.getKey());

        doExecuteNextTask(procInstID, user, variables);

        metadata(user, updater, context);
        User updated = userDAO.save(user);

        publisher.publishEvent(
                new AnyLifecycleEvent<>(this, SyncDeltaType.UPDATE, updated, AuthContextUtils.getDomain()));

        variables.keySet().forEach(key -> engine.getRuntimeService().removeVariable(procInstID, key));
        engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.USER);
        engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.WF_EXECUTOR);
    }

    @Override
    protected UserWorkflowResult<Pair<UserUR, Boolean>> doConfirmPasswordReset(
            final User user, final String token, final String password, final String updater, final String context) {

        Map<String, Object> variables = new HashMap<>(5);
        variables.put(FlowableRuntimeUtils.TOKEN, token);
        variables.put(FlowableRuntimeUtils.PASSWORD, password);
        variables.put(FlowableRuntimeUtils.USER_TO, dataBinder.getUserTO(user, true));
        variables.put(FlowableRuntimeUtils.TASK, "confirmPasswordReset");
        variables.put(FlowableRuntimeUtils.EVENT, "confirmPasswordReset");

        String procInstID = FlowableRuntimeUtils.getWFProcInstID(engine, user.getKey());

        Set<String> tasks = doExecuteNextTask(procInstID, user, variables);

        metadata(user, updater, context);
        User updated = userDAO.save(user);

        publisher.publishEvent(
                new AnyLifecycleEvent<>(this, SyncDeltaType.UPDATE, updated, AuthContextUtils.getDomain()));

        variables.keySet().forEach(key -> engine.getRuntimeService().removeVariable(procInstID, key));
        engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.USER);
        engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.WF_EXECUTOR);

        @SuppressWarnings("unchecked")
        PropagationByResource<String> propByRes = engine.getRuntimeService().getVariable(
                procInstID, FlowableRuntimeUtils.PROP_BY_RESOURCE, PropagationByResource.class);
        engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.PROP_BY_RESOURCE);
        @SuppressWarnings("unchecked")
        PropagationByResource<Pair<String, String>> propByLinkedAccount = engine.getRuntimeService().getVariable(
                procInstID, FlowableRuntimeUtils.PROP_BY_LINKEDACCOUNT, PropagationByResource.class);
        engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.PROP_BY_LINKEDACCOUNT);
        UserUR updatedReq = engine.getRuntimeService().getVariable(
                procInstID, FlowableRuntimeUtils.USER_UR, UserUR.class);
        engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.USER_UR);
        Boolean propagateEnable = engine.getRuntimeService().getVariable(
                procInstID, FlowableRuntimeUtils.PROPAGATE_ENABLE, Boolean.class);
        engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.PROPAGATE_ENABLE);

        return new UserWorkflowResult<>(Pair.of(updatedReq, propagateEnable), propByRes, propByLinkedAccount, tasks);
    }

    @Override
    protected void doDelete(final User user, final String eraser, final String context) {
        String procInstID = FlowableRuntimeUtils.getWFProcInstID(engine, user.getKey());

        doExecuteNextTask(procInstID, user, Map.of(FlowableRuntimeUtils.TASK, "delete"));

        PropagationByResource<String> propByRes = new PropagationByResource<>();
        propByRes.set(ResourceOperation.DELETE, userDAO.findAllResourceKeys(user.getKey()));

        PropagationByResource<Pair<String, String>> propByLinkedAccount = new PropagationByResource<>();
        user.getLinkedAccounts().forEach(account -> propByLinkedAccount.add(
                ResourceOperation.DELETE,
                Pair.of(account.getResource().getKey(), account.getConnObjectKeyValue())));

        if (engine.getRuntimeService().createProcessInstanceQuery().
                processInstanceId(procInstID).active().list().isEmpty()) {

            userDAO.delete(user.getKey());

            publisher.publishEvent(
                    new AnyLifecycleEvent<>(this, SyncDeltaType.DELETE, user, AuthContextUtils.getDomain()));

            if (!engine.getHistoryService().createHistoricProcessInstanceQuery().
                    processInstanceId(procInstID).list().isEmpty()) {

                engine.getHistoryService().deleteHistoricProcessInstance(procInstID);
            }
        } else {
            FlowableRuntimeUtils.saveForFormSubmit(
                    engine,
                    procInstID,
                    dataBinder.getUserTO(user, true),
                    null,
                    null,
                    propByRes,
                    propByLinkedAccount);

            FlowableRuntimeUtils.updateStatus(engine, procInstID, user);
            metadata(user, eraser, context);
            User updated = userDAO.save(user);

            publisher.publishEvent(
                    new AnyLifecycleEvent<>(this, SyncDeltaType.UPDATE, updated, AuthContextUtils.getDomain()));

            engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.TASK);
            engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.USER);
            engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.WF_EXECUTOR);
        }
    }

    @Override
    public UserWorkflowResult<String> executeNextTask(final WorkflowTaskExecInput workflowTaskExecInput) {
        User user = userDAO.authFind(workflowTaskExecInput.getUserKey());

        String procInstID = FlowableRuntimeUtils.getWFProcInstID(engine, user.getKey());

        Map<String, Object> variables = new HashMap<>();
        variables.put(FlowableRuntimeUtils.USER_TO, dataBinder.getUserTO(user, true));
        variables.putAll(workflowTaskExecInput.getVariables());

        Set<String> performedTasks = doExecuteNextTask(procInstID, user, variables);
        FlowableRuntimeUtils.updateStatus(engine, procInstID, user);
        user = userDAO.save(user);

        publisher.publishEvent(
                new AnyLifecycleEvent<>(this, SyncDeltaType.UPDATE, user, AuthContextUtils.getDomain()));

        engine.getRuntimeService().setVariable(
                procInstID, FlowableRuntimeUtils.USER_TO, dataBinder.getUserTO(user, true));

        if (engine.getRuntimeService().createProcessInstanceQuery().
                processInstanceId(procInstID).active().list().isEmpty()) {

            userDAO.delete(user.getKey());

            publisher.publishEvent(
                    new AnyLifecycleEvent<>(this, SyncDeltaType.DELETE, user, AuthContextUtils.getDomain()));

            if (!engine.getHistoryService().createHistoricProcessInstanceQuery().
                    processInstanceId(procInstID).list().isEmpty()) {

                engine.getHistoryService().deleteHistoricProcessInstance(procInstID);
            }
        } else {
            @SuppressWarnings("unchecked")
            PropagationByResource<String> propByRes = engine.getRuntimeService().
                    getVariable(procInstID, FlowableRuntimeUtils.PROP_BY_RESOURCE, PropagationByResource.class);
            @SuppressWarnings("unchecked")
            PropagationByResource<Pair<String, String>> propByLinkedAccount = engine.getRuntimeService().getVariable(
                    procInstID, FlowableRuntimeUtils.PROP_BY_LINKEDACCOUNT, PropagationByResource.class);

            FlowableRuntimeUtils.saveForFormSubmit(
                    engine,
                    procInstID,
                    dataBinder.getUserTO(user, true),
                    null,
                    null,
                    propByRes,
                    propByLinkedAccount);
        }

        return new UserWorkflowResult<>(user.getKey(), null, null, performedTasks);
    }

    protected static void navigateAvailableTasks(final FlowElement flow, final List<String> availableTasks) {
        if (flow instanceof Gateway) {
            ((Gateway) flow).getOutgoingFlows().forEach(subflow -> navigateAvailableTasks(subflow, availableTasks));
        } else if (flow instanceof SequenceFlow) {
            navigateAvailableTasks(((SequenceFlow) flow).getTargetFlowElement(), availableTasks);
        } else if (flow instanceof org.flowable.bpmn.model.Task) {
            availableTasks.add(flow.getId());
        } else {
            LOG.debug("Unexpected flow found: {}", flow);
        }
    }

    @Override
    public List<WorkflowTask> getAvailableTasks(final String userKey) {
        String procInstID = FlowableRuntimeUtils.getWFProcInstID(engine, userKey);

        List<String> availableTasks = new ArrayList<>();
        try {
            Task currentTask = engine.getTaskService().createTaskQuery().processInstanceId(procInstID).singleResult();

            Process process = engine.getRepositoryService().
                    getBpmnModel(FlowableRuntimeUtils.getLatestProcDefByKey(
                            engine, FlowableRuntimeUtils.WF_PROCESS_ID).getId()).getProcesses().get(0);
            process.getFlowElements().stream().
                    filter(SequenceFlow.class::isInstance).
                    map(SequenceFlow.class::cast).
                    filter(flow -> flow.getSourceRef().equals(currentTask.getTaskDefinitionKey())).
                    forEach(flow -> navigateAvailableTasks(flow.getTargetFlowElement(), availableTasks));
        } catch (FlowableException e) {
            throw new WorkflowException(
                    "While reading available tasks for workflow instance " + procInstID, e);
        }

        return availableTasks.stream().map(input -> {
            WorkflowTask workflowTaskTO = new WorkflowTask();
            workflowTaskTO.setName(input);
            return workflowTaskTO;
        }).collect(Collectors.toList());
    }
}
