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

import cn.axzo.workflow.common.model.request.bpmn.definition.BpmnProcessDefinitionUpdateDTO;
import cn.axzo.workflow.common.model.request.bpmn.process.BpmnProcessDefinitionPageDTO;
import cn.axzo.workflow.common.model.response.BpmPageResult;
import cn.axzo.workflow.common.model.response.bpmn.process.BpmnProcessDefinitionVO;
import cn.axzo.workflow.core.common.exception.WorkflowEngineException;
import cn.axzo.workflow.core.common.utils.BpmnJsonConverterUtil;
import cn.axzo.workflow.core.repository.entity.ExtAxReProcDef;
import cn.axzo.workflow.core.service.BpmnProcessDefinitionService;
import cn.axzo.workflow.core.service.ExtAxReProcDefService;
import cn.axzo.workflow.core.service.converter.BpmnProcessDefinitionConverter;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.common.engine.impl.db.SuspensionState;
import org.flowable.engine.RepositoryService;
import org.flowable.engine.repository.Deployment;
import org.flowable.engine.repository.Model;
import org.flowable.engine.repository.ProcessDefinition;
import org.flowable.engine.repository.ProcessDefinitionQuery;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.util.ArrayList;
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.BPMN_FILE_SUFFIX;
import static cn.axzo.workflow.core.common.code.BpmnModelRespCode.BPMN_BYTES_NOT_EXISTS;
import static cn.axzo.workflow.core.common.code.BpmnModelRespCode.MODEL_NOT_EXISTS;
import static cn.axzo.workflow.core.common.code.BpmnProcessDefinitionRespCode.PROCESS_DEFINITION_DEPLOY_ID_NOT_EXISTS;
import static cn.axzo.workflow.core.common.code.BpmnProcessDefinitionRespCode.PROCESS_DEFINITION_HAS_DIRTY_DATA;
import static cn.axzo.workflow.core.common.code.BpmnProcessDefinitionRespCode.PROCESS_DEFINITION_ID_NOT_EXISTS;
import static cn.axzo.workflow.core.common.code.BpmnProcessDefinitionRespCode.PROCESS_DEFINITION_KEY_NOT_EXISTS;
import static cn.axzo.workflow.core.common.code.BpmnProcessDefinitionRespCode.PROCESS_DEFINITION_KEY_NOT_MATCH;
import static cn.axzo.workflow.core.common.code.BpmnProcessDefinitionRespCode.PROCESS_DEFINITION_NAME_NOT_MATCH;
import static cn.axzo.workflow.core.common.code.BpmnProcessDefinitionRespCode.PROCESS_DEFINITION_RESULT_TOO_MANY;
import static cn.axzo.workflow.core.common.utils.BpmnCollectionUtils.addIfNotNull;
import static cn.axzo.workflow.core.common.utils.BpmnCollectionUtils.convertMap;
import static java.util.Collections.emptyList;

@Service
@Slf4j
public class BpmnProcessDefinitionServiceImpl implements BpmnProcessDefinitionService {

    @Resource
    private RepositoryService repositoryService;
    @Resource
    private ExtAxReProcDefService extAxReProcDefService;
    @Resource
    private BpmnProcessDefinitionConverter processDefinitionConverter;
    @Resource
    private String serviceVersion;

    @Override
    public String createProcessDefinition(Model model, byte[] bpmnBytes) {
        if (Objects.isNull(model)) {
            throw new WorkflowEngineException(MODEL_NOT_EXISTS);
        }
        if (Objects.isNull(bpmnBytes)) {
            throw new WorkflowEngineException(BPMN_BYTES_NOT_EXISTS);
        }

        Deployment deploy = repositoryService.createDeployment()
                .key(model.getKey())
                .name(model.getName())
                .category(model.getCategory())
                .tenantId(model.getTenantId())
                .addBytes(model.getKey() + BPMN_FILE_SUFFIX, bpmnBytes)
                // 关闭 BPMN 文件内容校验
                .disableBpmnValidation()
                .disableSchemaValidation()
                .deploy();

        // 设置 ProcessDefinition 的 category 分类
        ProcessDefinition definition = repositoryService.createProcessDefinitionQuery()
                .processDefinitionTenantId(deploy.getTenantId())
                .deploymentId(deploy.getId()).singleResult();
        repositoryService.setProcessDefinitionCategory(definition.getId(), model.getCategory());

        // 注意 1，ProcessDefinition 的 key 和 name 是通过 BPMN 中的 <bpmn2:process /> 的 id 和 name 决定
        // 注意 2，目前该项目的设计上，需要保证 Model、Deployment、ProcessDefinition 使用相同的 key，保证关联性。
        //          否则，会导致 ProcessDefinition 的分页无法查询到。
        if (!Objects.equals(model.getKey(), definition.getKey())) {
            throw new WorkflowEngineException(PROCESS_DEFINITION_KEY_NOT_MATCH);
        }
        if (!Objects.equals(model.getName(), definition.getName())) {
            throw new WorkflowEngineException(PROCESS_DEFINITION_NAME_NOT_MATCH, model.getName(), definition.getName());
        }

        return definition.getId();
    }

    @Override
    public void updateProcessDefinition(String processModelId, byte[] bpmnBytes) {
        if (bpmnBytes != null && bpmnBytes.length != 0) {
            this.repositoryService.addModelEditorSource(processModelId, bpmnBytes);
        }
    }

    @Override
    public void updateProcessDefinition(String processModelId, String bpmnXml) {
        if (StringUtils.hasLength(bpmnXml)) {
            updateProcessDefinition(processModelId, bpmnXml.getBytes());
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateProcessDefinition(BpmnProcessDefinitionUpdateDTO dto) {
        Model model = repositoryService.getModel(dto.getProcessModelId());
        if (Objects.isNull(model)) {
            throw new WorkflowEngineException(MODEL_NOT_EXISTS);
        }
        BpmnModel bpmnModel = BpmnJsonConverterUtil.convertToBpmn(dto.getJsonModel().getNode(),
                StringUtils.hasLength(dto.getKey()) ? dto.getKey() : dto.getCategory(),
                dto.getName(),
                dto.getDescription(),
                dto.getJsonModel().getNoticeConf(),
                dto.getJsonModel().getButtonConf(),
                dto.getJsonModel().getFieldConf(),
                serviceVersion);
        byte[] bpmn = BpmnJsonConverterUtil.transformBytes(bpmnModel);
        updateProcessDefinition(model.getId(), bpmn);
    }

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

    @Override
    public BpmPageResult<BpmnProcessDefinitionVO> getProcessDefinitionPage(BpmnProcessDefinitionPageDTO dto) {
        ProcessDefinitionQuery definitionQuery = repositoryService.createProcessDefinitionQuery()
                .processDefinitionKey(dto.getKey());
        if (Objects.nonNull(dto.getTenantId())) {
            definitionQuery.processDefinitionTenantId(dto.getTenantId());
        }
        // 执行查询
        List<ProcessDefinition> processDefinitions = definitionQuery.orderByProcessDefinitionVersion()
                .desc()
                .listPage((dto.getPageNo() - 1) * dto.getPageSize(), dto.getPageSize());

        if (CollUtil.isEmpty(processDefinitions)) {
            return BpmPageResult.empty();
        }
        List<ExtAxReProcDef> procDefs =
                extAxReProcDefService.listByProcDefIds(processDefinitions.stream().map(ProcessDefinition::getId).collect(Collectors.toList()));
        // 拼接结果
        return new BpmPageResult<>(processDefinitionConverter.toVos(processDefinitions, procDefs),
                definitionQuery.count());
    }

    @Override
    public BpmnProcessDefinitionVO getProcessDefinition(String id) {
        ProcessDefinition processDefinition = repositoryService.getProcessDefinition(id);
        if (Objects.isNull(processDefinition)) {
            throw new WorkflowEngineException(PROCESS_DEFINITION_ID_NOT_EXISTS, id);
        }
        BpmnProcessDefinitionVO vo = processDefinitionConverter.toVo(processDefinition);
        vo.setJsonModel(BpmnJsonConverterUtil.convertToJson(repositoryService.getBpmnModel(id)));
        return vo;
    }

    @Override
    public Boolean hasActiveProcessDefinition(String deploymentId) {
        return repositoryService.createProcessDefinitionQuery().active().deploymentId(deploymentId).count() > 0;
    }

    @Override
    public ProcessDefinition getProcessDefinitionByDeploymentId(String deploymentId) {
        ProcessDefinition processDefinition =
                repositoryService.createProcessDefinitionQuery().deploymentId(deploymentId)
                        .singleResult();
        if (Objects.isNull(processDefinition)) {
            throw new WorkflowEngineException(PROCESS_DEFINITION_DEPLOY_ID_NOT_EXISTS, deploymentId);
        }
        return processDefinition;
    }

    @Override
    public BpmnProcessDefinitionVO getActiveProcessDefinitionByKey(String key, String tenantId) {
        List<ProcessDefinition> list = repositoryService.createProcessDefinitionQuery()
                .processDefinitionKey(key)
                .active()
                .list();

        if (CollectionUtils.isEmpty(list)) {
            throw new WorkflowEngineException(PROCESS_DEFINITION_KEY_NOT_EXISTS, key);
        }

        ProcessDefinition processDefinition;
        List<ProcessDefinition> filterDefinitions = list.stream()
                .filter(i -> Objects.equals(i.getDerivedVersion(), 0))
                .filter(i -> Objects.equals(tenantId, i.getTenantId()) || Objects.equals("", i.getTenantId()))
                .collect(Collectors.toList());
        if (CollectionUtils.isEmpty(filterDefinitions)) {
            throw new WorkflowEngineException(PROCESS_DEFINITION_KEY_NOT_EXISTS, key);
        } else if (filterDefinitions.size() == 1) {
            processDefinition = filterDefinitions.get(0);
        } else {
            processDefinition =
                    filterDefinitions.stream().filter(i -> Objects.equals(tenantId, i.getTenantId())).findFirst()
                            .orElseThrow(() -> new WorkflowEngineException(PROCESS_DEFINITION_KEY_NOT_EXISTS, key));
        }

        return processDefinitionConverter.toVo(processDefinition);
    }

    @Override
    public String getActiveProcessDefinitionId(String tenantId, String category) {
        List<ProcessDefinition> list = repositoryService.createProcessDefinitionQuery()
                .processDefinitionTenantId(tenantId)
                .processDefinitionCategory(category)
                .latestVersion()
                .list();
        if (!CollectionUtils.isEmpty(list) && list.size() != 1) {
            throw new WorkflowEngineException(PROCESS_DEFINITION_RESULT_TOO_MANY, tenantId, category);
        } else if (CollectionUtils.isEmpty(list)) {
            throw new WorkflowEngineException(PROCESS_DEFINITION_HAS_DIRTY_DATA, category);
        } else {
            return list.get(0).getId();
        }
    }

    @Override
    public List<ProcessDefinition> getProcessDefinitionListByDeploymentIds(Set<String> deploymentIds) {
        if (CollUtil.isEmpty(deploymentIds)) {
            return emptyList();
        }
        return repositoryService.createProcessDefinitionQuery().deploymentIds(deploymentIds).list();
    }

    @Override
    public void delete(String deploymentId, Boolean cascade) {
        if (Objects.isNull(cascade)) {
            repositoryService.deleteDeployment(deploymentId);
        } else {
            repositoryService.deleteDeployment(deploymentId, cascade);
        }
    }

    private Map<String, Deployment> getDeploymentMap(Set<String> ids) {
        return convertMap(getDeployments(ids), Deployment::getId);
    }

    private List<Deployment> getDeployments(Set<String> ids) {
        if (CollUtil.isEmpty(ids)) {
            return emptyList();
        }
        List<Deployment> list = new ArrayList<>(ids.size());
        for (String id : ids) {
            addIfNotNull(list, getDeployment(id));
        }
        return list;
    }

    private Deployment getDeployment(String id) {
        if (StrUtil.isEmpty(id)) {
            return null;
        }
        return repositoryService.createDeploymentQuery().deploymentId(id).singleResult();
    }
}
