package cn.axzo.workflow.core.engine.cmd;

import cn.axzo.workflow.common.exception.WorkflowEngineException;
import cn.axzo.workflow.common.model.request.bpmn.task.AttachmentDTO;
import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskDelegateAssigner;
import cn.axzo.workflow.core.common.utils.BpmnModelUtils;
import cn.axzo.workflow.core.common.utils.SpringContextUtils;
import cn.axzo.workflow.core.service.support.FlowNodeForecastService;
import com.alibaba.fastjson.JSON;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.flowable.bpmn.model.FlowElement;
import org.flowable.bpmn.model.FlowNode;
import org.flowable.bpmn.model.Process;
import org.flowable.bpmn.model.UserTask;
import org.flowable.common.engine.impl.identity.Authentication;
import org.flowable.common.engine.impl.interceptor.CommandContext;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.impl.cfg.ProcessEngineConfigurationImpl;
import org.flowable.engine.impl.util.CommandContextUtil;
import org.flowable.engine.impl.util.ProcessDefinitionUtil;
import org.flowable.task.api.history.HistoricTaskInstance;
import org.flowable.task.service.TaskService;
import org.flowable.task.service.impl.persistence.entity.TaskEntity;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import java.io.Serializable;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;

import static cn.axzo.workflow.common.code.BpmnTaskRespCode.BACK_NODE_CANNOT_REACHABLE;
import static cn.axzo.workflow.common.code.BpmnTaskRespCode.BACK_TARGET_ACTIVITY_NOT_EXISTS;
import static cn.axzo.workflow.common.code.BpmnTaskRespCode.TASK_COMPLETE_FAIL_NOT_EXISTS;
import static cn.axzo.workflow.common.constant.BpmnConstants.COMMENT_TYPE_ADVICE;
import static cn.axzo.workflow.common.constant.BpmnConstants.COMMENT_TYPE_OPERATION_DESC;
import static cn.axzo.workflow.common.constant.BpmnConstants.INTERNAL_TASK_RELATION_ASSIGNEE_LIST_INFO;
import static cn.axzo.workflow.common.constant.BpmnConstants.TASK_COMPLETE_OPERATION_TYPE;
import static cn.axzo.workflow.common.enums.BpmnProcessInstanceResultEnum.BACKED;
import static cn.axzo.workflow.core.engine.cmd.helper.CustomTaskHelper.addComment;
import static cn.axzo.workflow.core.engine.cmd.helper.CustomTaskHelper.batchAddAttachment;
import static cn.axzo.workflow.core.engine.cmd.helper.CustomTaskHelper.validTask;

/**
 * 回退命令
 */
@Slf4j
public class CustomBackTaskCmd extends AbstractCommand<Void> implements Serializable {
    private static final long serialVersionUID = -1241290344311892346L;


    private final List<String> targetTaskIds;
    private final String advice;
    private final String operationDesc;
    private final List<AttachmentDTO> attachmentList;
    private final BpmnTaskDelegateAssigner operator;
    private final String processInstanceId;
    private final String currentActivityId;
    private final String toActivityId;
    private final boolean validateApprover;

    private static final String OPERATION_DESC = "回退至";

    public CustomBackTaskCmd(CustomBackParamsDto customBackParamsDto) {
        if (customBackParamsDto == null) {
            throw new NullPointerException("customBackParamsDto is null");
        }
        if (customBackParamsDto.getOperator() == null) {
            throw new NullPointerException("operator is null");
        }
        this.targetTaskIds = customBackParamsDto.getTargetTaskIds();
        this.advice = customBackParamsDto.getAdvice();
        this.operationDesc = customBackParamsDto.getOperationDesc();
        this.attachmentList = customBackParamsDto.getAttachmentList();
        this.operator = customBackParamsDto.getOperator();
        this.processInstanceId = customBackParamsDto.getProcessInstanceId();
        this.currentActivityId = customBackParamsDto.getCurrentActivityId();
        this.toActivityId = customBackParamsDto.getToActivityId();
        this.validateApprover = customBackParamsDto.isValidateApprover();
    }

    @Override
    public String paramToJsonString() {
        Map<String, Object> params = new HashMap<>();
        params.put("targetTaskIds", JSON.toJSONString(this.targetTaskIds));
        params.put("advice", advice);
        params.put("operationDesc", OPERATION_DESC);
        params.put("attachmentList", JSON.toJSONString(attachmentList));
        params.put("operator", JSON.toJSONString(operator));
        params.put("toActivityId", JSON.toJSONString(this.toActivityId));
        params.put("processInstanceId", JSON.toJSONString(this.processInstanceId));
        params.put("validateApprover", validateApprover);
        return JSON.toJSONString(params);
    }

    @Override
    public Void execute(CommandContext commandContext) {
        ProcessEngineConfigurationImpl processEngineConfiguration =
                CommandContextUtil.getProcessEngineConfiguration(commandContext);
        List<HistoricTaskInstance> taskList = processEngineConfiguration.getHistoryService()
                .createHistoricTaskInstanceQuery()
                .taskDefinitionKey(currentActivityId)
                .processInstanceId(processInstanceId)
                .list();
        List<HistoricTaskInstance> valueableTaskList = taskList.stream()
                .filter(hi -> {
                    if (hi.getEndTime() != null) {
                        return false;
                    }
                    if (!CollectionUtils.isEmpty(targetTaskIds)) {
                        return targetTaskIds.contains(hi.getId());
                    }
                    return true;
                })
                .collect(Collectors.toList());
        if (CollectionUtils.isEmpty(valueableTaskList)) {
            throw new WorkflowEngineException(TASK_COMPLETE_FAIL_NOT_EXISTS);
        }
        TaskService taskService = processEngineConfiguration.getTaskServiceConfiguration().getTaskService();
        if (validateApprover) {
            valueableTaskList.forEach(hi -> validTask(hi, taskService.getTask(hi.getId()), operator, null));
        }
        String processDefinitionId = valueableTaskList.get(0).getProcessDefinitionId();
        Process process = ProcessDefinitionUtil.getProcess(processDefinitionId);
        FlowElement targetFlowElement = process.getFlowElement(toActivityId, true);
        if (Objects.isNull(targetFlowElement)) {
            throw new WorkflowEngineException(BACK_TARGET_ACTIVITY_NOT_EXISTS, toActivityId);
        }
        FlowElement sourceFlowElement = process.getFlowElement(currentActivityId, true);
        // 退回节点到当前节点不可达到，不允许退回
        if (!BpmnModelUtils.isReachable(process, (FlowNode) targetFlowElement, (FlowNode) sourceFlowElement)) {
            throw new WorkflowEngineException(BACK_NODE_CANNOT_REACHABLE, toActivityId);
        }
        RuntimeService runtimeService = processEngineConfiguration.getRuntimeService();
        valueableTaskList.forEach(th -> {
            TaskEntity task = taskService.getTask(th.getId());
            Authentication.setAuthenticatedUserId(operator.buildAssigneeId());
            batchAddAttachment(commandContext, processInstanceId, task, attachmentList, operator);
            addComment(commandContext, task, COMMENT_TYPE_ADVICE, advice);
            addComment(commandContext, task, COMMENT_TYPE_OPERATION_DESC, StringUtils.hasText(operationDesc) ? operationDesc : OPERATION_DESC + targetFlowElement.getName());
            Authentication.setAuthenticatedUserId(null);
            task.setTransientVariable(TASK_COMPLETE_OPERATION_TYPE + task.getId(), BACKED.getStatus());
        });
        // 移除回退到的指定节点的变量，让 EngineExecutionStartListener 重新计算该节点的人
        List<String> removeActivityIds = collectorThoughtFlowElement(processInstanceId, toActivityId, currentActivityId, true);
        runtimeService.removeVariables(processInstanceId, removeActivityIds.stream().map(i -> INTERNAL_TASK_RELATION_ASSIGNEE_LIST_INFO + i).collect(Collectors.toList()));
        runtimeService.createChangeActivityStateBuilder()
                .processInstanceId(processInstanceId)
                .moveActivityIdsToSingleActivityId(Collections.singletonList(currentActivityId), toActivityId)
                .changeState();
        return null;
    }

    private List<String> collectorThoughtFlowElement(String processInstanceId, String startActivityId, String currentActivityId, Boolean containsStart) {
        FlowNodeForecastService forecastService = SpringContextUtils.getBean(FlowNodeForecastService.class);
        List<FlowElement> flowElements = forecastService.performProcessForecasting(processInstanceId, null, startActivityId, containsStart);
        AtomicBoolean flag = new AtomicBoolean(true);
        return flowElements.stream()
                .map(FlowElement::getId)
                .filter(id -> StringUtils.startsWithIgnoreCase(id, "node_"))
                .filter(id -> {
                    if (Objects.equals(id, currentActivityId) && flag.get()) {
                        flag.set(false);
                        return true;
                    }
                    return flag.get();
                }).collect(Collectors.toList());
    }

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @Builder
    public static final class CustomBackParamsDto {
        private List<String> targetTaskIds;
        private String advice;
        private String operationDesc;
        private List<AttachmentDTO> attachmentList;
        private BpmnTaskDelegateAssigner operator;
        private String processInstanceId;
        private String currentActivityId;
        private String toActivityId;
        private boolean validateApprover;
    }

}
