package com.usky.cdi.service.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.nacos.shaded.com.google.gson.Gson;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.usky.cdi.domain.*;
import com.usky.cdi.service.enums.MqttTopics;
import com.usky.cdi.mapper.*;
import com.usky.cdi.service.CdiDeliveryLogService;
import com.usky.cdi.service.mqtt.MqttConnectionTool;
import com.usky.cdi.service.util.SnowflakeIdGenerator;
import com.usky.cdi.service.vo.SyncTaskStatisticsVO;
import com.usky.cdi.service.vo.info.FloorPlaneVO;
import com.usky.cdi.service.vo.info.ProtectiveUnitVO;
import com.usky.common.core.bean.CommonPage;
import com.usky.common.core.exception.BusinessException;
import com.usky.common.mybatis.core.AbstractCrudService;
import com.usky.common.security.utils.SecurityUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Value;

import javax.annotation.PostConstruct;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;

/**
 * <p>
 * 人防_投递日志表（记录数据的MQTT投递情况） 服务实现类
 * </p>
 *
 * @author fu
 * @since 2026-02-02
 */
@Slf4j
@Service
public class CdiDeliveryLogServiceImpl extends AbstractCrudService<CdiDeliveryLogMapper, CdiDeliveryLog> implements CdiDeliveryLogService {

    private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    @Autowired
    private SysUserMapper sysUserMapper;

    @Autowired
    private BaseBuildFacilityMapper baseBuildFacilityMapper;

    @Autowired
    private BaseBuildUnitMapper baseBuildUnitMapper;

    @Autowired
    private CdiDefenseProjectMapper cdiDefenseProjectMapper;

    @Autowired
    private BaseBuildPlaneMapper baseBuildPlaneMapper;

    @Autowired
    private BaseBuildMapper baseBuildMapper;

    @Autowired
    private BaseDataTransferService baseDataTransferService;

    @Autowired
    private IotDataTransferService iotDataTransferService;

    @Autowired
    private MqttConnectionTool mqttConnectionTool;

    // 从配置文件读取Snowflake参数，默认值为1
    @Value("${snowflake.worker-id:1}")
    private long workerId;

    @Value("${snowflake.data-center-id:1}")
    private long dataCenterId;

    private SnowflakeIdGenerator idGenerator;

    @PostConstruct
    public void init() {
        this.idGenerator = new SnowflakeIdGenerator(workerId, dataCenterId);
    }

    /**
     * 生成数据包ID
     */
    private Long generateDataPacketID() {
        return idGenerator.nextPacketId();
    }

    @Override
    public List<SyncTaskStatisticsVO> selectById(Long id) {
        Integer tenantId = SecurityUtils.getTenantId();
        if (tenantId == null || tenantId <= 0) {
            log.warn("未获取到当前租户ID，无法查询人防投递日志");
            return Collections.emptyList();
        }

        List<CdiDeliveryLog> logList = lambdaQuery()
                .eq(CdiDeliveryLog::getTenantId, tenantId)
                .eq(id != null, CdiDeliveryLog::getId, id)
                .orderByDesc(CdiDeliveryLog::getId)
                .list();

        LambdaQueryWrapper<CdiDefenseProject> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(CdiDefenseProject::getTenantId, tenantId);
        CdiDefenseProject defenseProject = cdiDefenseProjectMapper.selectOne(queryWrapper);
        boolean isEnable = defenseProject != null && defenseProject.getIsEnable() == 0;

        List<SyncTaskStatisticsVO> finalResult = new ArrayList<>();

        // 预先查询各类型的基础数据量（无日志时使用）
        List<BaseBuildUnit> buildUnitList = getBuildUnitList(tenantId);
        List<BaseBuild> buildList = getBuildList(tenantId);
        List<Integer> buildIdList = buildList.stream().map(BaseBuild::getId).collect(Collectors.toList());
        List<BaseBuildPlane> buildPlaneList = buildIdList.isEmpty() ? Collections.emptyList() : getBuildPlaneList(buildIdList);
        List<BaseBuildFacility> buildFacilityList = getBuildFacilityList(tenantId);

        if (CollectionUtils.isEmpty(logList)) {
            fillEmptyStatistics(finalResult, tenantId, isEnable);
            return finalResult;
        }

        List<CdiDeliveryLog> validLogList = logList.stream()
                .filter(logEntity -> logEntity != null
                        && StrUtil.isNotBlank(logEntity.getInfoContent())
                        && logEntity.getDataType() != null)
                .collect(Collectors.toList());

        if (CollectionUtils.isEmpty(validLogList)) {
            log.warn("租户ID:{} 查询到{}条日志，但所有日志的JSON内容为空或dataType为空", tenantId, logList.size());
            fillEmptyStatistics(finalResult, tenantId, isEnable);
            return finalResult;
        }

        // ★ 改动1：按 dataType 分组，保留每种类型的全部日志
        Map<Integer, List<CdiDeliveryLog>> allLogsByType = validLogList.stream()
                .collect(Collectors.groupingBy(CdiDeliveryLog::getDataType));

        for (int type = 1; type <= 5; type++) {
            List<CdiDeliveryLog> logsForType = allLogsByType.get(type);

            // 创建基础 VO，设置 dataType 和 topic
            SyncTaskStatisticsVO vo = createEmptyVO(type, isEnable);

            // ★ total 始终从数据库查询获取（单元表、平面图表等），与日志记录无关
            switch (type) {
                case 1:
                    vo.setTotal(buildUnitList.size());
                    break;
                case 2:
                    vo.setTotal(buildPlaneList.size());
                    break;
                case 3:
                case 4:
                    vo.setTotal(buildFacilityList.isEmpty() ? 0 : buildFacilityList.size());
                    break;
                case 5:
                default:
                    break;
            }

            if (logsForType != null && !logsForType.isEmpty()) {
                // ★ 有日志时：按类型累加 success / fail / notSynced
                int sumSuccess = 0, sumFail = 0, sumNotSynced = 0;

                for (CdiDeliveryLog logEntry : logsForType) {
                    try {
                        // ★ infoContent 是 JSON 对象，用 parseObj 解析
                        JSONObject o = JSONUtil.parseObj(logEntry.getInfoContent());
                        sumSuccess += o.getInt("successNumber");
                        sumFail += o.getInt("failNumber");
                        sumNotSynced += o.getInt("notSynced");
                    } catch (Exception ignored) {
                        // 单条解析失败不影响其他日志的累加
                    }
                }

                vo.setSuccessNumber(sumSuccess);
                vo.setFailNumber(sumFail);
                vo.setNotSynced(sumNotSynced);

                // 取最新一条日志作为元数据来源（createTime / costTime 等）
                CdiDeliveryLog latestLog = logsForType.stream()
                        .max(Comparator.comparingLong(CdiDeliveryLog::getId))
                        .orElse(null);

                if (latestLog != null) {
                    try {
                        // ★ infoContent 是 JSON 对象，用 parseObj 解析
                        JSONObject obj = JSONUtil.parseObj(latestLog.getInfoContent());
                        vo.setId(obj.getLong("id"));
                        vo.setCreateTime(obj.getStr("createTime"));
                        vo.setCostTime(obj.getFloat("costTime"));
                        vo.setState(isEnable ? 1 : obj.getInt("state"));
                    } catch (Exception ex) {
                        log.error("租户ID:{} dataType:{} 解析最新日志元数据失败", tenantId, type, ex);
                    }
                }

                log.info("租户ID:{} dataType:{} 共{}条日志, 聚合结果: total={}, success={}, fail={}, notSynced={}",
                        tenantId, type, logsForType.size(), vo.getTotal(), sumSuccess, sumFail, sumNotSynced);

            } else {
                // ★ 无日志时：success/fail/notSynced 保持为0（createEmptyVO已设置）
            }

            finalResult.add(vo);
        }

        finalResult.sort(Comparator.comparingInt(SyncTaskStatisticsVO::getDataType));
        return finalResult;
    }

    private void fillEmptyStatistics(List<SyncTaskStatisticsVO> result, Integer tenantId, boolean isEnable) {
        List<BaseBuildUnit> buildUnitList = getBuildUnitList(tenantId);
        SyncTaskStatisticsVO vo1 = createEmptyVO(1, isEnable);
        vo1.setTotal(buildUnitList.size());
        result.add(vo1);

        List<Integer> buildIdList = getBuildList(tenantId).stream().map(BaseBuild::getId).collect(Collectors.toList());
        List<BaseBuildPlane> buildPlaneList = buildIdList.isEmpty() ? Collections.emptyList() : getBuildPlaneList(buildIdList);
        SyncTaskStatisticsVO vo2 = createEmptyVO(2, isEnable);
        vo2.setTotal(buildPlaneList.size());
        result.add(vo2);

        List<BaseBuildFacility> buildFacilityList = getBuildFacilityList(tenantId);
        SyncTaskStatisticsVO vo3 = createEmptyVO(3, isEnable);
        vo3.setTotal(buildFacilityList.isEmpty() ? 0 : buildFacilityList.size());
        result.add(vo3);

        SyncTaskStatisticsVO vo4 = createEmptyVO(4, isEnable);
        vo4.setTotal(buildFacilityList.isEmpty() ? 0 : buildFacilityList.size());
        result.add(vo4);

        SyncTaskStatisticsVO vo5 = createEmptyVO(5, isEnable);
        result.add(vo5);
    }

    private SyncTaskStatisticsVO createEmptyVO(int dataType, boolean isEnable) {
        SyncTaskStatisticsVO vo = new SyncTaskStatisticsVO();
        vo.setDataType(dataType);
        vo.setSuccessNumber(0);
        vo.setFailNumber(0);
        vo.setNotSynced(0);
        vo.setState(isEnable ? 1 : 0);

        switch (dataType) {
            case 1:
                vo.setDataTypeName(MqttTopics.Base.PROTECTIVE_UNIT.getDesc());
                vo.setTopic(MqttTopics.Base.PROTECTIVE_UNIT.getTopic());
                break;
            case 2:
                vo.setDataTypeName(MqttTopics.Base.FLOOR_PLANE.getDesc());
                vo.setTopic(MqttTopics.Base.FLOOR_PLANE.getTopic());
                break;
            case 3:
                vo.setDataTypeName(MqttTopics.Base.SENSOR_INFO.getDesc());
                vo.setTopic(MqttTopics.Base.SENSOR_INFO.getTopic());
                break;
            case 4:
                vo.setDataTypeName(MqttTopics.IotInfo.MONITORING_DATA.getDesc());
                vo.setTopic(MqttTopics.IotInfo.MONITORING_DATA.getTopic());
                break;
            case 5:
                vo.setDataTypeName(MqttTopics.Alarm.MESSAGE.getDesc());
                vo.setTopic(MqttTopics.Alarm.MESSAGE.getTopic());
                break;
        }
        return vo;
    }

    private SyncTaskStatisticsVO parseSyncStatisticsVO(JSONObject obj, boolean isEnable) {
        SyncTaskStatisticsVO vo = new SyncTaskStatisticsVO();
        vo.setId(obj.getLong("id"));
        vo.setTopic(obj.getStr("topic"));

        int dataType = obj.getInt("dataType");
        vo.setDataType(dataType);

        switch (dataType) {
            case 1:
                vo.setDataTypeName(MqttTopics.Base.PROTECTIVE_UNIT.getDesc());
                break;
            case 2:
                vo.setDataTypeName(MqttTopics.Base.FLOOR_PLANE.getDesc());
                break;
            case 3:
                vo.setDataTypeName(MqttTopics.Base.SENSOR_INFO.getDesc());
                break;
            case 4:
                vo.setDataTypeName(MqttTopics.IotInfo.MONITORING_DATA.getDesc());
                break;
            case 5:
                vo.setDataTypeName(MqttTopics.Alarm.MESSAGE.getDesc());
                break;
        }

        vo.setTotal(obj.getInt("total"));
        vo.setSuccessNumber(obj.getInt("successNumber"));
        vo.setFailNumber(obj.getInt("failNumber"));
        vo.setNotSynced(obj.getInt("notSynced"));
        vo.setCreateTime(obj.getStr("createTime"));
        vo.setCostTime(obj.getFloat("costTime"));
        vo.setState(isEnable ? 1 : obj.getInt("state"));

        return vo;
    }

    @Override
    public CommonPage<CdiDeliveryLog> logList(Long id, Integer pageNum, Integer pageSize, Integer dataType, Integer
            logType, String startTime, String endTime) {
        IPage<CdiDeliveryLog> page = new Page<>(pageNum, pageSize);

        LambdaQueryWrapper<SysUser> queryUser = new LambdaQueryWrapper<>();
        queryUser.eq(SysUser::getTenantId, SecurityUtils.getTenantId());
        Map<String, String> userMap = sysUserMapper.selectList(queryUser).stream().collect(
                Collectors.toMap(SysUser::getUserName, SysUser::getNickName)
        );

        LambdaQueryWrapper<CdiDeliveryLog> queryWrapper = new LambdaQueryWrapper<>();
        if (id != null) {
            queryWrapper.eq(CdiDeliveryLog::getId, id);
            page = this.page(page, queryWrapper);
        } else {
            LocalDateTime start = StrUtil.isNotBlank(startTime) ? LocalDateTime.parse(startTime, DATETIME_FORMATTER) : null;
            LocalDateTime end = StrUtil.isNotBlank(endTime) ? LocalDateTime.parse(endTime, DATETIME_FORMATTER) : null;

            queryWrapper.eq(CdiDeliveryLog::getTenantId, SecurityUtils.getTenantId())
                    .eq(dataType != null, CdiDeliveryLog::getDataType, dataType)
                    .eq(logType != null, CdiDeliveryLog::getPushFlag, logType)
                    .between(start != null && end != null && start.isBefore(end),
                            CdiDeliveryLog::getCreateTime, start, end)
                    .orderByDesc(CdiDeliveryLog::getCreateTime);
            page = this.page(page, queryWrapper);
        }
        page.getRecords().forEach(log -> {
            log.setNickName(userMap.getOrDefault(log.getUserName(), log.getUserName()));
            log.setDataTypeName((MqttTopics.getDescByTopic(log.getTopic())));
        });

        return new CommonPage<>(page.getRecords(), page.getTotal(), pageSize, pageNum);
    }

    /**
     * 获取建筑设施列表
     *
     * @param tenantId 租户ID
     * @return 结果列表
     */
    private List<BaseBuildFacility> getBuildFacilityList(Integer tenantId) {
        LambdaQueryWrapper<BaseBuildFacility> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(BaseBuildFacility::getTenantId, tenantId)
                .eq(BaseBuildFacility::getDeleteFlag, 0);
        return baseBuildFacilityMapper.selectList(queryWrapper);
    }

    /**
     * 获取建筑单元列表
     *
     * @param tenantId 租户ID
     * @return 结果列表
     */
    private List<BaseBuildUnit> getBuildUnitList(Integer tenantId) {
        LambdaQueryWrapper<BaseBuildUnit> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(BaseBuildUnit::getTenantId, tenantId);
        return baseBuildUnitMapper.selectList(queryWrapper);
    }

    /**
     * 获取建筑列表
     *
     * @param tenantId 租户ID
     * @return 列表
     */
    private List<BaseBuild> getBuildList(Integer tenantId) {
        LambdaQueryWrapper<BaseBuild> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(BaseBuild::getTenantId, tenantId);
        return baseBuildMapper.selectList(queryWrapper);
    }

    /**
     * 获取建筑平面列表
     *
     * @param buildIds 建筑ID列表
     * @return 结果列表
     */
    private List<BaseBuildPlane> getBuildPlaneList(List<Integer> buildIds) {
        LambdaQueryWrapper<BaseBuildPlane> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.in(BaseBuildPlane::getBuildId, buildIds);
        return baseBuildPlaneMapper.selectList(queryWrapper);
    }

    /**
     * 同步数据
     *
     * @param vo 同步任务统计信息
     * @return null
     */
    @Override
    public void synchronizeData(SyncTaskStatisticsVO vo) {

        if (vo.getDataType() == null) {
            throw new BusinessException("数据类型不能为空！");
        } else if (vo.getDataType() < 1 || vo.getDataType() > 4) {
            throw new BusinessException("同步数据类型错误！");
        }

        LambdaQueryWrapper<CdiDefenseProject> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(CdiDefenseProject::getTenantId, SecurityUtils.getTenantId());
        CdiDefenseProject one = cdiDefenseProjectMapper.selectOne(queryWrapper);

        if (one == null) {
            throw new BusinessException("未找到对应的工程配置！无法同步");
        } else if (!one.getIsEnable().equals(1)) {
            throw new BusinessException("工程配置未启用！无法同步");
        }

        Long engineeringId = one.getEngineeringId();
        String username = one.getMqttUserName();
        String password = one.getMqttPassword();
        Integer tenantId = one.getTenantId();
        LocalDateTime now = LocalDateTime.now();

        String userName = "自动同步";
        try {
            userName = SecurityUtils.getUsername();
        } catch (Exception e) {
            log.error("无法获取用户名或姓名，使用默认‘自动同步’", e);
        }

        switch (vo.getDataType()) {
            // 单元信息
            case 1:
                log.info("开始同步单元信息");

                long startTime, endTime;
                startTime = System.currentTimeMillis();
                int total = 0, success = 0, failure = 0, notSynced = 0;

                List<ProtectiveUnitVO> unitVOS = synchronizeBuildUnit(tenantId, engineeringId);
                total = unitVOS.size();
                mqttConnectionTool.connectOrRefresh(username, password);
                iotDataTransferService.createMqttConnection(username, password);
                String topic = MqttTopics.Base.PROTECTIVE_UNIT.getTopic();
                String desc = MqttTopics.Base.PROTECTIVE_UNIT.getDesc();

                for (ProtectiveUnitVO unitVO : unitVOS) {
                    try {
                        iotDataTransferService.sendMqttMessage(topic, unitVO, desc, username);
                        success++;
                    } catch (Exception e) {
                        log.error("同步单元信息失败！", e);
                        failure++;
                    }
                }
                endTime = System.currentTimeMillis();

                notSynced = total - success - failure;
                saveLog(topic, desc, 1, tenantId, engineeringId, now, startTime, endTime, total, success, failure, notSynced, failure > 0 ? 0 : 1, userName);

                break;
            // 平面图信息
            case 2:
                log.info("开始同步楼层平面图信息");

                long startTime2, endTime2;
                startTime2 = System.currentTimeMillis();
                int total2 = 0, success2 = 0, failure2 = 0, notSynced2 = 0;

                List<FloorPlaneVO> floorPlaneVOS = buildPlanes(tenantId, engineeringId);
                if (CollUtil.isEmpty(floorPlaneVOS)) {
                    log.error("未找到楼层平面图信息！");
                    break;
                }
                total2 = floorPlaneVOS.size();
                iotDataTransferService.createMqttConnection(username, password);
                String topic1 = MqttTopics.Base.FLOOR_PLANE.getTopic();
                String desc1 = MqttTopics.Base.FLOOR_PLANE.getDesc();


                for (FloorPlaneVO floorPlaneVO : floorPlaneVOS) {
                    try {
                        iotDataTransferService.sendMqttMessage(topic1, floorPlaneVO, desc1, username);
                        success2++;
                    } catch (Exception e) {
                        log.error("同步楼层平面图信息失败！", e);
                        failure2++;
                    }
                }
                endTime2 = System.currentTimeMillis();
                notSynced2 = total2 - success2 - failure2;
                saveLog(topic1, desc1, 2, tenantId, engineeringId, now, startTime2, endTime2, total2, success2,
                        failure2, notSynced2, failure2 > 0 ? 0 : 1, userName);

                break;
            // 推送设施信息
            case 3:
                log.info("开始同步设施信息");

                long startTime3, endTime3;
                startTime3 = System.currentTimeMillis();

                Map<String, Integer> map = baseDataTransferService.batchSendSensorInfos(tenantId, engineeringId, username, password);

                endTime3 = System.currentTimeMillis();
                saveLog(MqttTopics.Base.SENSOR_INFO.getTopic(), MqttTopics.Base.SENSOR_INFO.getDesc(), 3, tenantId, engineeringId,
                        now, startTime3, endTime3, map.get("total"), map.get("success"), map.get("failure"), map.get("notSynced"), map.get("failure") > 0 ? 0 : 1, userName);

                break;
            // 推送监测数据
            case 4:
                log.info("开始同步监测数据");
                iotDataTransferService.synchronizeDeviceData(tenantId, engineeringId, username, password);
                // iotDataTransferService.synchronizeDeviceData(1205, 3101070011L, "3101070011", "5RqhJ7VG");
                break;
        }
    }

    // 存储日志
    @Override
    @Async("asyncServiceExecutor")
    public void saveLog(String topic, String dataTypeName, Integer dataType, Integer tenantId, Long engineeringId, LocalDateTime now, long startTime, long endTime,
                        int total, int success, int failure, int notSynced, int pushFlag, String userName) {

        SyncTaskStatisticsVO vo = new SyncTaskStatisticsVO();
        vo.setDataType(dataType);
        vo.setTotal(total);
        vo.setSuccessNumber(success);
        vo.setFailNumber(failure);
        vo.setNotSynced(notSynced);
        vo.setCreateTime(now.format(DATETIME_FORMATTER));
        vo.setCostTime((float) (endTime - startTime) / 1000.0f);
        vo.setState(1);
        vo.setTopic(topic);
        vo.setDataTypeName(dataTypeName);

        CdiDeliveryLog log = new CdiDeliveryLog();
        log.setEngineeringId(engineeringId);
        log.setDataType(dataType);
        log.setTopic(topic);
        log.setDataTypeName(vo.getDataTypeName());
        log.setUserName(userName);
        log.setCreateTime(now);
        log.setTenantId(tenantId);
        log.setPushFlag(pushFlag);
        log.setInfoContent(JSON.toJSONString(vo));

        this.save(log);
    }

    // 单元信息
    private List<ProtectiveUnitVO> synchronizeBuildUnit(Integer tenantId, Long engineeringId) {
        List<BaseBuildUnit> buildUnitList = getBuildUnitList(tenantId);
        if (CollUtil.isEmpty(buildUnitList)) {
            return Collections.emptyList();
        }

        String time = DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss.SSS");

        List<ProtectiveUnitVO> result = new ArrayList<>(buildUnitList.size());
        for (BaseBuildUnit buildUnit : buildUnitList) {
            ProtectiveUnitVO vo = new ProtectiveUnitVO();
            vo.setDataPacketID(generateDataPacketID());
            vo.setEngineeringID(engineeringId);
            vo.setUnitName(buildUnit.getUnitName());
            vo.setFloor(buildUnit.getFloor());
            vo.setUnitArea(buildUnit.getUnitArea() != null
                    ? BigDecimal.valueOf(buildUnit.getUnitArea())
                    : BigDecimal.ZERO);
            vo.setUnitUsage(buildUnit.getUnitUsage());
            vo.setPeopleNumber(buildUnit.getPeopleNumber());
            vo.setUnitmainexit(buildUnit.getUnitMainExit());
            vo.setUnitotherexit(buildUnit.getUnitOtherExit());
            vo.setPublishTime(time);
            result.add(vo);
        }

        return result;
    }

    // 平面图信息
    private List<FloorPlaneVO> buildPlanes(Integer tenantId, Long engineeringId) {
        List<BaseBuild> buildList = getBuildList(tenantId);
        if (CollUtil.isEmpty(buildList)) {
            return Collections.emptyList();
        }

        List<Integer> buildIds = buildList.stream().map(BaseBuild::getId).collect(Collectors.toList());
        List<BaseBuildPlane> buildPlaneList = getBuildPlaneList(buildIds);

        if (CollUtil.isEmpty(buildPlaneList)) {
            return Collections.emptyList();
        }

        Map<String, BaseBuildPlane> latestPlaneByFloor = buildPlaneList.stream()
                .collect(Collectors.groupingBy(
                        BaseBuildPlane::getFloor,
                        Collectors.collectingAndThen(
                                Collectors.maxBy(Comparator.comparingInt(BaseBuildPlane::getId)),
                                opt -> opt.orElse(null)
                        )
                ));

        List<BaseBuildPlane> filteredPlaneList = latestPlaneByFloor.values().stream()
                .filter(Objects::nonNull)
                .sorted(Comparator.comparing(BaseBuildPlane::getFloor))
                .collect(Collectors.toList());

        log.info("楼层平面图数据过滤：原始{}条，按楼层去重后{}条", buildPlaneList.size(), filteredPlaneList.size());

        String time = DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss.SSS");

        List<FloorPlaneVO> result = new ArrayList<>(filteredPlaneList.size());
        int skipCount = 0;

        for (BaseBuildPlane buildPlane : filteredPlaneList) {
            try {
                String planeViewUrl = buildPlane.getPlaneViewUrl();

                // ✅ 修复1: 增加详细的处理日志
                log.info("正在处理楼层平面图：ID={}, Floor={}, URL={}",
                        buildPlane.getId(), buildPlane.getFloor(), planeViewUrl);

                FloorPlaneVO vo = new FloorPlaneVO();

                // ✅ 修复2: 单条记录异常不中断整个批次
                if (StrUtil.isBlank(planeViewUrl)) {
                    log.warn("楼层平面图URL为空，跳过该记录：ID={}, Floor={}", buildPlane.getId(), buildPlane.getFloor());
                    skipCount++;
                    continue;
                }

                // 读取文件并校验大小
                checkFileSize(vo, planeViewUrl);

                // ✅ 修复3: 复用已读取的文件数据来获取图片尺寸（避免重复I/O）
                fillImageInfoWithBytes(vo, planeViewUrl, vo.getFloorFile());

                // 设置基础字段
                vo.setDataPacketID(generateDataPacketID());
                vo.setEngineeringID(engineeringId);
                vo.setFloor(convertFloor(buildPlane.getFloor()));
                vo.setFloorFileID(Long.valueOf(buildPlane.getId()));
                vo.setPublishTime(time);

                // ✅ 修复4: 校验关键字段完整性
                if (vo.getFloorFile() == null || vo.getFloorFile().length == 0) {
                    log.warn("楼层平面图文件为空，跳过该记录：ID={}, Floor={}", buildPlane.getId(), buildPlane.getFloor());
                    skipCount++;
                    continue;
                }

                // ✅ 修复5: 记录成功处理的详细信息
                log.info("楼层平面图处理成功：FileID={}, Floor={}, FileName={}, FileSize={}bytes, Pix={}x{}",
                        vo.getFloorFileID(),
                        vo.getFloor(),
                        vo.getFloorFileName(),
                        vo.getFloorFile().length,
                        vo.getFilePixWidth(),
                        vo.getFilePixHeight());

                result.add(vo);

            } catch (BusinessException e) {
                log.error("楼层平面图业务校验失败，跳过该记录：ID={}, Floor={}, 错误={}",
                        buildPlane.getId(), buildPlane.getFloor(), e.getMessage());
                skipCount++;
            } catch (Exception e) {
                log.error("楼层平面图处理异常，跳过该记录：ID={}, Floor={}",
                        buildPlane.getId(), buildPlane.getFloor(), e);
                skipCount++;
            }
        }

        if (skipCount > 0) {
            log.warn("楼层平面图处理完成：成功{}条，跳过{}条（文件不存在/超限/读取失败等）",
                    result.size(), skipCount);
        }

        return result;
    }

    // 校验文件大小不超过5MB，然后存入vo中
    private static final long MAX_FILE_SIZE_MB = 5;
    private static final long MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024;

    private void checkFileSize(FloorPlaneVO vo, String filePath) {
        Assert.notBlank(filePath, "文件路径不能为空");

        byte[] fileBytes;

        if (filePath.startsWith("http://") || filePath.startsWith("https://")) {
            fileBytes = downloadFileFromUrl(filePath);
        } else {
            File localFile = new File(filePath);
            if (!localFile.exists()) {
                log.warn("本地文件不存在: {}", filePath);
                throw new BusinessException("楼层平面图文件不存在：" + filePath);
            }

            long size = FileUtil.size(localFile);
            if (size > MAX_FILE_SIZE_BYTES) {
                double sizeMB = size / 1024.0 / 1024.0;
                throw new BusinessException(
                        StrUtil.format("楼层平面图大小超过{}MB！当前：{:.2f}MB", MAX_FILE_SIZE_MB, sizeMB)
                );
            }
            fileBytes = FileUtil.readBytes(localFile);
        }

        vo.setFloorFile(fileBytes);
    }

    private byte[] downloadFileFromUrl(String fileUrl) {
        try {
            String encodedUrl = encodeUrl(fileUrl);
            URL url = new URL(encodedUrl);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod("GET");
            conn.setConnectTimeout(5000);
            conn.setReadTimeout(10000);
            conn.setRequestProperty("User-Agent", "Mozilla/5.0");

            int responseCode = conn.getResponseCode();
            if (responseCode != HttpURLConnection.HTTP_OK) {
                log.error("下载文件失败，HTTP状态码: {}, URL: {}", responseCode, fileUrl);
                throw new BusinessException("下载楼层平面图失败，服务器返回状态码：" + responseCode);
            }

            int contentLength = conn.getContentLength();
            if (contentLength > MAX_FILE_SIZE_BYTES) {
                double sizeMB = contentLength / 1024.0 / 1024.0;
                throw new BusinessException(
                        StrUtil.format("楼层平面图大小超过{}MB！当前：{:.2f}MB", MAX_FILE_SIZE_MB, sizeMB)
                );
            }

            try (InputStream inputStream = conn.getInputStream()) {
                return IoUtil.readBytes(inputStream);
            }
        } catch (BusinessException e) {
            throw e;
        } catch (IOException e) {
            log.error("从URL下载文件失败: {}", fileUrl, e);
            throw new BusinessException("下载楼层平面图失败：" + e.getMessage());
        }
    }

    private String convertFloor(String floor) {
        if (floor == null || floor.trim().isEmpty()) {
            return floor;
        }
        try {
            int floorNum = Integer.parseInt(floor.trim());
            if (floorNum < 0) {
                return "B" + Math.abs(floorNum);
            }
        } catch (NumberFormatException e) {
            log.warn("楼层格式转换失败，原始值: {}", floor);
        }
        return floor;
    }

    private String encodeUrl(String url) throws UnsupportedEncodingException {
        if (StrUtil.isBlank(url)) {
            return url;
        }

        if (!(url.startsWith("http://") || url.startsWith("https://"))) {
            return url;
        }

        int protocolEnd = url.indexOf("://");
        String protocol = url.substring(0, protocolEnd + 3);
        String rest = url.substring(protocolEnd + 3);

        int firstSlash = rest.indexOf("/");
        if (firstSlash == -1) {
            return url;
        }

        String hostAndPort = rest.substring(0, firstSlash);
        String path = rest.substring(firstSlash);

        String[] pathSegments = path.split("/");
        StringBuilder encodedPath = new StringBuilder();
        for (int i = 0; i < pathSegments.length; i++) {
            if (i > 0) {
                encodedPath.append("/");
            }
            encodedPath.append(URLEncoder.encode(pathSegments[i], "UTF-8"));
        }

        return protocol + hostAndPort + encodedPath.toString();
    }

    /**
     * 填充图片信息（使用已读取的字节数组，避免重复I/O）
     * @param vo 楼层平面图VO
     * @param imageUrl 图片URL或本地路径（用于提取文件名和后缀）
     * @param imageBytes 已读取的图片字节数组
     */
    private void fillImageInfoWithBytes(FloorPlaneVO vo, String imageUrl, byte[] imageBytes) {
        if (StrUtil.isBlank(imageUrl)) {
            return;
        }

        // 从URL/路径中提取文件名和后缀
        String fileName = FileUtil.getName(imageUrl);
        vo.setFloorFileName(FileUtil.mainName(fileName));
        vo.setFloorFileSuffix(FileUtil.extName(fileName));

        // ✅ 优化：直接从已读取的byte[]获取尺寸，避免重复读取文件
        try {
            if (imageBytes != null && imageBytes.length > 0) {
                ByteArrayInputStream bais = new ByteArrayInputStream(imageBytes);
                BufferedImage image = ImageIO.read(bais);

                if (image != null) {
                    vo.setFilePixWidth(image.getWidth());
                    vo.setFilePixHeight(image.getHeight());
                }
            }
        } catch (IOException e) {
            log.error("从字节数组获取图片尺寸失败: {}", imageUrl, e);
        }
    }

    private void fillImageInfo(FloorPlaneVO vo, String imageUrl) {
        if (StrUtil.isBlank(imageUrl)) {
            return;
        }

        String fileName = FileUtil.getName(imageUrl);
        vo.setFloorFileName(FileUtil.mainName(fileName));
        vo.setFloorFileSuffix(FileUtil.extName(fileName));

        try {
            BufferedImage image;
            if (imageUrl.startsWith("http://") || imageUrl.startsWith("https://")) {
                String encodedUrl = encodeUrl(imageUrl);
                URL url = new URL(encodedUrl);
                URLConnection conn = url.openConnection();
                conn.setConnectTimeout(3000);
                conn.setReadTimeout(5000);

                try (InputStream in = conn.getInputStream()) {
                    image = ImageIO.read(in);
                }
            } else {
                File imageFile = new File(imageUrl);
                if (imageFile.exists()) {
                    image = ImageIO.read(imageFile);
                } else {
                    log.warn("图片文件不存在: {}", imageUrl);
                    vo.setFilePixWidth(7016);
                    vo.setFilePixHeight(9933);
                    return;
                }
            }

            if (image != null) {
                vo.setFilePixWidth(image.getWidth());
                vo.setFilePixHeight(image.getHeight());
            } else {
                log.warn("无法读取图片尺寸: {}", imageUrl);
                vo.setFilePixWidth(7016);
                vo.setFilePixHeight(9933);
            }
        } catch (IOException e) {
            log.error("获取图片尺寸失败: {}", imageUrl, e);
            vo.setFilePixWidth(7016);
            vo.setFilePixHeight(9933);
        }
    }


}
