package com.purui.prims.headless.server.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.google.common.collect.Lists;
import com.purui.prims.auth.api.authentication.pojo.User;
import com.purui.prims.common.pojo.Constants;
import com.purui.prims.common.pojo.DataEvent;
import com.purui.prims.common.pojo.DataItem;
import com.purui.prims.common.pojo.ModelRela;
import com.purui.prims.common.pojo.enums.EventType;
import com.purui.prims.common.pojo.enums.StatusEnum;
import com.purui.prims.common.pojo.enums.TypeEnums;
import com.purui.prims.common.pojo.exception.InvalidArgumentException;
import com.purui.prims.headless.api.pojo.request.DimensionReq;
import com.purui.prims.headless.api.pojo.request.MetaBatchReq;
import com.purui.prims.headless.api.pojo.request.PageDimensionReq;
import com.purui.prims.headless.api.pojo.response.DataSetResp;
import com.purui.prims.headless.api.pojo.response.DimensionResp;
import com.purui.prims.headless.api.pojo.response.ModelResp;
import com.purui.prims.headless.server.persistence.dataobject.DimensionDO;
import com.purui.prims.headless.server.persistence.mapper.DimensionDOMapper;
import com.purui.prims.headless.server.persistence.repository.DimensionRepository;
import com.purui.prims.headless.server.pojo.DimensionFilter;
import com.purui.prims.headless.server.pojo.DimensionsFilter;
import com.purui.prims.headless.server.pojo.MetaFilter;
import com.purui.prims.headless.server.pojo.ModelFilter;
import com.purui.prims.headless.server.service.DataSetService;
import com.purui.prims.headless.server.service.DatabaseService;
import com.purui.prims.headless.server.service.DimensionService;
import com.purui.prims.headless.server.service.ModelRelaService;
import com.purui.prims.headless.server.service.ModelService;
import com.purui.prims.headless.server.utils.DimensionConverter;
import com.purui.prims.headless.server.utils.NameCheckUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Service
@Slf4j
public class DimensionServiceImpl extends ServiceImpl<DimensionDOMapper, DimensionDO>
    implements DimensionService {


    private DimensionRepository dimensionRepository;

    private ModelService modelService;

    private DatabaseService databaseService;

    private ModelRelaService modelRelaService;

    private DataSetService dataSetService;

    @Autowired
    private ApplicationEventPublisher eventPublisher;


    public DimensionServiceImpl(DimensionRepository dimensionRepository,
                                ModelService modelService,
                                DatabaseService databaseService,
                                ModelRelaService modelRelaService,
                                DataSetService dataSetService) {
        this.modelService = modelService;
        this.dimensionRepository = dimensionRepository;
        this.databaseService = databaseService;
        this.modelRelaService = modelRelaService;
        this.dataSetService = dataSetService;
    }

    @Override
    public DimensionResp createDimension(DimensionReq dimensionReq, User user) {
        checkExist(Lists.newArrayList(dimensionReq));
        dimensionReq.createdBy(user.getUserName());
        DimensionDO dimensionDO = DimensionConverter.convert2DimensionDO(dimensionReq);
        dimensionRepository.createDimension(dimensionDO);
        sendEventBatch(Lists.newArrayList(dimensionDO), EventType.ADD);
        return DimensionConverter.convert2DimensionResp(dimensionDO);
    }

    @Override
    public void createDimensionBatch(List<DimensionReq> dimensionReqs, User user) {
        if (CollectionUtils.isEmpty(dimensionReqs)) {
            return;
        }
        Long modelId = dimensionReqs.get(0).getModelId();
        List<DimensionResp> dimensionResps = getDimensions(modelId);
        Map<String, DimensionResp> bizNameMap = dimensionResps.stream()
                                                              .collect(Collectors.toMap(DimensionResp::getBizName, a -> a, (k1, k2) -> k1));
        Map<String, DimensionResp> nameMap = dimensionResps.stream()
                                                           .collect(Collectors.toMap(DimensionResp::getName, a -> a, (k1, k2) -> k1));
        List<DimensionReq> dimensionToInsert = dimensionReqs.stream()
                                                            .filter(dimension -> !bizNameMap.containsKey(dimension.getBizName())
                                                                                 && !nameMap.containsKey(dimension.getName()))
                                                            .collect(Collectors.toList());
        dimensionReqs.stream()
                                                            .filter(dimension -> bizNameMap.containsKey(dimension.getBizName())
                                                                                 || nameMap.containsKey(dimension.getName()))
            .forEach(dimension -> {
                DimensionResp dimensionResp = bizNameMap.get(dimension.getBizName());
                if (dimensionResp != null) {
                    dimension.setId(dimensionResp.getId());
                }
                updateDimension(dimension, user);
            });

        if (CollectionUtils.isEmpty(dimensionToInsert)) {
            return;
        }
        List<DimensionDO> dimensionDOS = dimensionToInsert.stream().peek(dimension ->
                                                              dimension.createdBy(user.getUserName()))
                                                          .map(DimensionConverter::convert2DimensionDO)
                                                          .collect(Collectors.toList());
        dimensionRepository.createDimensionBatch(dimensionDOS);
        sendEventBatch(dimensionDOS, EventType.ADD);
    }

    @Override
    public void updateDimension(DimensionReq dimensionReq, User user) {
        checkExist(Lists.newArrayList(dimensionReq));
        DimensionDO dimensionDO = dimensionRepository.getDimensionById(dimensionReq.getId());
        dimensionReq.updatedBy(user.getUserName());
        String oldName = dimensionDO.getName();
        DimensionConverter.convert(dimensionDO, dimensionReq);
        dimensionRepository.updateDimension(dimensionDO);
        if (!oldName.equals(dimensionDO.getName())) {
            sendEvent(DataItem.builder().modelId(dimensionDO.getModelId() + Constants.UNDERLINE)
                              .newName(dimensionReq.getName()).name(oldName).type(TypeEnums.DIMENSION)
                              .id(dimensionDO.getId() + Constants.UNDERLINE).build(), EventType.UPDATE);
        }
    }

    @Override
    public void batchUpdateStatus(MetaBatchReq metaBatchReq, User user) {
        if (CollectionUtils.isEmpty(metaBatchReq.getIds())) {
            return;
        }
        DimensionFilter dimensionFilter = new DimensionFilter();
        dimensionFilter.setIds(metaBatchReq.getIds());
        List<DimensionDO> dimensionDOS = dimensionRepository.getDimension(dimensionFilter);
        if (CollectionUtils.isEmpty(dimensionDOS)) {
            return;
        }
        dimensionDOS = dimensionDOS.stream()
                                   .peek(dimensionDO -> {
                                       dimensionDO.setStatus(metaBatchReq.getStatus());
                                       dimensionDO.setUpdatedAt(new Date());
                                       dimensionDO.setUpdatedBy(user.getUserName());
                                   })
                                   .collect(Collectors.toList());
        dimensionRepository.batchUpdateStatus(dimensionDOS);
        if (StatusEnum.OFFLINE.getCode().equals(metaBatchReq.getStatus())
            || StatusEnum.DELETED.getCode().equals(metaBatchReq.getStatus())) {
            sendEventBatch(dimensionDOS, EventType.DELETE);
        } else if (StatusEnum.ONLINE.getCode().equals(metaBatchReq.getStatus())) {
            sendEventBatch(dimensionDOS, EventType.ADD);
        }
    }

    @Override
    public void batchUpdateSensitiveLevel(MetaBatchReq metaBatchReq, User user) {
        DimensionFilter metaFilter = new DimensionFilter();
        metaFilter.setIds(metaBatchReq.getIds());
        List<DimensionDO> dimensionDOS = queryDimension(metaFilter);
        for (DimensionDO dimensionDO : dimensionDOS) {
            dimensionDO.setUpdatedAt(new Date());
            dimensionDO.setUpdatedBy(user.getUserName());
            dimensionDO.setSensitiveLevel(metaBatchReq.getSensitiveLevel());
        }
        updateBatchById(dimensionDOS);
    }

    @Override
    public void deleteDimension(Long id, User user) {
        DimensionDO dimensionDO = dimensionRepository.getDimensionById(id);
        if (dimensionDO == null) {
            throw new RuntimeException(String.format("the dimension %s not exist", id));
        }
        dimensionDO.setStatus(StatusEnum.DELETED.getCode());
        dimensionDO.setUpdatedAt(new Date());
        dimensionDO.setUpdatedBy(user.getUserName());
        dimensionRepository.updateDimension(dimensionDO);
        sendEventBatch(Lists.newArrayList(dimensionDO), EventType.DELETE);
    }

    @Override
    public DimensionResp getDimension(String bizName, Long modelId) {
        List<DimensionResp> dimensionResps = getDimensions(modelId);
        if (CollectionUtils.isEmpty(dimensionResps)) {
            return null;
        }
        for (DimensionResp dimensionResp : dimensionResps) {
            if (dimensionResp.getBizName().equalsIgnoreCase(bizName)) {
                return dimensionResp;
            }
        }
        return null;
    }

    @Override
    public DimensionResp getDimension(Long id) {
        DimensionDO dimensionDO = dimensionRepository.getDimensionById(id);
        if (dimensionDO == null) {
            return null;
        }
        return DimensionConverter.convert2DimensionResp(dimensionDO, new HashMap<>());
    }

    @Override
    public PageInfo<DimensionResp> queryDimension(PageDimensionReq pageDimensionReq) {
        DimensionFilter dimensionFilter = new DimensionFilter();
        BeanUtils.copyProperties(pageDimensionReq, dimensionFilter);
        dimensionFilter.setModelIds(pageDimensionReq.getModelIds());
        PageInfo<DimensionDO> dimensionDOPageInfo = PageHelper.startPage(pageDimensionReq.getCurrent(),
                                                                  pageDimensionReq.getPageSize())
                                                              .doSelectPageInfo(() -> queryDimension(dimensionFilter));
        PageInfo<DimensionResp> pageInfo = new PageInfo<>();
        BeanUtils.copyProperties(dimensionDOPageInfo, pageInfo);
        pageInfo.setList(convertList(dimensionDOPageInfo.getList()));
        return pageInfo;
    }

    private List<DimensionDO> queryDimension(DimensionFilter dimensionFilter) {
        return dimensionRepository.getDimension(dimensionFilter);
    }

    @Override
    public List<DimensionResp> queryDimensions(DimensionsFilter dimensionsFilter) {
        List<DimensionDO> dimensions = dimensionRepository.getDimensions(dimensionsFilter);
        return convertList(dimensions);
    }

    @Override
    public List<DimensionResp> getDimensions(MetaFilter metaFilter) {
        DimensionFilter dimensionFilter = new DimensionFilter();
        BeanUtils.copyProperties(metaFilter, dimensionFilter);
        List<DimensionDO> dimensionDOS = dimensionRepository.getDimension(dimensionFilter);
        List<DimensionResp> dimensionResps = convertList(dimensionDOS);
        if (!CollectionUtils.isEmpty(metaFilter.getFieldsDepend())) {
            return filterByField(dimensionResps, metaFilter.getFieldsDepend());
        }
        if (metaFilter.getDataSetId() != null) {
            DataSetResp dataSetResp = dataSetService.getDataSet(metaFilter.getDataSetId());
            return DimensionConverter.filterByDataSet(dimensionResps, dataSetResp);
        }
        return dimensionResps;
    }

    private List<DimensionResp> getDimensions(Long modelId) {
        return getDimensions(new MetaFilter(Lists.newArrayList(modelId)));
    }

    private List<DimensionResp> filterByField(List<DimensionResp> dimensionResps, List<String> fields) {
        List<DimensionResp> dimensionFiltered = Lists.newArrayList();
        for (DimensionResp dimensionResp : dimensionResps) {
            for (String field : fields) {
                if (dimensionResp.getExpr().contains(field)) {
                    dimensionFiltered.add(dimensionResp);
                }
            }
        }
        return dimensionFiltered;
    }

    @Override
    public List<DimensionResp> getDimensionInModelCluster(Long modelId) {
        ModelResp modelResp = modelService.getModel(modelId);
        List<ModelRela> modelRelas = modelRelaService.getModelRelaList(modelResp.getDomainId());
        List<Long> modelIds = new ArrayList<>();
        modelIds.add(modelId);
        for (ModelRela modelRela : modelRelas) {
            modelIds.add(modelRela.getFromModelId());
            modelIds.add(modelRela.getToModelId());
        }
        DimensionFilter dimensionFilter = new DimensionFilter();
        dimensionFilter.setModelIds(modelIds);
        return getDimensions(dimensionFilter);
    }

    private List<DimensionResp> convertList(List<DimensionDO> dimensionDOS) {
        List<Long> modelIds = dimensionDOS.stream().map(DimensionDO::getModelId)
                                          .collect(Collectors.toList());
        ModelFilter modelFilter = new ModelFilter(false, modelIds);
        Map<Long, ModelResp> modelMap = modelService.getModelMap(modelFilter);
        List<DimensionResp> dimensionResps = Lists.newArrayList();
        if (!CollectionUtils.isEmpty(dimensionDOS)) {
            dimensionResps = dimensionDOS.stream()
                                         .map(dimensionDO -> DimensionConverter
                                             .convert2DimensionResp(dimensionDO, modelMap))
                                         .collect(Collectors.toList());
        }
        return dimensionResps;
    }

    private void checkExist(List<DimensionReq> dimensionReqs) {
        Long modelId = dimensionReqs.get(0).getModelId();
        List<DimensionResp> dimensionResps = getDimensions(modelId);
        Map<String, DimensionResp> bizNameMap = dimensionResps.stream()
                                                              .collect(Collectors.toMap(DimensionResp::getBizName, a -> a, (k1, k2) -> k1));
        Map<String, DimensionResp> nameMap = dimensionResps.stream()
                                                           .collect(Collectors.toMap(DimensionResp::getName, a -> a, (k1, k2) -> k1));
        for (DimensionReq dimensionReq : dimensionReqs) {
            String forbiddenCharacters = NameCheckUtils.findForbiddenCharacters(dimensionReq.getName());
            if (StringUtils.isNotBlank(forbiddenCharacters)) {
                throw new InvalidArgumentException(String.format("名称包含特殊字符, 请修改: %s，特殊字符: %s",
                    dimensionReq.getName(), forbiddenCharacters));
            }
            if (bizNameMap.containsKey(dimensionReq.getBizName())) {
                DimensionResp dimensionResp = bizNameMap.get(dimensionReq.getBizName());
                if (!dimensionResp.getId().equals(dimensionReq.getId())) {
                    throw new RuntimeException(String.format("该主题域下存在相同的维度字段名:%s 创建人:%s",
                        dimensionReq.getBizName(), dimensionResp.getCreatedBy()));
                }
            }
            if (nameMap.containsKey(dimensionReq.getName())) {
                DimensionResp dimensionResp = nameMap.get(dimensionReq.getName());
                if (!dimensionResp.getId().equals(dimensionReq.getId())) {
                    throw new RuntimeException(String.format("该主题域下存在相同的维度名:%s 创建人:%s",
                        dimensionReq.getName(), dimensionResp.getCreatedBy()));
                }
            }
        }
    }

    @Override
    public void sendDimensionEventBatch(List<Long> modelIds, EventType eventType) {
        DimensionFilter dimensionFilter = new DimensionFilter();
        dimensionFilter.setModelIds(modelIds);
        List<DimensionDO> dimensionDOS = queryDimension(dimensionFilter);
        sendEventBatch(dimensionDOS, eventType);
    }

    private void sendEventBatch(List<DimensionDO> dimensionDOS, EventType eventType) {
        DataEvent dataEvent = getDataEvent(dimensionDOS, eventType);
        eventPublisher.publishEvent(dataEvent);
    }

    public DataEvent getDataEvent() {
        DimensionFilter dimensionFilter = new DimensionFilter();
        List<DimensionDO> dimensionDOS = queryDimension(dimensionFilter);
        return getDataEvent(dimensionDOS, EventType.ADD);
    }

    private DataEvent getDataEvent(List<DimensionDO> dimensionDOS, EventType eventType) {
        List<DataItem> dataItems = dimensionDOS.stream()
                                               .map(dimensionDO -> DataItem.builder().id(dimensionDO.getId() + Constants.UNDERLINE)
                                                                           .name(dimensionDO.getName())
                                                                           .modelId(dimensionDO.getModelId() + Constants.UNDERLINE)
                                                                           .type(TypeEnums.DIMENSION).build())
                                               .collect(Collectors.toList());
        return new DataEvent(this, dataItems, eventType);
    }

    private void sendEvent(DataItem dataItem, EventType eventType) {
        eventPublisher.publishEvent(new DataEvent(this,
            Lists.newArrayList(dataItem), eventType));
    }
}