package cn.stylefeng.workflow.instance.core.biz;

import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.stylefeng.roses.kernel.rule.exception.base.ServiceException;
import cn.stylefeng.team.form.api.FormModelServiceApi;
import cn.stylefeng.team.project.api.FormDataServiceApi;
import cn.stylefeng.team.project.api.pojo.request.BusinessSaveRequest;
import cn.stylefeng.workflow.api.DefinitionApi;
import cn.stylefeng.workflow.api.NodeApi;
import cn.stylefeng.workflow.api.entity.design.Definition;
import cn.stylefeng.workflow.api.entity.design.Node;
import cn.stylefeng.workflow.api.entity.instance.RuntimeInstance;
import cn.stylefeng.workflow.api.entity.instance.RuntimeNode;
import cn.stylefeng.workflow.api.enums.InstanceStatusEnum;
import cn.stylefeng.workflow.api.enums.ModelEventTypeEnum;
import cn.stylefeng.workflow.api.enums.NodeStatusEnum;
import cn.stylefeng.workflow.api.enums.TaskTypeEnum;
import cn.stylefeng.workflow.api.exception.VariableException;
import cn.stylefeng.workflow.api.pojo.InstanceContext;
import cn.stylefeng.workflow.api.pojo.InstanceVariable;
import cn.stylefeng.workflow.event.api.EventPublishApi;
import cn.stylefeng.workflow.instance.api.WorkflowControlApi;
import cn.stylefeng.workflow.instance.api.WorkflowStatusApi;
import cn.stylefeng.workflow.instance.api.exceptions.InstanceExceptionEnum;
import cn.stylefeng.workflow.instance.api.factory.RuntimeInstanceFactory;
import cn.stylefeng.workflow.instance.api.factory.RuntimeNodeFactory;
import cn.stylefeng.workflow.instance.api.pojo.request.*;
import cn.stylefeng.workflow.instance.core.flow.SyncWorkflowEngine;
import cn.stylefeng.workflow.instance.core.path.NodePathCalc;
import cn.stylefeng.workflow.instance.core.timer.WorkflowTimerExec;
import cn.stylefeng.workflow.instance.modular.draft.service.FlowEngineDraftService;
import cn.stylefeng.workflow.instance.modular.manage.service.RuntimeInstanceService;
import cn.stylefeng.workflow.instance.modular.manage.service.RuntimeNodeService;
import cn.stylefeng.workflow.instance.modular.task.service.RuTaskService;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Date;
import java.util.List;
import java.util.Map;

@Service
public class WorkflowControlService implements WorkflowControlApi {

    @Resource
    private RuntimeInstanceService runtimeInstanceService;

    @Resource
    private DefinitionApi definitionApi;

    @Resource
    private WorkflowStatusApi workflowStatusApi;

    @Resource
    private SyncWorkflowEngine syncWorkflowEngine;

    @Resource
    private EventPublishApi eventPublishApi;

    @Resource
    private RuntimeNodeService runtimeNodeService;

    @Resource
    private NodeApi nodeApi;

    @Resource
    private FormModelServiceApi formModelServiceApi;

    @Resource
    private FormDataServiceApi formDataServiceApi;

    @Resource
    private FlowEngineDraftService flowEngineDraftService;

    @Resource
    private RuTaskService ruTaskService;

    @Resource
    private WorkflowTimerExec workflowTimerExec;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public RuntimeInstance start(WorkflowStartRequest workflowStartRequest) {

        if (ObjectUtil.isEmpty(workflowStartRequest.getDefinitionId())) {
            throw new ServiceException(InstanceExceptionEnum.DEFINITION_ID_IS_NULL);
        }

        if (ObjectUtil.isEmpty(workflowStartRequest.getWorkflowStartType())) {
            throw new ServiceException(InstanceExceptionEnum.START_TYPE_IS_NULL);
        }

        Definition definitionInfo = definitionApi.getDefinitionInfo(workflowStartRequest.getDefinitionId());


        String primaryKeyId = null;
        if (StrUtil.isNotBlank(workflowStartRequest.getPrimaryKeyId())) {
            primaryKeyId = workflowStartRequest.getPrimaryKeyId();
        } else {
            primaryKeyId = this.saveFormData(workflowStartRequest);
        }

        RuntimeInstance ruInstance = RuntimeInstanceFactory.createRuInstance(workflowStartRequest, definitionInfo);
        ruInstance.setBizPrimaryKey(primaryKeyId);
        this.runtimeInstanceService.save(ruInstance);

        InstanceContext instanceContext = workflowStatusApi.buildInstanceContext(workflowStartRequest.getDefinitionId(), ruInstance,
                workflowStartRequest.getInstanceVariable());

        eventPublishApi.publishModelEvent(ModelEventTypeEnum.START, instanceContext);

        NodePathCalc nodePathCalc = new NodePathCalc(instanceContext);
        Node startNode = nodePathCalc.getStartNode();
        syncWorkflowEngine.begin(null, startNode, instanceContext);

        if (workflowStartRequest.getDraftId() != null) {
            flowEngineDraftService.removeById(workflowStartRequest.getDraftId());
        }

        workflowTimerExec.startTimerOnInstanceStart(definitionInfo, instanceContext);

        if (ObjectUtil.isEmpty(workflowStartRequest.getCopyToUserIdList())) {
            return ruInstance;
        }
        if (ObjectUtil.isEmpty(instanceContext.getRuntimeNode())) {
            return ruInstance;
        }
        for (Long userId : workflowStartRequest.getCopyToUserIdList()) {
            ruTaskService.createUserTask(userId, TaskTypeEnum.COPY, instanceContext.getRuntimeNode(), instanceContext);
        }

        return ruInstance;
    }

    @Override
    public void finishNode(WorkflowFinishNodeRequest workflowFinishNodeRequest) {

        if (ObjectUtil.isEmpty(workflowFinishNodeRequest.getInstanceId())) {
            throw new ServiceException(InstanceExceptionEnum.INSTANCE_ID_IS_NULL);
        }
        if (ObjectUtil.isEmpty(workflowFinishNodeRequest.getRuntimeNodeIdList())) {
            throw new ServiceException(InstanceExceptionEnum.RU_NODE_ID_IS_NULL);
        }

        InstanceContext instanceContext = workflowStatusApi.buildInstanceContext(workflowFinishNodeRequest.getInstanceId());

        for (Long runtimeNodeId : workflowFinishNodeRequest.getRuntimeNodeIdList()) {

            Integer instanceStatus = instanceContext.getRuntimeInstance().getInstanceStatus();
            if (!InstanceStatusEnum.PROCESSING.getStatus().equals(instanceStatus)) {
                InstanceStatusEnum instanceStatusEnum = InstanceStatusEnum.FINISH.parseToEnum(instanceStatus.toString());
                throw new VariableException(InstanceExceptionEnum.INSTANCE_STATUS_ERROR,
                        instanceStatusEnum == null ? instanceStatus : instanceStatusEnum.getName());
            }

            RuntimeNode runtimeNode = runtimeNodeService.getRuntimeNodeById(runtimeNodeId);

            if (runtimeNode.getNodeStatus().equals(NodeStatusEnum.FINISH.getStatus())) {
                continue;
            }

            runtimeNode.setNodeStatus(NodeStatusEnum.FINISH.getStatus());
            this.runtimeNodeService.updateById(runtimeNode);

            NodePathCalc nodePathCalc = new NodePathCalc(instanceContext);
            List<Node> nextNodeList = nodePathCalc.calcNextNodeList(runtimeNode.getNodeId());

            for (Node nextNode : nextNodeList) {
                syncWorkflowEngine.begin(runtimeNode, nextNode, instanceContext);
            }
        }

    }

    @Override
    public void suspend(WorkflowChangeStatusRequest workflowSuspendRequest) {

        if (ObjectUtil.isEmpty(workflowSuspendRequest.getInstanceId())) {
            throw new ServiceException(InstanceExceptionEnum.INSTANCE_ID_IS_NULL);
        }

        RuntimeInstance runtimeInstance = runtimeInstanceService.getInstanceInfById(workflowSuspendRequest.getInstanceId());

        if (!runtimeInstance.getInstanceStatus().equals(InstanceStatusEnum.PROCESSING.getStatus())) {
            InstanceStatusEnum instanceStatusEnum = InstanceStatusEnum.FINISH.parseToEnum(runtimeInstance.getInstanceStatus().toString());
            throw new VariableException(InstanceExceptionEnum.SUSPEND_ERROR_ERROR,
                    instanceStatusEnum == null ? runtimeInstance.getInstanceStatus() : instanceStatusEnum.getName());
        }

        runtimeInstance.setInstanceStatus(InstanceStatusEnum.SUSPEND.getStatus());
        this.runtimeInstanceService.updateById(runtimeInstance);
    }

    @Override
    public void active(WorkflowChangeStatusRequest workflowSuspendRequest) {

        if (ObjectUtil.isEmpty(workflowSuspendRequest.getInstanceId())) {
            throw new ServiceException(InstanceExceptionEnum.INSTANCE_ID_IS_NULL);
        }

        RuntimeInstance runtimeInstance = runtimeInstanceService.getInstanceInfById(workflowSuspendRequest.getInstanceId());

        if (!runtimeInstance.getInstanceStatus().equals(InstanceStatusEnum.SUSPEND.getStatus())) {
            InstanceStatusEnum instanceStatusEnum = InstanceStatusEnum.FINISH.parseToEnum(runtimeInstance.getInstanceStatus().toString());
            throw new VariableException(InstanceExceptionEnum.ACTIVE_ERROR_ERROR,
                    instanceStatusEnum == null ? runtimeInstance.getInstanceStatus() : instanceStatusEnum.getName());
        }

        runtimeInstance.setInstanceStatus(InstanceStatusEnum.PROCESSING.getStatus());
        this.runtimeInstanceService.updateById(runtimeInstance);


    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void invalid(WorkflowChangeStatusRequest workflowSuspendRequest) {

        if (ObjectUtil.isEmpty(workflowSuspendRequest.getInstanceId())) {
            throw new ServiceException(InstanceExceptionEnum.INSTANCE_ID_IS_NULL);
        }

        RuntimeInstance runtimeInstance = runtimeInstanceService.getInstanceInfById(workflowSuspendRequest.getInstanceId());

        if (runtimeInstance.getInstanceStatus().equals(InstanceStatusEnum.FINISH.getStatus())) {
            throw new VariableException(InstanceExceptionEnum.INVALID_ERROR_ERROR);
        }

        runtimeInstance.setInstanceStatus(InstanceStatusEnum.INVALID.getStatus());
        runtimeInstance.setCompletionTime(new Date());
        this.runtimeInstanceService.updateById(runtimeInstance);

        runtimeNodeService.closeInstanceRuntimeNodes(workflowSuspendRequest.getInstanceId());

        ruTaskService.deleteInstanceProcessingTask(workflowSuspendRequest.getInstanceId());

    }

    @Override
    public void turn(WorkflowTurnRequest workflowTurnRequest) {

        Node nodeInfo = nodeApi.getNodeInfo(workflowTurnRequest.getReNodeId());

        LambdaUpdateWrapper<RuntimeNode> queryRuntimeNode = new LambdaUpdateWrapper<>();
        queryRuntimeNode.eq(RuntimeNode::getInstanceId, workflowTurnRequest.getInstanceId());
        queryRuntimeNode.eq(RuntimeNode::getNodeId, workflowTurnRequest.getReNodeId());
        RuntimeNode jumpToRuntimeNode = runtimeNodeService.getOne(queryRuntimeNode);

        if (jumpToRuntimeNode != null) {
            runtimeNodeService.cleanRuntimeNodeAndEdges(workflowTurnRequest.getInstanceId(), jumpToRuntimeNode.getRuNodeId());

            if (!workflowTurnRequest.getContinueFlow()) {
                jumpToRuntimeNode.setNodeStatus(NodeStatusEnum.RUNNING.getStatus());
                runtimeNodeService.updateRuntimeNode(jumpToRuntimeNode);
            } else {

                runtimeNodeService.removeById(jumpToRuntimeNode.getRuNodeId());
            }
        }

        else {

            this.updateRuntimeNodeStatus(workflowTurnRequest.getInstanceId(), NodeStatusEnum.SKIP);

            if (!workflowTurnRequest.getContinueFlow()) {
                jumpToRuntimeNode = RuntimeNodeFactory.initRuntimeNode(nodeInfo, workflowTurnRequest.getInstanceId());
                jumpToRuntimeNode.setNodeStatus(NodeStatusEnum.RUNNING.getStatus());
                this.runtimeNodeService.save(jumpToRuntimeNode);
            }

        }

        if (workflowTurnRequest.getContinueFlow()) {

            InstanceContext instanceContext = workflowTurnRequest.getInstanceContext();
            if (instanceContext == null) {
                instanceContext = workflowStatusApi.buildInstanceContext(workflowTurnRequest.getInstanceId());
            }

            syncWorkflowEngine.begin(null, nodeInfo, instanceContext);
        }
    }

    @Override
    public void reject(WorkflowChangeStatusRequest workflowSuspendRequest) {

        if (ObjectUtil.isEmpty(workflowSuspendRequest.getInstanceId())) {
            throw new ServiceException(InstanceExceptionEnum.INSTANCE_ID_IS_NULL);
        }

        RuntimeInstance runtimeInstance = runtimeInstanceService.getInstanceInfById(workflowSuspendRequest.getInstanceId());

        if (runtimeInstance.getInstanceStatus().equals(InstanceStatusEnum.FINISH.getStatus())) {
            throw new VariableException(InstanceExceptionEnum.FINISH_ERROR);
        }

        runtimeInstance.setInstanceStatus(InstanceStatusEnum.REJECT.getStatus());
        runtimeInstance.setCompletionTime(new Date());
        this.runtimeInstanceService.updateById(runtimeInstance);

        this.updateRuntimeNodeStatus(workflowSuspendRequest.getInstanceId(), NodeStatusEnum.REJECT);

    }

    @Override
    public void updateRuntimeNodeStatus(Long instanceId, NodeStatusEnum nodeStatusEnum) {
        LambdaUpdateWrapper<RuntimeNode> lambdaUpdateWrapper = new LambdaUpdateWrapper<>();
        lambdaUpdateWrapper.eq(RuntimeNode::getInstanceId, instanceId);
        lambdaUpdateWrapper.eq(RuntimeNode::getNodeStatus, NodeStatusEnum.RUNNING.getStatus());
        lambdaUpdateWrapper.set(RuntimeNode::getNodeStatus, nodeStatusEnum.getStatus());
        runtimeNodeService.update(lambdaUpdateWrapper);
    }

    @Override
    public void saveDraft(WorkflowSaveDraftRequest workflowSaveDraftRequest) {
        flowEngineDraftService.add(workflowSaveDraftRequest);
    }

    private String saveFormData(WorkflowStartRequest workflowStartRequest) {

        InstanceVariable instanceVariable = workflowStartRequest.getInstanceVariable();
        if (instanceVariable == null) {
            return null;
        }

        Map<String, Map<String, Object>> formData = instanceVariable.getFormData();
        if (formData == null) {
            return null;
        }

        String formDefinitionId = formData.keySet().iterator().next();
        Long formBusinessId = formModelServiceApi.getFormBusinessId(Convert.toLong(formDefinitionId));
        if (formBusinessId == null) {
            return null;
        }

        BusinessSaveRequest businessSaveRequest = new BusinessSaveRequest();
        businessSaveRequest.setBusinessId(formBusinessId);
        businessSaveRequest.setFormDetail(formData);
        return formDataServiceApi.save(businessSaveRequest);
    }

}
