package cn.axzo.workflow.core.service.impl;

import cn.axzo.workflow.common.enums.BpmnFlowNodeMode;
import cn.axzo.workflow.common.enums.BpmnProcessInstanceResultEnum;
import cn.axzo.workflow.common.model.request.bpmn.process.BpmnProcessInstanceCancelDTO;
import cn.axzo.workflow.common.model.request.bpmn.process.BpmnProcessInstanceCreateDTO;
import cn.axzo.workflow.common.model.request.bpmn.process.BpmnProcessInstanceCreateWithFormDTO;
import cn.axzo.workflow.common.model.request.bpmn.process.BpmnProcessInstanceMyPageReqVO;
import cn.axzo.workflow.common.model.request.bpmn.process.BpmnProcessInstanceQueryDTO;
import cn.axzo.workflow.common.model.request.bpmn.process.HistoricProcessInstanceSearchDTO;
import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskDelegateAssigner;
import cn.axzo.workflow.common.model.response.BpmPageResult;
import cn.axzo.workflow.common.model.response.bpmn.process.BpmnProcessDefinitionVO;
import cn.axzo.workflow.common.model.response.bpmn.process.BpmnProcessInstancePageItemVO;
import cn.axzo.workflow.common.model.response.bpmn.process.BpmnProcessInstanceVO;
import cn.axzo.workflow.common.model.response.bpmn.process.HistoricProcessInstanceVO;
import cn.axzo.workflow.common.model.response.bpmn.process.ProcessNodeDetailVO;
import cn.axzo.workflow.core.common.enums.BpmnProcessTaskResultEnum;
import cn.axzo.workflow.core.common.exception.WorkflowEngineException;
import cn.axzo.workflow.core.common.utils.BpmnCollectionUtils;
import cn.axzo.workflow.core.service.BpmnProcessDefinitionService;
import cn.axzo.workflow.core.service.BpmnProcessInstanceService;
import cn.axzo.workflow.core.service.converter.BpmnHistoricProcessInstanceConverter;
import cn.axzo.workflow.core.service.converter.BpmnProcessInstanceConverter;
import cn.axzo.workflow.core.service.converter.BpmnProcessInstancePageItemConverter;
import cn.axzo.workflow.core.service.support.FlowNodeForecastService;
import cn.axzo.workflow.core.service.support.ProcessGraphicService;
import cn.azxo.framework.common.utils.StringUtils;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.bpmn.model.FlowElement;
import org.flowable.bpmn.model.UserTask;
import org.flowable.common.engine.impl.db.SuspensionState;
import org.flowable.common.engine.impl.identity.Authentication;
import org.flowable.engine.HistoryService;
import org.flowable.engine.ManagementService;
import org.flowable.engine.RepositoryService;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.history.HistoricActivityInstance;
import org.flowable.engine.history.HistoricProcessInstance;
import org.flowable.engine.history.HistoricProcessInstanceQuery;
import org.flowable.engine.history.NativeHistoricProcessInstanceQuery;
import org.flowable.engine.impl.bpmn.behavior.MultiInstanceActivityBehavior;
import org.flowable.engine.impl.bpmn.behavior.UserTaskActivityBehavior;
import org.flowable.engine.repository.ProcessDefinition;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.engine.runtime.ProcessInstanceQuery;
import org.flowable.form.api.FormInfo;
import org.flowable.variable.api.history.HistoricVariableInstance;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import javax.annotation.Nullable;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import static cn.axzo.workflow.common.constant.BpmnConstants.AND_SIGN_EXPRESSION;
import static cn.axzo.workflow.common.constant.BpmnConstants.BIZ_ORG_RELATION;
import static cn.axzo.workflow.common.constant.BpmnConstants.INTERNAL_DELETE_PROCESS_FLAG;
import static cn.axzo.workflow.common.constant.BpmnConstants.INTERNAL_END_TENANT_ID;
import static cn.axzo.workflow.common.constant.BpmnConstants.INTERNAL_END_USER_ID;
import static cn.axzo.workflow.common.constant.BpmnConstants.INTERNAL_END_USER_NAME;
import static cn.axzo.workflow.common.constant.BpmnConstants.INTERNAL_INITIATOR;
import static cn.axzo.workflow.common.constant.BpmnConstants.INTERNAL_PROCESS_TYPE_CANCEL;
import static cn.axzo.workflow.common.constant.BpmnConstants.INTERNAL_SPECIFY_NEXT_APPROVER;
import static cn.axzo.workflow.common.enums.BpmnFlowNodeMode.AND;
import static cn.axzo.workflow.common.enums.BpmnFlowNodeMode.OR;
import static cn.axzo.workflow.core.common.enums.BpmnErrorCode.PROCESS_DEFINITION_ID_NOT_EXISTS;
import static cn.axzo.workflow.core.common.enums.BpmnErrorCode.PROCESS_DEFINITION_IS_SUSPENDED;
import static cn.axzo.workflow.core.common.enums.BpmnErrorCode.PROCESS_DEFINITION_KEY_NOT_EXISTS;
import static cn.axzo.workflow.core.common.enums.BpmnErrorCode.PROCESS_INSTANCE_CANCELLED;
import static cn.axzo.workflow.core.common.enums.BpmnErrorCode.PROCESS_INSTANCE_CANCEL_FAIL_NOT_EXISTS;
import static cn.axzo.workflow.core.common.enums.BpmnErrorCode.PROCESS_INSTANCE_CANCEL_FAIL_NOT_SELF;
import static cn.axzo.workflow.core.common.enums.BpmnErrorCode.PROCESS_INSTANCE_ID_NOT_EXISTS;
import static cn.axzo.workflow.core.common.enums.BpmnErrorCode.PROCESS_INSTANCE_NOT_EXISTS;
import static cn.axzo.workflow.core.common.enums.BpmnErrorCode.PROCESS_OPERATION_PARAM_VALID_ERROR;
import static cn.axzo.workflow.core.common.utils.BpmnNativeQueryUtil.countSql;
import static cn.axzo.workflow.core.common.utils.BpmnNativeQueryUtil.sqlConnectors;

@Service
@Slf4j
public class BpmnProcessInstanceServiceImpl implements BpmnProcessInstanceService {

    @Resource
    private RepositoryService repositoryService;
    @Resource
    private RuntimeService runtimeService;
    @Resource
    private BpmnProcessDefinitionService processDefinitionService;
    @Resource
    private HistoryService historyService;
    @Resource
    private BpmnProcessInstancePageItemConverter instancePageItemConverter;
    @Resource
    private BpmnProcessInstanceConverter instanceConverter;
    @Resource
    private BpmnHistoricProcessInstanceConverter historicProcessInstanceConverter;
    @Resource
    private ManagementService managementService;
    @Resource
    private ProcessGraphicService graphicService;
    @Resource
    private FlowNodeForecastService forecastService;

    @Override
    public HistoricProcessInstance getProcessInstanceByBusinessKey(String businessKey, @Nullable String tenantId,
                                                                   @Nullable Boolean hasVariable) {
        HistoricProcessInstanceQuery query =
                historyService.createHistoricProcessInstanceQuery().processInstanceBusinessKey(businessKey);
        if (Boolean.TRUE.equals(hasVariable)) {
            query.includeProcessVariables();
        }
        if (StringUtils.isNotBlank(tenantId)) {
            query.processInstanceTenantId(tenantId);
        }
        return query.singleResult();
    }

    @Override
    public BpmPageResult<BpmnProcessInstancePageItemVO> getMyProcessInstancePage(@Valid BpmnProcessInstanceMyPageReqVO dto) {
        HistoricProcessInstanceQuery query =
                historyService.createHistoricProcessInstanceQuery().processInstanceTenantId(dto.getTenantId());
        if (StringUtils.isNotBlank(dto.getName())) {
            query.processInstanceName(dto.getName());
        }
        if (StringUtils.isNotBlank(dto.getProcessDefinitionId())) {
            query.processDefinitionId(dto.getProcessDefinitionId());
        }
        if (StringUtils.isNotBlank(dto.getCategory())) {
            query.processDefinitionCategory(dto.getCategory());
        }
        if (StringUtils.isNotBlank(dto.getBusinessKey())) {
            query.processInstanceBusinessKey(dto.getBusinessKey());
        }
        if (StringUtils.isNotBlank(dto.getUserId())) {
            query.startedBy(dto.getUserId());
        }
        if (Objects.nonNull(dto.getBeginCreateTime())) {
            query.startedAfter(dto.getBeginCreateTime());
        }
        if (Objects.nonNull(dto.getEndCreateTime())) {
            query.startedBefore(dto.getEndCreateTime());
        }
        if (Objects.nonNull(dto.getResult())) {
            query.processInstanceBusinessStatus(dto.getResult().getStatus());
        }
        if (StringUtils.isNotBlank(dto.getTenantId())) {
            query.processInstanceTenantId(dto.getTenantId());
        }
        List<HistoricProcessInstance> instances = query.orderByProcessInstanceStartTime().desc()
                .listPage((dto.getPageNo() - 1) * dto.getPageSize(), dto.getPageSize());

        if (CollectionUtils.isEmpty(instances)) {
            return BpmPageResult.empty();
        }

        List<BpmnProcessInstancePageItemVO> vos = new ArrayList<>();
        instances.forEach(i -> {
            BpmnProcessInstancePageItemVO vo = instancePageItemConverter.toVo(i);
            // 暂不对外提供发起人姓名
            //            List<HistoricVariableInstance> list = historyService.createHistoricVariableInstanceQuery()
            //                    .processInstanceId(i.getId()).variableName(INTERNAL_START_USER_NAME).list();
            //            vo.setStartUserName((String) list.get(0).getValue());
            vos.add(vo);
        });
        return new BpmPageResult<>(vos, query.count());
    }

    @Override
    public HistoricProcessInstance getProcessInstance(String id, @Nullable String tenantId,
                                                      @Nullable Boolean hasVariable) {
        HistoricProcessInstanceQuery query = historyService.createHistoricProcessInstanceQuery().processInstanceId(id);
        if (Boolean.TRUE.equals(hasVariable)) {
            query.includeProcessVariables();
        }
        if (StringUtils.isNotBlank(tenantId)) {
            query.processInstanceTenantId(tenantId);
        }
        return query.singleResult();
    }


    public List<ProcessInstance> getProcessInstances(Set<String> ids) {
        return runtimeService.createProcessInstanceQuery().processInstanceIds(ids).list();
    }


    @Override
    public Boolean updateProcessStatus(String processDefinitionId, Integer status) {
        // 激活
        if (Objects.equals(SuspensionState.ACTIVE.getStateCode(), status)) {
            repositoryService.activateProcessDefinitionById(processDefinitionId, false, null);
            return true;
        }
        // 挂起
        if (Objects.equals(SuspensionState.SUSPENDED.getStateCode(), status)) {
            // suspendProcessInstances = false，进行中的任务，不进行挂起。
            // 原因：只要新的流程不允许发起即可，老流程继续可以执行。
            repositoryService.suspendProcessDefinitionById(processDefinitionId, false, null);
            return true;
        }
        log.error("[updateProcessDefinitionState][流程定义({}) 修改未知状态({})]", processDefinitionId, status);
        return false;
    }


    @Override
    public String createProcessInstance(BpmnProcessInstanceCreateDTO dto) {
        BpmnProcessDefinitionVO definition =
                processDefinitionService.getActiveProcessDefinitionByKey(dto.getProcessDefinitionKey(),
                        dto.getTenantId());
        // 校验流程定义
        if (definition == null) {
            throw new WorkflowEngineException(PROCESS_DEFINITION_KEY_NOT_EXISTS, dto.getProcessDefinitionKey());
        }
        if (Objects.equals(SuspensionState.SUSPENDED.getStateCode(), definition.getSuspensionState())) {
            throw new WorkflowEngineException(PROCESS_DEFINITION_IS_SUSPENDED, dto.getProcessDefinitionKey());
        }

        dto.getVariables().put(INTERNAL_INITIATOR, dto.getInitiator());
        dto.getVariables().put(BIZ_ORG_RELATION, dto.getCooperationOrg());
        if (Objects.nonNull(dto.getNextApprover())) {
            BpmnTaskDelegateAssigner nextApprover = dto.getNextApprover();
            nextApprover.setTenantId(StringUtils.isNotBlank(nextApprover.getTenantId()) ? nextApprover.getTenantId() :
                    dto.getInitiator().getTenantId());
            dto.getVariables().put(INTERNAL_SPECIFY_NEXT_APPROVER, nextApprover);
        }

        // 创建流程实例
        // 设置流程实例的开始人，参考 https://wenku.baidu.com/view/5538062e7a563c1ec5da50e2524de518964bd3f9.html
        Authentication.setAuthenticatedUserId(dto.getInitiator().buildAssigneeId());
        String name = StringUtils.isNotBlank(dto.getCustomProcessInstanceName()) ?
                dto.getCustomProcessInstanceName()
                : definition.getName();
        // 设置流程名字
        ProcessInstance instance = runtimeService.createProcessInstanceBuilder()
                .processDefinitionId(definition.getId())
                .businessKey(dto.getBusinessKey())
                .variables(dto.getVariables())
                .name(name)
                .tenantId(dto.getInitiator().getTenantId())
                .start();
        return instance.getProcessInstanceId();
    }

    @Override
    public String createProcessInstanceWithForm(BpmnProcessInstanceCreateWithFormDTO dto) {
        BpmnProcessDefinitionVO definition =
                processDefinitionService.getActiveProcessDefinitionByKey(dto.getProcessDefinitionKey(),
                        dto.getInitiator().getTenantId());
        // 校验流程定义
        if (definition == null) {
            throw new WorkflowEngineException(PROCESS_DEFINITION_KEY_NOT_EXISTS, dto.getProcessDefinitionKey());
        }
        if (Objects.equals(SuspensionState.SUSPENDED.getStateCode(), definition.getSuspensionState())) {
            throw new WorkflowEngineException(PROCESS_DEFINITION_IS_SUSPENDED, dto.getProcessDefinitionKey());
        }
        dto.getVariables().put(INTERNAL_INITIATOR, dto.getInitiator());

        // 创建流程实例
        // 设置流程实例的开始人，参考https://wenku.baidu.com/view/5538062e7a563c1ec5da50e2524de518964bd3f9.html
        Authentication.setAuthenticatedUserId(dto.getInitiator().buildAssigneeId());
        String name = StringUtils.isNotBlank(dto.getCustomProcessInstanceName()) ?
                dto.getCustomProcessInstanceName()
                : definition.getName();

        ProcessInstance instance = runtimeService.startProcessInstanceWithForm(definition.getId(),
                dto.getOutcome(), dto.getVariables(), name);
        return instance.getProcessInstanceId();
    }

    @Override
    public FormInfo getStartFormModel(String processDefinitionId, String processInstanceId) {
        return runtimeService.getStartFormModel(processDefinitionId, processInstanceId);
    }

    @Override
    public Boolean cancelProcessInstance(BpmnProcessInstanceCancelDTO dto) {
        HistoricProcessInstance instance = null;
        if (StringUtils.isNotBlank(dto.getId())) {
            instance = getProcessInstance(dto.getId(), null, false);
            //            instance = getProcessInstance(dto.getId(), dto.getInitiator().getTenantId(), false);
        } else if (StringUtils.isNotBlank(dto.getBusinessKey())) {
            instance = getProcessInstanceByBusinessKey(dto.getBusinessKey(), dto.getInitiator().getTenantId(), false);
        } else {
            throw new WorkflowEngineException(PROCESS_OPERATION_PARAM_VALID_ERROR);
        }
        if (instance == null) {
            throw new WorkflowEngineException(PROCESS_INSTANCE_CANCEL_FAIL_NOT_EXISTS);
        }
        if (!Objects.equals(instance.getStartUserId(), dto.getInitiator().buildAssigneeId())) {
            throw new WorkflowEngineException(PROCESS_INSTANCE_CANCEL_FAIL_NOT_SELF);
        }
        if (Objects.nonNull(instance.getEndTime()) && Objects.equals(BpmnProcessInstanceResultEnum.CANCELLED.getStatus(), instance.getBusinessStatus())) {
            throw new WorkflowEngineException(PROCESS_INSTANCE_CANCELLED);
        }

        Map<String, Object> variables = new HashMap<>();
        variables.put(INTERNAL_END_TENANT_ID, dto.getInitiator().getTenantId());
        variables.put(INTERNAL_END_USER_ID, dto.getInitiator().buildAssigneeId());
        variables.put(INTERNAL_END_USER_NAME, dto.getInitiator().getAssignerName());
        variables.put(INTERNAL_DELETE_PROCESS_FLAG, INTERNAL_PROCESS_TYPE_CANCEL);
        runtimeService.setVariables(instance.getId(), variables);

        String deleteReason = StringUtils.isBlank(dto.getReason()) ?
                BpmnProcessTaskResultEnum.INITIATOR_REVOCATION.getDesc()
                : BpmnProcessTaskResultEnum.INITIATOR_REVOCATION.getDesc() + ", 删除原因: " + dto.getReason();
        deleteProcessInstance(instance.getId(), deleteReason);
        return true;
    }

    @Override
    public HistoricProcessInstance getHistoricProcessInstance(String id, String tenantId) {
        HistoricProcessInstanceQuery query = historyService.createHistoricProcessInstanceQuery().processInstanceId(id);
        if (StringUtils.isNotBlank(tenantId)) {
            query.processInstanceTenantId(tenantId);
        }
        return query.singleResult();
    }


    public HistoricProcessInstance getHistoricProcessInstanceByBusinessKey(String businessKey, String tenantId) {
        HistoricProcessInstanceQuery query =
                historyService.createHistoricProcessInstanceQuery().processInstanceBusinessKey(businessKey);
        if (StringUtils.isNotBlank(tenantId)) {
            query.processInstanceTenantId(tenantId);
        }
        return query.singleResult();
    }

    @Override
    public BpmnProcessInstanceVO getProcessInstanceVO(BpmnProcessInstanceQueryDTO dto) {
        HistoricProcessInstance processInstance = null;
        if (StringUtils.isNotBlank(dto.getProcessInstanceId())) {
            processInstance = getProcessInstance(dto.getProcessInstanceId(), dto.getTenantId(), dto.getHasVariable());
        } else if (StringUtils.isNotBlank(dto.getBusinessKey())) {
            processInstance = getProcessInstanceByBusinessKey(dto.getBusinessKey(), dto.getTenantId(),
                    dto.getHasVariable());
        }

        // 获得流程实例
        if (Objects.isNull(processInstance)) {
            throw new WorkflowEngineException(PROCESS_INSTANCE_NOT_EXISTS);
        }
        HistoricVariableInstance initiatorVariableInstance = historyService.createHistoricVariableInstanceQuery()
                .processInstanceId(processInstance.getId()).variableName(INTERNAL_INITIATOR).singleResult();

        // 获得流程定义
        BpmnProcessDefinitionVO processDefinition = processDefinitionService
                .getProcessDefinition(processInstance.getProcessDefinitionId());
        if (Objects.isNull(processDefinition)) {
            throw new WorkflowEngineException(PROCESS_DEFINITION_ID_NOT_EXISTS,
                    processInstance.getProcessDefinitionId());
        }

        return instanceConverter.toVo(processInstance, processDefinition, initiatorVariableInstance);
    }

    @Override
    public void deleteProcessInstance(String id, String reason) {
        runtimeService.deleteProcessInstance(id, reason);
    }

    /**
     * 获得流程实例 Map
     *
     * @param ids 流程实例的编号集合
     * @return 流程实例列表 Map
     */
    public Map<String, ProcessInstance> getProcessInstanceMap(Set<String> ids) {
        return BpmnCollectionUtils.convertMap(getProcessInstances(ids),
                ProcessInstance::getProcessInstanceId);
    }


    /**
     * 获得历史的流程实例 Map
     *
     * @param ids 流程实例的编号集合
     * @return 历史的流程实例列表 Map
     */
    public Map<String, HistoricProcessInstance> getHistoricProcessInstanceMap(Set<String> ids) {
        return BpmnCollectionUtils.convertMap(getHistoricProcessInstances(ids),
                HistoricProcessInstance::getId);
    }

    public List<HistoricProcessInstance> getHistoricProcessInstances(Set<String> ids) {
        HistoricProcessInstanceQuery historicProcessInstanceQuery =
                historyService.createHistoricProcessInstanceQuery().processInstanceIds(ids);
        return historicProcessInstanceQuery.list();
    }

    @Override
    public ObjectNode getProcessInstanceGraphical(String processInstanceId, @Nullable String tenantId) {
        HistoricProcessInstanceQuery query = historyService.createHistoricProcessInstanceQuery()
                .processInstanceId(processInstanceId);
        if (StringUtils.isNotBlank(tenantId)) {
            query.processInstanceTenantId(tenantId);
        }
        HistoricProcessInstance processInstance = query.singleResult();
        if (Objects.isNull(processInstance)) {
            throw new WorkflowEngineException(PROCESS_INSTANCE_NOT_EXISTS);
        }
        BpmnModel bpmnModel = repositoryService.getBpmnModel(processInstance.getProcessDefinitionId());

        List<HistoricActivityInstance> activityInstances =
                historyService.createHistoricActivityInstanceQuery().processInstanceId(processInstanceId).list();

        Set<String> completedActivityInstances = new HashSet<>();
        Set<String> currentElements = new HashSet<>();
        if (!CollectionUtils.isEmpty(activityInstances)) {
            for (HistoricActivityInstance activityInstance : activityInstances) {
                if (activityInstance.getEndTime() != null) {
                    completedActivityInstances.add(activityInstance.getActivityId());
                } else {
                    currentElements.add(activityInstance.getActivityId());
                }
            }
        }
        // Gather completed flows
        List<String> completedFlows = graphicService.gatherCompletedFlows(completedActivityInstances, currentElements
                , bpmnModel);

        Set<String> completedElements = new HashSet<>(completedActivityInstances);
        completedElements.addAll(completedFlows);

        ObjectNode displayNode = graphicService.processProcessElements(bpmnModel, completedElements, currentElements,
                new ArrayList<>(), processInstanceId);
        if (!CollectionUtils.isEmpty(completedActivityInstances)) {
            ArrayNode completedActivities = displayNode.putArray("completedActivities");
            for (String completed : completedActivityInstances) {
                completedActivities.add(completed);
            }
        }

        if (!CollectionUtils.isEmpty(currentElements)) {
            ArrayNode currentActivities = displayNode.putArray("currentActivities");
            for (String current : currentElements) {
                currentActivities.add(current);
            }
        }

        if (!CollectionUtils.isEmpty(completedFlows)) {
            ArrayNode completedSequenceFlows = displayNode.putArray("completedSequenceFlows");
            for (String current : completedFlows) {
                completedSequenceFlows.add(current);
            }
        }
        return displayNode;
    }

    @Override
    public BpmPageResult<HistoricProcessInstanceVO> historicProcessInstancePage(HistoricProcessInstanceSearchDTO dto) {
        NativeHistoricProcessInstanceQuery query = historyService.createNativeHistoricProcessInstanceQuery();
        String tableName = managementService.getTableName(HistoricProcessInstance.class);
        StringBuilder baseQuerySql = new StringBuilder("SELECT a.* FROM ").append(tableName).append(" a");
        if (StringUtils.isNotBlank(dto.getCategory())) {
            baseQuerySql.append(" LEFT JOIN ACT_RE_PROCDEF b ON a.PROC_DEF_ID_ = b.ID_ ")
                    .append(sqlConnectors(baseQuerySql))
                    .append(" b.CATEGORY_ = #{category}");
            query.parameter("category", dto.getCategory());
        }
        if (StringUtils.isNotBlank(dto.getSearchKey())) {
            baseQuerySql.append(sqlConnectors(baseQuerySql))
                    .append(" (a.BUSINESS_KEY_ LIKE #{searchKey}")
                    .append(" or")
                    .append(" a.NAME_ LIKE #{searchKey})");
            query.parameter("searchKey", "%" + dto.getSearchKey() + "%");
        }
        if (StringUtils.isNotBlank(dto.getResult())) {
            baseQuerySql.append(sqlConnectors(baseQuerySql))
                    .append(" a.BUSINESS_STATUS_ = #{result}");
            query.parameter("result", dto.getResult());
        }
        if (StringUtils.isNotBlank(dto.getTenantId())) {
            baseQuerySql.append(sqlConnectors(baseQuerySql))
                    .append(" a.TENANT_ID_ = #{tenantId}");
            query.parameter("tenantId", dto.getTenantId());
        }
        if (!CollectionUtils.isEmpty(dto.getUserIds())) {
            baseQuerySql.append(sqlConnectors(baseQuerySql))
                    .append(" a.START_USER_ID_ in (");
            for (int i = 0; i < dto.getUserIds().size(); i++) {
                baseQuerySql.append("#{userId").append(i).append("}");
                if (i < dto.getUserIds().size() - 1) {
                    baseQuerySql.append(",");
                }
                query.parameter("userId" + i, dto.getUserIds().get(i));
            }
            baseQuerySql.append(")");
        }
        if (Objects.nonNull(dto.getStartBeginTime()) && Objects.nonNull(dto.getStartEndTime())) {
            baseQuerySql.append(sqlConnectors(baseQuerySql))
                    .append(" a.START_TIME_ between #{startBeginTime} and #{startEndTime}");
            query.parameter("startBeginTime", dto.getStartBeginTime());
            query.parameter("startEndTime", dto.getStartEndTime());
        }
        if (Objects.nonNull(dto.getFinishedBeginTime()) && Objects.nonNull(dto.getFinishedEndTime())) {
            baseQuerySql.append(sqlConnectors(baseQuerySql))
                    .append(" a.END_TIME_ between #{finishedBeginTime} and #{finishedEndTime}");
            query.parameter("finishedBeginTime", dto.getFinishedBeginTime());
            query.parameter("finishedEndTime", dto.getFinishedEndTime());
        }
        baseQuerySql.append(" ORDER BY a.START_TIME_ DESC");
        NativeHistoricProcessInstanceQuery dataSqlQuery = query.sql(baseQuerySql.toString());
        List<HistoricProcessInstance> instances = dataSqlQuery
                .listPage((dto.getPageNo() - 1) * dto.getPageSize(), dto.getPageSize());
        if (CollectionUtils.isEmpty(instances)) {
            return BpmPageResult.empty();
        }

        NativeHistoricProcessInstanceQuery countSqlQuery = query.sql(countSql(baseQuerySql));
        if (StringUtils.isNotBlank(dto.getCategory())) {
            return new BpmPageResult(historicProcessInstanceConverter.toVos(instances, dto.getCategory()),
                    countSqlQuery.count());
        } else {
            Set<String> procDefIds = new HashSet<>();
            instances.forEach(i -> procDefIds.add(i.getProcessDefinitionId()));
            Map<String, String> defCategoryMap =
                    repositoryService.createProcessDefinitionQuery().processDefinitionIds(procDefIds).list()
                            .stream()
                            .collect(Collectors.toMap(ProcessDefinition::getId, ProcessDefinition::getCategory, (s,
                                                                                                                 t) -> s));
            List<HistoricProcessInstanceVO> vos = new ArrayList<>();
            instances.forEach(i -> vos.add(historicProcessInstanceConverter.toVo(i,
                    defCategoryMap.getOrDefault(i.getProcessDefinitionId(), ""))));
            return new BpmPageResult(vos, countSqlQuery.count());
        }
    }

    @Override
    public List<ProcessNodeDetailVO> getProcessInstanceNodeForecast(String processInstanceId,
                                                                    @Nullable String tenantId) {
        ProcessInstanceQuery query = runtimeService.createProcessInstanceQuery()
                .processInstanceId(processInstanceId);
        if (StringUtils.isNotBlank(tenantId)) {
            query.processInstanceTenantId(tenantId);
        }
        ProcessInstance instance = query.singleResult();
        if (Objects.isNull(instance)) {
            throw new WorkflowEngineException(PROCESS_INSTANCE_ID_NOT_EXISTS, processInstanceId);
        }
        List<FlowElement> flowElements = forecastService.performProcessForecasting(processInstanceId, instance);

        List<ProcessNodeDetailVO> resultList = new ArrayList<>(flowElements.size() + 1);
        flowElements.stream().filter(UserTask.class::isInstance).forEach(i -> {
            UserTask userTask = (UserTask) i;
            ProcessNodeDetailVO node = new ProcessNodeDetailVO().setId(userTask.getId())
                    .setName(userTask.getName())
                    .setFormKey(userTask.getFormKey());
            if (userTask.getBehavior() instanceof MultiInstanceActivityBehavior) {
                MultiInstanceActivityBehavior behavior = (MultiInstanceActivityBehavior) userTask.getBehavior();
                node.setNodeMode(Objects.equals(AND_SIGN_EXPRESSION, behavior.getCompletionCondition()) ? AND : OR);
            } else if (userTask.getBehavior() instanceof UserTaskActivityBehavior) {
                node.setNodeMode(BpmnFlowNodeMode.GENERAL);
            }
            resultList.add(node);
        });

        // 处理发起节点
        /*List<FlowElement> startNodes =
                flowElements.stream().filter(StartEvent.class::isInstance).collect(Collectors.toList());
        startNodes.forEach(i -> {
            StartEvent startEvent = (StartEvent) i;
            ProcessNodeDetailVO node = new ProcessNodeDetailVO()
                    .setId(startEvent.getId())
                    .setName(startEvent.getName())
                    .setFormKey(startEvent.getFormKey());
                    //  FIXME by wangli
//                    .setNodeMode(BpmnFlowNodeMode.STARTNODE);
            resultList.add(0, node);
        });*/
        return resultList;
    }
}
