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

import cn.axzo.framework.jackson.utility.JSON;
import cn.axzo.workflow.common.model.request.bpmn.BpmnJsonModel;
import cn.axzo.workflow.common.model.request.bpmn.model.BpmnModelCreateDTO;
import cn.axzo.workflow.common.model.request.bpmn.model.BpmnModelSearchDTO;
import cn.axzo.workflow.common.model.request.bpmn.model.BpmnModelUpdateDTO;
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.model.BpmnModelDetailVO;
import cn.axzo.workflow.core.common.exception.WorkflowEngineException;
import cn.axzo.workflow.core.common.utils.BpmnJsonConverterUtil;
import cn.axzo.workflow.core.common.utils.ContextHolder;
import cn.axzo.workflow.core.service.BpmnProcessDefinitionService;
import cn.axzo.workflow.core.service.BpmnProcessModelService;
import cn.axzo.workflow.core.service.ExtAxReModelService;
import cn.axzo.workflow.core.service.converter.BpmnModelConverter;
import cn.hutool.json.JSONUtil;
import org.flowable.bpmn.converter.BpmnXMLConverter;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.common.engine.impl.db.SuspensionState;
import org.flowable.common.engine.impl.util.io.BytesStreamSource;
import org.flowable.engine.ManagementService;
import org.flowable.engine.RepositoryService;
import org.flowable.engine.repository.Deployment;
import org.flowable.engine.repository.Model;
import org.flowable.engine.repository.ModelQuery;
import org.flowable.engine.repository.NativeModelQuery;
import org.flowable.engine.repository.ProcessDefinition;
import org.flowable.engine.repository.ProcessDefinitionQuery;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import javax.annotation.Nullable;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

import static cn.axzo.workflow.common.constant.BpmnConstants.DISABLED;
import static cn.axzo.workflow.common.constant.BpmnConstants.ENABLED;
import static cn.axzo.workflow.common.constant.MetaInfoConstants.MODEL_DESCRIPTION;
import static cn.axzo.workflow.common.constant.MetaInfoConstants.MODEL_TYPE;
import static cn.axzo.workflow.common.constant.MetaInfoConstants.MODEL_TYPE_PROCESS;
import static cn.axzo.workflow.core.common.code.BpmnModelRespCode.MODEL_ID_NOT_EXISTS;
import static cn.axzo.workflow.core.common.code.BpmnModelRespCode.MODEL_KEY_EXISTS;
import static cn.axzo.workflow.core.common.code.BpmnModelRespCode.MODEL_KEY_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_BPMN_NOT_EXISTS;
import static cn.axzo.workflow.core.common.utils.BpmnNativeQueryUtil.countSql;
import static cn.axzo.workflow.core.common.utils.BpmnNativeQueryUtil.sqlConnectors;

@Service
public class BpmnProcessModelServiceImpl implements BpmnProcessModelService {

    @Resource
    @Lazy
    private RepositoryService repositoryService;
    @Resource
    private BpmnProcessDefinitionService processDefinitionService;
    @Resource
    private ManagementService managementService;
    @Resource
    private ExtAxReModelService extAxReModelService;
    @Resource
    private BpmnModelConverter bpmnModelConverter;
    @Resource
    private String serviceVersion;

    @Override
    public BpmPageResult<BpmnModelDetailVO> getModelPage(BpmnModelSearchDTO dto) {
        String tableName = managementService.getTableName(Model.class);
        StringBuilder baseQuerySql = new StringBuilder("SELECT ");
        if (Objects.nonNull(dto.getStatus())) {
            baseQuerySql.append("a.*");
        } else {
            baseQuerySql.append("*");
        }
        baseQuerySql.append(" FROM ")
                .append(tableName).append(" a ");
        NativeModelQuery query = repositoryService.createNativeModelQuery();
        if (Objects.nonNull(dto.getStatus())) {
            baseQuerySql.append(" INNER JOIN ext_ax_re_model b ON a.ID_ = b.model_id ")
                    .append(sqlConnectors(baseQuerySql))
                    .append(" b.status = #{status}");
            query.parameter("status", dto.getStatus() == 1);
        }
        if (StringUtils.hasLength(dto.getKey())) {
            baseQuerySql.append(sqlConnectors(baseQuerySql))
                    .append(" a.KEY_ = #{key}");
            query.parameter("key", dto.getKey());
        }
        if (StringUtils.hasLength(dto.getName())) {
            baseQuerySql.append(sqlConnectors(baseQuerySql))
                    .append(" a.NAME_ like #{name}");
            query.parameter("name", "%" + dto.getName() + "%");
        }
        if (!CollectionUtils.isEmpty(dto.getCategories())) {
            baseQuerySql.append(sqlConnectors(baseQuerySql))
                    .append(" a.CATEGORY_ in (");
            for (int i = 0; i < dto.getCategories().size(); i++) {
                baseQuerySql.append("#{CATEGORY_").append(i).append("}");
                if (i < dto.getCategories().size() - 1) {
                    baseQuerySql.append(",");
                }
                query.parameter("CATEGORY_" + i, dto.getCategories().get(i));
            }
            baseQuerySql.append(")");
        }
        if (Objects.nonNull(dto.getAgent())) {
            if (dto.getAgent()) {
                baseQuerySql.append(sqlConnectors(baseQuerySql))
                        .append(" TENANT_ID_ != '' ");
                if (!CollectionUtils.isEmpty(dto.getTenantIds())) {
                    baseQuerySql.append(sqlConnectors(baseQuerySql))
                            .append(" TENANT_ID_ in (");
                    for (int i = 0; i < dto.getTenantIds().size(); i++) {
                        baseQuerySql.append("#{tenantId").append(i).append("}");
                        if (i < dto.getTenantIds().size() - 1) {
                            baseQuerySql.append(",");
                        }
                        query.parameter("tenantId" + i, dto.getTenantIds().get(i));
                    }
                    baseQuerySql.append(")");
                }
            } else {
                baseQuerySql.append(sqlConnectors(baseQuerySql))
                        .append(" TENANT_ID_ = '' ");
            }
        }

        baseQuerySql.append(sqlConnectors(baseQuerySql))
                .append(" META_INFO_ like #{metaInfo}");
        query.parameter("metaInfo", "%" + MODEL_TYPE_PROCESS + "%");
        NativeModelQuery dataSqlQuery = query.sql(baseQuerySql.append(" order by LAST_UPDATE_TIME_ desc").toString());
        List<Model> models = dataSqlQuery
                .listPage((dto.getPageNo() - 1) * dto.getPageSize(), dto.getPageSize());
        if (CollectionUtils.isEmpty(models)) {
            return BpmPageResult.empty();
        }
        NativeModelQuery countSqlQuery = query.sql(countSql(baseQuerySql));
        List<BpmnModelDetailVO> vos = bpmnModelConverter.toVos(models);

        // 设置未发布过一次的模型的 jsonValue
        Map<String, BpmnJsonModel> jsonValueMap =
                getUndeployModelJsonValueMap(vos.stream().filter(i -> Objects.isNull(i.getDeploymentId())).filter(i -> Objects.nonNull(i.getEditorSourceId())).map(BpmnModelDetailVO::getId).collect(Collectors.toSet()));
        vos.forEach(i -> {
            if (Objects.nonNull(i.getEditorSourceId())) {
                i.setBpmJson(jsonValueMap.get(i.getId()));
            }
        });
        // 设置至少发布过一次的模型的 jsonValue
        List<ProcessDefinition> processDefinitions =
                processDefinitionService.getProcessDefinitionListByDeploymentIds(vos.stream().map(BpmnModelDetailVO::getDeploymentId).collect(Collectors.toSet()));
        if (CollectionUtils.isEmpty(processDefinitions)) {
            vos.forEach(i -> i.setVersion(0));
        }
        processDefinitions.stream().collect(Collectors.toMap(ProcessDefinition::getDeploymentId, Function.identity(),
                (s, t) -> s)).forEach((k, v) -> vos.forEach(i -> {
            if (Objects.equals(i.getDeploymentId(), k)) {
                i.setVersion(v.getVersion());
                i.setBpmJson(BpmnJsonConverterUtil.convertToJson(repositoryService.getBpmnModel(v.getId())));
            }
        }));

        if (Objects.isNull(dto.getStatus())) {
            extAxReModelService.listByModelIds(vos.stream().map(BpmnModelDetailVO::getId).collect(Collectors.toList()))
                    .forEach(i -> vos.forEach(j -> {
                        if (Objects.equals(i.getModelId(), j.getId())) {
                            j.setStatus(i.getStatus());
                        }
                    }));
        } else {
            vos.forEach(i -> i.setStatus(dto.getStatus()));
        }

        return new BpmPageResult<>(vos, countSqlQuery.count());
    }

    private Map<String, BpmnJsonModel> getUndeployModelJsonValueMap(Set<String> modelIds) {
        if (CollectionUtils.isEmpty(modelIds)) {
            return Collections.emptyMap();
        }

        Map<String, BpmnJsonModel> result = new HashMap<>();
        modelIds.forEach(i -> {
            byte[] modelEditorSource = repositoryService.getModelEditorSource(i);
            if (modelEditorSource != null) {
                BpmnModel bpmnModel =
                        new BpmnXMLConverter().convertToBpmnModel(new BytesStreamSource(modelEditorSource), false,
                                false);
                result.put(i, BpmnJsonConverterUtil.convertToJson(bpmnModel));
            }
        });
        return result;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public String createBpmModel(@Valid BpmnModelCreateDTO dto) {
        ModelQuery modelQuery = repositoryService.createModelQuery()
                .modelTenantId(dto.getTenantId())
                .modelKey(dto.getCategory());

        Model existModel = modelQuery.singleResult();
        if (!ObjectUtils.isEmpty(existModel)) {
            throw new WorkflowEngineException(MODEL_KEY_EXISTS, dto.getCategory());
        }

        Map<String, String> metaInfoMap = new HashMap<>();
        metaInfoMap.put(MODEL_TYPE, MODEL_TYPE_PROCESS);
        metaInfoMap.put(MODEL_DESCRIPTION, dto.getDescription());

        Model model = repositoryService.newModel();
        model.setName(dto.getName());
        model.setMetaInfo(JSONUtil.toJsonStr(metaInfoMap));
        model.setCategory(dto.getCategory());
        model.setKey(StringUtils.hasLength(dto.getKey()) ? dto.getKey() : dto.getCategory());
        model.setTenantId(dto.getTenantId());
        repositoryService.saveModel(model);
        //存储Bpmn协议
        if (Objects.nonNull(dto.getJsonModel())) {
            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);
            BpmnJsonConverterUtil.setCategory(bpmnModel, dto.getCategory());
            byte[] bpmn = BpmnJsonConverterUtil.transformBytes(bpmnModel);
            processDefinitionService.updateProcessDefinition(model.getId(), bpmn);
        }

        extAxReModelService.create(model.getId());
        return model.getId();
    }

    @Override
    public String createBpmModelWithXml(String fileUrl) {

        String bpmnXmlFile = "test.bpmn20.xml";
        Deployment deploy = repositoryService.createDeployment()
                .addClasspathResource(bpmnXmlFile)
                .category("test-category")
                .name("bpmnUsingXml")
                .key("bpmnUsingXml")
                .tenantId("69")
                //                .disableBpmnValidation()
                // 只能临时关闭 BPMN 协议 Schema 校验, 主要用于在 BPMN 中添加自定义的节点
                .disableSchemaValidation()
                .deploy();
        return deploy.getId();
    }

    /**
     * 获取模型
     */
    @Override
    public BpmnModelDetailVO getById(String processModelId, @Nullable String tenantId) {
        Model model = repositoryService.getModel(processModelId);
        if (Objects.isNull(model) || !model.getMetaInfo().contains(MODEL_TYPE_PROCESS)) {
            throw new WorkflowEngineException(MODEL_ID_NOT_EXISTS, processModelId);
        }
        if (Objects.nonNull(tenantId) && !Objects.equals(model.getTenantId(), tenantId)) {
            throw new WorkflowEngineException(MODEL_ID_NOT_EXISTS, tenantId);
        }
        return bpmnModelConverter.toVoParseDesc(model);
    }

    /**
     * 获取模型
     */
    @Override
    public BpmnModelDetailVO getByKey(String processModelKey, @Nullable String tenantId) {
        Model model = repositoryService.createModelQuery().modelKey(processModelKey).singleResult();
        if (Objects.isNull(model) || !model.getMetaInfo().contains(MODEL_TYPE_PROCESS)) {
            throw new WorkflowEngineException(MODEL_KEY_NOT_EXISTS, processModelKey);
        }
        if (Objects.nonNull(tenantId) && !Objects.equals(model.getTenantId(), tenantId)) {
            throw new WorkflowEngineException(MODEL_ID_NOT_EXISTS, tenantId);
        }
        return bpmnModelConverter.toVoParseDesc(model);
    }

    @Override
    public void updateBpmModel(BpmnModelUpdateDTO dto) {
        ModelQuery query = repositoryService.createModelQuery()
                .modelId(dto.getProcessModelId())
                .modelTenantId(dto.getTenantId())
                .modelKey(dto.getCategory());

        Model originModel = query.singleResult();
        if (ObjectUtils.isEmpty(originModel)) {
            throw new WorkflowEngineException(MODEL_ID_NOT_EXISTS, dto.getProcessModelId());
        }

        Map<String, String> metaInfoMap = JSON.parseMap(originModel.getMetaInfo(), String.class, String.class);
        metaInfoMap.put(MODEL_DESCRIPTION, dto.getDescription());

        originModel.setName(dto.getName());
        originModel.setMetaInfo(JSONUtil.toJsonStr(metaInfoMap));
        originModel.setCategory(dto.getCategory());
        originModel.setKey(StringUtils.hasLength(dto.getKey()) ? dto.getKey() : dto.getCategory());
        originModel.setTenantId(dto.getTenantId());
        repositoryService.saveModel(originModel);

        //存储Bpmn协议
        if (Objects.nonNull(dto.getJsonModel())) {
            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);
            BpmnJsonConverterUtil.setCategory(bpmnModel, dto.getCategory());
            byte[] bpmn = BpmnJsonConverterUtil.transformBytes(bpmnModel);
            processDefinitionService.updateProcessDefinition(originModel.getId(), bpmn);
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public String deployBpmModelById(String processModelId, @Nullable String modelTenantId,
                                     BpmnTaskDelegateAssigner operator) {
        ContextHolder.set(operator);
        Model model = this.repositoryService.getModel(processModelId);
        if (Objects.isNull(model)) {
            throw new WorkflowEngineException(MODEL_ID_NOT_EXISTS, processModelId);
        }
        if (StringUtils.hasLength(modelTenantId) && !Objects.equals(model.getTenantId(), modelTenantId)) {
            throw new WorkflowEngineException(MODEL_ID_NOT_EXISTS, processModelId);
        }

        byte[] bpmnBytes = this.repositoryService.getModelEditorSource(model.getId());
        if (bpmnBytes == null) {
            throw new WorkflowEngineException(PROCESS_DEFINITION_BPMN_NOT_EXISTS, model.getId());
        }

        String definitionId = this.processDefinitionService.createProcessDefinition(model, bpmnBytes);
        // 挂起原来的流程定义
        this.updateProcessDefinitionSuspended(model.getDeploymentId());
        ProcessDefinitionQuery definitionQuery =
                repositoryService.createProcessDefinitionQuery().processDefinitionId(definitionId);
        if (StringUtils.hasLength(modelTenantId)) {
            definitionQuery.processDefinitionTenantId(modelTenantId);
        }
        ProcessDefinition definition = definitionQuery.singleResult();
        model.setDeploymentId(definition.getDeploymentId());
        this.repositoryService.saveModel(model);
        extAxReModelService.changeStatus(model.getId(), ENABLED);
        return definition.getId();
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public String deployBpmModelByKey(String processModelKey, String tenantId, BpmnTaskDelegateAssigner operator) {
        ModelQuery modelQuery = repositoryService.createModelQuery()
                .modelKey(processModelKey);
        if (StringUtils.hasLength(tenantId)) {
            modelQuery.modelTenantId(tenantId);
        }
        Model model = modelQuery.singleResult();
        if (Objects.isNull(model)) {
            throw new WorkflowEngineException(MODEL_KEY_NOT_EXISTS, processModelKey);
        }
        return deployBpmModelById(model.getId(), null, operator);
    }

    @Override
    public void deleteBpmModelById(String processModelId, @Nullable String tenantId) {
        // 校验流程模型存在
        Model model = repositoryService.getModel(processModelId);
        if (Objects.isNull(model) || !model.getMetaInfo().contains(MODEL_TYPE_PROCESS)) {
            throw new WorkflowEngineException(MODEL_ID_NOT_EXISTS, processModelId);
        }
        if (StringUtils.hasText(tenantId) && !Objects.equals(model.getTenantId(), tenantId)) {
            throw new WorkflowEngineException(MODEL_ID_NOT_EXISTS, processModelId);
        }
        // 执行删除
        repositoryService.deleteModel(processModelId);
    }

    @Override
    public void deleteBpmModelByKey(String processModelKey, @Nullable String tenantId) {
        ModelQuery modelQuery = repositoryService.createModelQuery().modelKey(processModelKey);
        if (StringUtils.hasLength(tenantId)) {
            modelQuery.modelTenantId(tenantId);
        }
        Model model = modelQuery.singleResult();
        if (Objects.isNull(model) || !model.getMetaInfo().contains(MODEL_TYPE_PROCESS)) {
            throw new WorkflowEngineException(MODEL_NOT_EXISTS);
        }
        repositoryService.deleteModel(model.getId());
    }

    @Override
    @Transactional(rollbackFor = Throwable.class)
    public void unDeployBpmModelById(String processModelId, @Nullable String tenantId,
                                     BpmnTaskDelegateAssigner operator) {
        ContextHolder.set(operator);
        Model model = this.repositoryService.getModel(processModelId);
        if (Objects.isNull(model)) {
            throw new WorkflowEngineException(MODEL_ID_NOT_EXISTS, processModelId);
        }
        if (Objects.nonNull(tenantId) && !Objects.equals(model.getTenantId(), tenantId)) {
            throw new WorkflowEngineException(MODEL_ID_NOT_EXISTS, processModelId);
        }
        updateProcessDefinitionSuspended(model.getDeploymentId());
        extAxReModelService.changeStatus(model.getId(), DISABLED);
    }

    @Override
    @Transactional(rollbackFor = Throwable.class)
    public void changeStatus(String modelId, Integer status, BpmnTaskDelegateAssigner operator) {
        Model model = this.repositoryService.getModel(modelId);
        // 公共模型不会真实的取消部署
        if (!Objects.equals("", model.getTenantId())) {
            if (Objects.equals(ENABLED, status)) {
                deployBpmModelById(modelId, null, operator);
            } else {
                unDeployBpmModelById(modelId, null, operator);
            }
        }
        extAxReModelService.changeStatus(modelId, status);
    }

    @Override
    public List<String> getModelCategoryList() {
        List<Model> list = repositoryService.createModelQuery().deployed().list();
        if (CollectionUtils.isEmpty(list)) {
            return Collections.emptyList();
        }
        return list.stream().map(Model::getCategory).collect(Collectors.toList());
    }

    @Override
    public List<String> getTenantIds() {
        List<Model> list = repositoryService.createModelQuery().list();
        if (CollectionUtils.isEmpty(list)) {
            return Collections.emptyList();
        }
        return list.stream().map(Model::getTenantId).collect(Collectors.toList());
    }

    private void updateProcessDefinitionSuspended(String deploymentId) {
        if (StringUtils.hasLength(deploymentId)) {
            ProcessDefinition oldDefinition =
                    this.processDefinitionService.getProcessDefinitionByDeploymentId(deploymentId);
            if (oldDefinition != null && !oldDefinition.isSuspended()) {
                this.processDefinitionService.updateProcessDefinitionSuspendedState(oldDefinition.getId(),
                        SuspensionState.SUSPENDED.getStateCode());
            }
        }
    }
}
