package com.zt.plat.module.base.service.masterdatasync;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil;
import com.zt.plat.module.base.dal.dao.materialclasses.MaterialClassesMapper;
import com.zt.plat.module.base.dal.dao.materialhasclasses.MaterialHasClassesMapper;
import com.zt.plat.module.base.dal.dao.materialhasproperties.MaterialHasPropertiesMapper;
import com.zt.plat.module.base.dal.dao.materialproperties.MaterialPropertiesMapper;
import com.zt.plat.module.base.dal.dataobject.base.MaterialInfomationDO;
import com.zt.plat.module.base.dal.dataobject.materialclasses.MaterialClassesDO;
import com.zt.plat.module.base.dal.dataobject.materialhasclasses.MaterialHasClassesDO;
import com.zt.plat.module.base.dal.dataobject.materialhasproperties.MaterialHasPropertiesDO;
import com.zt.plat.module.base.dal.dataobject.materialproperties.MaterialPropertiesDO;
import com.zt.plat.module.base.dal.dataobject.masterdata.MdmMaterialViewDO;
import com.zt.plat.module.base.dal.mysql.base.MaterialInfomationMapper;
import com.zt.plat.module.base.dal.mysql.masterdata.MdmMaterialViewMapper;
import com.zt.plat.module.base.enums.ErrorCodeConstants;
import com.zt.plat.module.base.framework.sync.config.MasterDataSyncProperties;
import com.zt.plat.module.base.service.masterdatasync.dto.MasterDataSyncCommand;
import com.zt.plat.module.base.service.masterdatasync.dto.MasterDataSyncReport;
import com.zt.plat.module.base.service.masterdatasync.support.MasterDataPropertyDefinition;
import com.zt.plat.module.base.service.masterdatasync.support.MasterDataSyncNotifier;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * 主数据同步核心实现，负责从外部 MDM 视图读取数据并落库到本地 BSE 表。
 */
@Slf4j
@Service
public class MasterDataSyncServiceImpl implements MasterDataSyncService {

    @Resource
    private MdmMaterialViewMapper mdmMaterialViewMapper;
    @Resource
    private MaterialInfomationMapper materialInfomationMapper;
    @Resource
    private MaterialClassesMapper materialClassesMapper;
    @Resource
    private MaterialHasClassesMapper materialHasClassesMapper;
    @Resource
    private MaterialPropertiesMapper materialPropertiesMapper;
    @Resource
    private MaterialHasPropertiesMapper materialHasPropertiesMapper;
    @Resource
    private TransactionTemplate transactionTemplate;
    @Resource
    private MasterDataSyncProperties properties;
    @Resource
    private MasterDataSyncNotifier notifier;

    @Override
    public MasterDataSyncReport sync(MasterDataSyncCommand command) {
        if (!properties.isEnabled()) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.MASTER_DATA_SYNC_DISABLED);
        }
        if (command.getBatchSize() > 1000) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.MASTER_DATA_SYNC_BATCH_SIZE_TOO_LARGE, command.getBatchSize());
        }
        MasterDataSyncReport report = MasterDataSyncReport.start(command);
        List<String> materialCodes = sanitizeCodes(command.getMaterialCodes());
        report.setMaterialCodes(materialCodes);
        int batchSize = resolveBatchSize(command.getBatchSize());
        report.setBatchSize(batchSize);
        Long recordLimit = resolveRecordLimit(command.getRecordLimit());
        report.setRecordLimit(recordLimit);
        LocalDateTime since = command.getSince();
        Map<MasterDataPropertyDefinition, MaterialPropertiesDO> propertyDefinitions = ensurePropertyDefinitions(report);
        // 缓存已读取的物料与分类，避免批次间重复查询数据库
        Map<Long, MaterialInfomationDO> materialCacheById = new LinkedHashMap<>();
        Map<String, MaterialInfomationDO> materialCacheByCode = new LinkedHashMap<>();
        Map<String, MaterialClassesDO> categoryCacheByCode = new LinkedHashMap<>();

        long offset = 0L;
        Throwable failure = null;
        try {
            while (true) {
                int fetchSize = batchSize;
                if (recordLimit != null) {
                    long remaining = recordLimit - report.getProcessedRecords();
                    if (remaining <= 0) {
                        break;
                    }
                    if (remaining < fetchSize) {
                        fetchSize = (int) remaining;
                    }
                }
                List<MdmMaterialViewDO> slice = mdmMaterialViewMapper.selectSlice(offset, fetchSize, since, materialCodes);
                if (CollUtil.isEmpty(slice)) {
                    break;
                }
                offset += slice.size();
                report.incrementProcessedRecords(slice.size());
                final List<MdmMaterialViewDO> batch = slice;
                // 单批落库使用事务，保证本批次原子性与幂等可重试
                transactionTemplate.executeWithoutResult(status -> processBatch(batch, propertyDefinitions, report,
                    materialCacheById, materialCacheByCode, categoryCacheByCode));
            }
            report.markSuccess();
            return report;
        } catch (Throwable ex) {
            failure = ex;
            report.markFailure(ex.getMessage());
            throw ex;
        } finally {
            report.finish();
            notifier.dispatch(report, failure);
        }
    }

    private List<String> sanitizeCodes(List<String> codes) {
        if (CollUtil.isEmpty(codes)) {
            return Collections.emptyList();
        }
        return codes.stream()
                .filter(StrUtil::isNotBlank)
                .map(code -> StrUtil.trim(code).toUpperCase())
                .distinct()
                .collect(Collectors.toList());
    }

    private int resolveBatchSize(Integer requested) {
        if (requested != null && requested > 0) {
            return requested;
        }
        return Math.max(1, properties.getBatchSize());
    }

    private Long resolveRecordLimit(Long requested) {
        if (requested == null || requested <= 0) {
            return null;
        }
        return requested;
    }

    /**
     * 确保所有需要的属性定义已经存在，若缺失则自动创建。
     */
    private Map<MasterDataPropertyDefinition, MaterialPropertiesDO> ensurePropertyDefinitions(MasterDataSyncReport report) {
        List<String> codes = Arrays.stream(MasterDataPropertyDefinition.values())
                .map(MasterDataPropertyDefinition::getCode)
                .toList();
        List<MaterialPropertiesDO> existing = materialPropertiesMapper.selectByCodes(codes);
        Map<String, MaterialPropertiesDO> existingMap = Optional.ofNullable(existing)
                .orElse(Collections.emptyList())
                .stream()
                .collect(Collectors.toMap(MaterialPropertiesDO::getCode, Function.identity()));

        Map<MasterDataPropertyDefinition, MaterialPropertiesDO> mapping = new EnumMap<>(MasterDataPropertyDefinition.class);
        List<MaterialPropertiesDO> pendingInsertDefinitions = new ArrayList<>();
        for (MasterDataPropertyDefinition definition : MasterDataPropertyDefinition.values()) {
            MaterialPropertiesDO current = existingMap.get(definition.getCode());
            if (current == null) {
                current = MaterialPropertiesDO.builder()
                        .id(IdWorker.getId())
                        .code(definition.getCode())
                        .name(definition.getDisplayName())
                        .dataType(definition.getDataType())
                        .remark("MDM同步自动创建")
                        .build();
                pendingInsertDefinitions.add(current);
                report.incrementInsertedPropertyDefinitions();
            } else {
                boolean needUpdate = false;
                if (!StrUtil.equals(current.getName(), definition.getDisplayName())) {
                    current.setName(definition.getDisplayName());
                    needUpdate = true;
                }
                if (!StrUtil.equals(current.getDataType(), definition.getDataType())) {
                    current.setDataType(definition.getDataType());
                    needUpdate = true;
                }
                if (needUpdate) {
                    MaterialPropertiesDO update = new MaterialPropertiesDO();
                    update.setId(current.getId());
                    update.setName(current.getName());
                    update.setDataType(current.getDataType());
                    materialPropertiesMapper.updateById(update);
                    report.incrementUpdatedPropertyDefinitions();
                }
            }
            mapping.put(definition, current);
        }
        if (CollUtil.isNotEmpty(pendingInsertDefinitions)) {
            materialPropertiesMapper.insertBatch(pendingInsertDefinitions);
        }
        return mapping;
    }

    private void processBatch(List<MdmMaterialViewDO> batch,
                              Map<MasterDataPropertyDefinition, MaterialPropertiesDO> propertyDefinitions,
                              MasterDataSyncReport report,
                              Map<Long, MaterialInfomationDO> materialCacheById,
                              Map<String, MaterialInfomationDO> materialCacheByCode,
                              Map<String, MaterialClassesDO> categoryCacheByCode) {
        if (CollUtil.isEmpty(batch)) {
            return;
        }
        // 1. 落库物料主数据
        Map<String, MaterialInfomationDO> materials = upsertMaterials(batch, report, materialCacheById, materialCacheByCode);
        // 2. 读取已存在的分类信息
        Map<String, MaterialClassesDO> categoryClasses = loadCategoryClasses(batch, categoryCacheByCode);
        // 3. 建立物料与分类的关系
        bindMaterialClasses(batch, materials, categoryClasses, report);
        // 4. 写入物料属性及属性值
        upsertMaterialProperties(batch, materials, propertyDefinitions, report);
    }

    private Map<String, MaterialClassesDO> loadCategoryClasses(List<MdmMaterialViewDO> batch,
                                                              Map<String, MaterialClassesDO> categoryCacheByCode) {
        Set<String> requestedCodes = batch.stream()
                .map(MdmMaterialViewDO::getCategoryCode)
                .map(this::normalizeValue)
                .filter(Objects::nonNull)
                .collect(Collectors.toCollection(LinkedHashSet::new));
        if (requestedCodes.isEmpty()) {
            return Collections.emptyMap();
        }
        Map<String, MaterialClassesDO> mapping = requestedCodes.stream()
                .map(code -> new Object[]{code, categoryCacheByCode.get(code)})
                .filter(pair -> pair[1] != null)
                .collect(Collectors.toMap(pair -> (String) pair[0], pair -> (MaterialClassesDO) pair[1], (a, b) -> a, LinkedHashMap::new));
        Set<String> missing = new LinkedHashSet<>(requestedCodes);
        missing.removeIf(mapping::containsKey);
        if (!missing.isEmpty()) {
            List<MaterialClassesDO> newlyLoaded = materialClassesMapper.selectByCodes(missing);
            Optional.ofNullable(newlyLoaded)
                    .orElse(Collections.emptyList())
                    .forEach(item -> {
                        String code = normalizeValue(item.getCode());
                        if (code != null) {
                            categoryCacheByCode.put(code, item);
                            mapping.put(code, item);
                        }
                    });
            missing.removeIf(mapping::containsKey);
            if (!missing.isEmpty()) {
                List<String> preview = missing.stream().limit(10).toList();
                log.warn("当前批次存在 {} 个分类未同步，示例: {}", missing.size(), preview);
            }
        }
        return mapping;
    }

    private Map<String, MaterialInfomationDO> upsertMaterials(List<MdmMaterialViewDO> batch,
                                                              MasterDataSyncReport report,
                                                              Map<Long, MaterialInfomationDO> materialCacheById,
                                                              Map<String, MaterialInfomationDO> materialCacheByCode) {
        Set<Long> sourceIds = batch.stream()
                .map(MdmMaterialViewDO::getCodeId)
                .filter(Objects::nonNull)
                .filter(id -> id > 0)
                .collect(Collectors.toCollection(LinkedHashSet::new));
        if (sourceIds.isEmpty()) {
            log.warn("本次批次缺少有效 CODEID，无法同步物料主数据");
            return Collections.emptyMap();
        }
        Map<Long, MaterialInfomationDO> existingById = sourceIds.stream()
                .map(materialCacheById::get)
                .filter(Objects::nonNull)
                .collect(Collectors.toMap(MaterialInfomationDO::getId, Function.identity(), (a, b) -> a, LinkedHashMap::new));
        Set<Long> missingIds = new LinkedHashSet<>(sourceIds);
        missingIds.removeIf(existingById::containsKey);
        if (!missingIds.isEmpty()) {
            List<MaterialInfomationDO> fetched = materialInfomationMapper.selectBatchIds(missingIds);
            Optional.ofNullable(fetched)
                    .orElse(Collections.emptyList())
                    .forEach(item -> {
                        if (item.getId() != null) {
                            existingById.put(item.getId(), item);
                            materialCacheById.put(item.getId(), item);
                            String normalizedCode = normalizeValue(item.getCode());
                            if (normalizedCode != null) {
                                materialCacheByCode.put(normalizedCode, item);
                            }
                        }
                    });
        }
        Map<String, MaterialInfomationDO> mapping = new LinkedHashMap<>();
        List<MaterialInfomationDO> pendingMaterialInserts = new ArrayList<>();
        List<MaterialInfomationDO> pendingMaterialUpdates = new ArrayList<>(); // 收集需批量更新的物料主数据
        for (MdmMaterialViewDO item : batch) {
            Long codeId = item.getCodeId();
            if (codeId == null || codeId <= 0) {
                log.warn("物料 {} 缺少主键 CODEID，已跳过", item.getMaterialCode());
                continue;
            }
            String code = normalizeValue(item.getMaterialCode());
            if (code == null) {
                continue;
            }
            MaterialInfomationDO current = existingById.get(codeId);
            // 名称优先取 MDM 的短描述（descshort），若为空再回退到 desc1
            String name = normalizeValue(item.getShortDescription());
            if (name == null) {
                name = normalizeValue(item.getMaterialName());
            }
            if (current == null) {
                MaterialInfomationDO created = MaterialInfomationDO.builder()
                        .id(codeId)
                        .code(code)
                        .name(StrUtil.emptyIfNull(name))
                        .remark(null)
                        .build();
                pendingMaterialInserts.add(created);
                existingById.put(codeId, created);
                materialCacheById.put(codeId, created);
                materialCacheByCode.put(code, created);
                mapping.put(code, created);
                report.incrementInsertedMaterials();
                continue;
            }

            boolean needUpdate = false;
            MaterialInfomationDO update = null;
            if (!StrUtil.equals(current.getCode(), code)) {
                current.setCode(code);
                needUpdate = true;
            }
            if (name != null && !StrUtil.equals(current.getName(), name)) {
                current.setName(name);
                needUpdate = true;
            }
            if (needUpdate) {
                update = new MaterialInfomationDO();
                update.setId(current.getId());
                update.setCode(current.getCode());
                update.setName(current.getName());
                pendingMaterialUpdates.add(update);
                report.incrementUpdatedMaterials();
            }
            mapping.put(code, current);
            materialCacheByCode.put(code, current);
        }
        if (CollUtil.isNotEmpty(pendingMaterialInserts)) {
            materialInfomationMapper.insertBatch(pendingMaterialInserts);
        }
        if (CollUtil.isNotEmpty(pendingMaterialUpdates)) {
            materialInfomationMapper.updateBatch(pendingMaterialUpdates);
        }
        return mapping;
    }

    private void bindMaterialClasses(List<MdmMaterialViewDO> batch,
                                     Map<String, MaterialInfomationDO> materials,
                                     Map<String, MaterialClassesDO> categoryClasses,
                                     MasterDataSyncReport report) {
        if (materials.isEmpty() || categoryClasses.isEmpty()) {
            return;
        }
        List<Long> infoIds = batch.stream()
            .map(MdmMaterialViewDO::getMaterialCode)
            .map(this::normalizeValue)
            .map(materials::get)
                .filter(Objects::nonNull)
                .map(MaterialInfomationDO::getId)
                .filter(Objects::nonNull)
                .distinct()
                .collect(Collectors.toList());
        Map<Long, MaterialHasClassesDO> existingRelations = materialHasClassesMapper.selectByInfoIds(infoIds)
            .stream()
            .collect(Collectors.toMap(MaterialHasClassesDO::getInfomationId, Function.identity(), (a, b) -> a));
        List<MaterialHasClassesDO> relationsToInsert = new ArrayList<>();
        List<MaterialHasClassesDO> relationsToUpdate = new ArrayList<>(); // 收集需批量更新的物料-分类关系
        for (MdmMaterialViewDO item : batch) {
            String materialCode = normalizeValue(item.getMaterialCode());
            MaterialInfomationDO info = materials.get(materialCode);
            if (info == null) {
                continue;
            }
            String categoryCode = normalizeValue(item.getCategoryCode());
            MaterialClassesDO cls = categoryClasses.get(categoryCode);
            if (cls == null) {
                continue;
            }
            MaterialHasClassesDO relation = existingRelations.get(info.getId());
            if (relation == null) {
                MaterialHasClassesDO created = MaterialHasClassesDO.builder()
                        .infomationId(info.getId())
                        .classesId(cls.getId())
                        .build();
                relationsToInsert.add(created);
                existingRelations.put(info.getId(), created);
                report.incrementCreatedClassRelations();
            } else if (!Objects.equals(relation.getClassesId(), cls.getId())) {
                MaterialHasClassesDO update = new MaterialHasClassesDO();
                update.setId(relation.getId());
                update.setClassesId(cls.getId());
                relationsToUpdate.add(update);
                relation.setClassesId(cls.getId());
                report.incrementUpdatedClassRelations();
            }
        }
        if (CollUtil.isNotEmpty(relationsToInsert)) {
            materialHasClassesMapper.insertBatch(relationsToInsert);
        }
        if (CollUtil.isNotEmpty(relationsToUpdate)) {
            materialHasClassesMapper.updateBatch(relationsToUpdate);
        }
    }

    private void upsertMaterialProperties(List<MdmMaterialViewDO> batch,
                                          Map<String, MaterialInfomationDO> materials,
                                          Map<MasterDataPropertyDefinition, MaterialPropertiesDO> propertyDefinitions,
                                          MasterDataSyncReport report) {
        if (materials.isEmpty() || propertyDefinitions.isEmpty()) {
            return;
        }
        List<Long> infoIds = batch.stream()
            .map(MdmMaterialViewDO::getMaterialCode)
            .map(this::normalizeValue)
            .map(materials::get)
                .filter(Objects::nonNull)
                .map(MaterialInfomationDO::getId)
                .filter(Objects::nonNull)
                .distinct()
                .collect(Collectors.toList());
        List<Long> propertyIds = propertyDefinitions.values().stream()
                .map(MaterialPropertiesDO::getId)
                .filter(Objects::nonNull)
                .distinct()
                .collect(Collectors.toList());
        Map<String, MaterialHasPropertiesDO> existing = materialHasPropertiesMapper
                .selectByInfoIdsAndPropertyIds(infoIds, propertyIds)
                .stream()
                .collect(Collectors.toMap(item -> buildRelationKey(item.getInfomationId(), item.getPropertiesId()),
                        Function.identity(), (a, b) -> a));
        List<MaterialHasPropertiesDO> propertyRelationsToInsert = new ArrayList<>();
        List<MaterialHasPropertiesDO> propertyRelationsToUpdate = new ArrayList<>(); // 收集需批量更新的物料属性值
        for (MdmMaterialViewDO item : batch) {
            String materialCode = normalizeValue(item.getMaterialCode());
            MaterialInfomationDO info = materials.get(materialCode);
            if (info == null || info.getId() == null) {
                continue;
            }
            for (Map.Entry<MasterDataPropertyDefinition, MaterialPropertiesDO> entry : propertyDefinitions.entrySet()) {
                MaterialPropertiesDO property = entry.getValue();
                if (property == null || property.getId() == null) {
                    continue;
                }
                String value = normalizeValue(entry.getKey().extractValue(item));
                if (value == null) {
                    continue;
                }
                String key = buildRelationKey(info.getId(), property.getId());
                MaterialHasPropertiesDO relation = existing.get(key);
                if (relation == null) {
                    MaterialHasPropertiesDO created = MaterialHasPropertiesDO.builder()
                            .infomationId(info.getId())
                            .propertiesId(property.getId())
                            .value(value)
                            .sort(entry.getKey().getSort())
                            .isKey(0)
                            .isMetering(0)
                            .build();
                    propertyRelationsToInsert.add(created);
                    existing.put(key, created);
                    report.incrementInsertedPropertyValues();
                } else if (!StrUtil.equals(relation.getValue(), value)
                        || !Objects.equals(relation.getSort(), entry.getKey().getSort())) {
                    MaterialHasPropertiesDO update = new MaterialHasPropertiesDO();
                    update.setId(relation.getId());
                    update.setValue(value);
                    update.setSort(entry.getKey().getSort());
                    propertyRelationsToUpdate.add(update);
                    relation.setValue(value);
                    relation.setSort(entry.getKey().getSort());
                    report.incrementUpdatedPropertyValues();
                }
            }
        }
        if (CollUtil.isNotEmpty(propertyRelationsToInsert)) {
            materialHasPropertiesMapper.insertBatch(propertyRelationsToInsert);
        }
        if (CollUtil.isNotEmpty(propertyRelationsToUpdate)) {
            materialHasPropertiesMapper.updateBatch(propertyRelationsToUpdate);
        }
    }

    private String buildRelationKey(Long infoId, Long propertyId) {
        return infoId + ":" + propertyId;
    }

    private String normalizeValue(String value) {
        return StrUtil.isBlank(value) ? null : StrUtil.trim(value);
    }
}
