package com.usky.ems.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.usky.common.core.bean.ApiResult;
import com.usky.common.core.exception.BusinessException;
import com.usky.common.security.utils.SecurityUtils;
import com.usky.demo.RemoteTsdbProxyService;
import com.usky.demo.domain.HistorysInnerRequestVO;
import com.usky.demo.domain.HistorysInnerResultVO;
import com.usky.demo.domain.MetricVO;
import com.usky.ems.domain.*;
import com.usky.ems.mapper.*;
import com.usky.ems.service.EmsReportService;
import com.usky.ems.service.vo.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.BorderStyle;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.FillPatternType;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.IndexedColors;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.VerticalAlignment;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 统计报表服务实现（设备列表基于 leo 设备与属性点位，统计与导出为占位/预留）
 */
@Service
@Slf4j
public class EmsReportServiceImpl implements EmsReportService {

    private static final String[] ENERGY_TYPE_NAMES = {"", "电", "水", "气"};

    private static final String ENERGY_NO_DATA = "-";
    private static final String ENERGY_ZERO = "0.00";

    private static final Set<String> CUMULATIVE_ENERGY_METRICS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
            "totalactiveenergyf",
            "totalactiveenergyr",
            "totalreactiveenergyf",
            "totalreactiveenergyr"
    )));

    private enum MetricAggregateType {
        /** 累计表计：时段内末次读数 - 首次读数 */
        CUMULATIVE_DELTA,
        /** 瞬时量：时段内算术平均 */
        INSTANTANEOUS_AVG
    }

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

    @Autowired
    private DmpDeviceMapper dmpDeviceMapper;
    @Autowired
    private EmsDeviceFunctionMapper emsDeviceFunctionMapper;
    @Autowired
    private RemoteTsdbProxyService remoteTsdbProxyService;
    @Autowired
    private BaseSpaceGatewayMapper baseSpaceGatewayMapper;
    @Autowired
    private EmsDeviceItemCodeMapper emsDeviceItemCodeMapper;
    @Autowired
    private EmsProductEnergyTypeMapper emsProductEnergyTypeMapper;
    @Autowired
    private BaseSpaceMapper baseSpaceMapper;
    @Autowired
    private EmsEnergyItemCodeMapper emsEnergyItemCodeMapper;

    private String energyTypeName(Long energyTypeId) {
        if (energyTypeId == null || energyTypeId < 1 || energyTypeId > 3) return "";
        return ENERGY_TYPE_NAMES[energyTypeId.intValue()];
    }

    @Override
    public EmsReportDevicesResponse getEnergyDevices(Long energyTypeId, String keyword, Long projectId) {
        LambdaQueryWrapper<DmpDevice> q = new LambdaQueryWrapper<DmpDevice>().eq(DmpDevice::getDeleteFlag, 0).eq(DmpDevice::getTenantId, SecurityUtils.getTenantId());
        if (StringUtils.hasText(keyword)) {
            q.and(w -> w.like(DmpDevice::getDeviceName, keyword));
        }
        List<DmpDevice> devices = dmpDeviceMapper.selectList(q);
        return buildReportDevicesResponse(devices, energyTypeId, null);
    }

    @Override
    public EmsEnergyStatisticsResponse getEnergyStatistics(EmsEnergyStatisticsRequest request) {
        EmsEnergyStatisticsResponse resp = new EmsEnergyStatisticsResponse();
        if (request != null) {
            resp.setTimeDimension(request.getTimeDimension());
            resp.setTimeValue(request.getTimeValue());
        }
        return resp;
    }

    @Override
    public void exportEnergy(String deviceIds, String attributePointIds, String timeDimension, String timeValue, String format, HttpServletResponse response) {
        doExport(response, format, "energy_report");
    }

    @Override
    public EmsReportDevicesResponse getRegionDevices(Long energyTypeId, Long regionId, String keyword, Long projectId) {
//        LambdaQueryWrapper<DmpDevice> q = new LambdaQueryWrapper<DmpDevice>().eq(DmpDevice::getDeleteFlag,0).eq(DmpDevice::getTenantId, SecurityUtils.getTenantId());
//        if (regionId != null) q.eq(EmsDevice::getInstallationLocation, regionId);
//        if (StringUtils.hasText(keyword)) {
//            q.and(w -> w.like(EmsDevice::getName, keyword).or().like(EmsDevice::getNumber, keyword));
//        }
//        List<DmpDevice> devices = dmpDeviceMapper.selectList(q);
        List<DmpDevice> devices = new ArrayList<>();
        return buildReportDevicesResponse(devices, energyTypeId, regionId);
    }

    @Override
    public EmsEnergyStatisticsResponse getRegionStatistics(EmsEnergyStatisticsRequest request, List<Long> regionIds) {
        return getEnergyStatistics(request);
    }

    @Override
    public void exportRegion(String deviceIds, String attributePointIds, String timeDimension, String timeValue, String regionIds, String format, HttpServletResponse response) {
        doExport(response, format, "region_report");
    }

    @Override
    public EmsReportDevicesResponse getCollectionDevices(Long energyTypeId, String keyword, Long projectId) {
        return getEnergyDevices(energyTypeId, keyword, projectId);
    }

    @Override
    public EmsCollectionRealtimeResponse getCollectionRealtime(EmsCollectionRealtimeRequest request) {
        EmsCollectionRealtimeResponse resp = new EmsCollectionRealtimeResponse();
        return resp;
    }

    @Override
    public EmsEnergyStatisticsResponse getCollectionStatistics(EmsEnergyStatisticsRequest request) {
        return getEnergyStatistics(request);
    }

    @Override
    public void exportCollection(String deviceIds, String attributePointIds, String timeDimension, String timeValue, String format, HttpServletResponse response) {
        doExport(response, format, "collection_report");
    }

    private EmsReportDevicesResponse buildReportDevicesResponse(List<DmpDevice> devices, Long energyTypeId, Long regionId) {
        EmsReportDevicesResponse resp = new EmsReportDevicesResponse();
        String typeName = energyTypeName(energyTypeId);
        for (DmpDevice d : devices) {
            EmsReportDeviceItemVO item = new EmsReportDeviceItemVO();
            item.setId(String.valueOf(d.getId()));
            item.setName(d.getDeviceName());
            item.setCode(d.getDeviceId());
            item.setEnergyTypeName(typeName);
            List<EmsDeviceFunction> funcs = emsDeviceFunctionMapper.selectList(
                    new LambdaQueryWrapper<EmsDeviceFunction>().eq(EmsDeviceFunction::getDeviceId, d.getId()));
            List<EmsReportAttributeVO> attrs = funcs.stream().map(f -> {
                EmsReportAttributeVO a = new EmsReportAttributeVO();
                a.setId(f.getId());
                a.setName(f.getName());
                a.setCode(f.getIdentifier());
                a.setUnit("");
                return a;
            }).collect(Collectors.toList());
            item.setAttributes(attrs);
            resp.getDevices().add(item);
        }
        return resp;
    }

    private void doExport(HttpServletResponse response, String format, String fileName) {
        try {
            if ("csv".equalsIgnoreCase(format != null ? format : "excel")) {
                response.setContentType("text/csv;charset=UTF-8");
                response.setHeader("Content-Disposition", "attachment; filename=" + fileName + ".csv");
                try (OutputStream os = response.getOutputStream()) {
                    os.write("\uFEFF".getBytes(StandardCharsets.UTF_8));
                    os.write("时间维度,时间值\n".getBytes(StandardCharsets.UTF_8));
                }
            } else {
                response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
                response.setHeader("Content-Disposition", "attachment; filename=" + fileName + ".xlsx");
                response.getOutputStream().flush();
            }
        } catch (IOException e) {
            throw new RuntimeException("导出失败", e);
        }
    }

    @Override
    public EnergyReportResponse getEnergyReport(EnergyReportRequest request) {
        if (request == null || request.getDeviceList() == null || request.getDeviceList().isEmpty()) {
            return createEmptyEnergyReport();
        }

        // 解析时间
        LocalDateTime startTime = parseDateTime(request.getStartTime());
        LocalDateTime endTime = parseDateTime(request.getEndTime());
        if (startTime == null || endTime == null) {
            return createEmptyEnergyReport();
        }

        // 校验时间参数
        validateTimeParams(startTime, endTime, request.getTimeType());

        // 生成列定义
        List<EnergyReportResponse.ColumnItem> columnList = generateEnergyReportColumns(request.getTimeType(), startTime, endTime);

        // 生成数据列表（按设备分组的二维数组）
        List<List<EnergyReportResponse.ValueItem>> valueList = generateEnergyReportData(request, startTime, endTime);

        EnergyReportResponse response = new EnergyReportResponse();
        response.setColumnList(columnList);
        response.setValueList(valueList);

        return response;
    }

    @Override
    public ItemReportResponse getItemReport(ItemReportRequest request) {
        if (request == null || request.getItemCodes() == null || request.getItemCodes().isEmpty()) {
            return createEmptyItemReport();
        }

        // 解析时间
        LocalDateTime startTime = parseDateTime(request.getStartTime());
        LocalDateTime endTime = parseDateTime(request.getEndTime());
        if (startTime == null || endTime == null) {
            return createEmptyItemReport();
        }

        // 校验时间参数
        validateTimeParams(startTime, endTime, request.getTimeType());

        // 生成标题列
        List<ItemReportResponse.TitleItem> titleList = generateItemReportTitles(request.getTimeType(), startTime, endTime);

        // 生成数据列表
        List<Map<String, Object>> dataList = generateItemReportData(request, startTime, endTime);

        ItemReportResponse response = new ItemReportResponse();
        response.setTitleList(titleList);
        response.setDataList(dataList);

        return response;
    }

    @Override
    public SpaceReportResponse getSpaceReport(SpaceReportRequest request) {
        if (request == null || request.getSpaces() == null || request.getSpaces().isEmpty()) {
            return createEmptySpaceReport();
        }

        // 解析时间
        LocalDateTime startTime = parseDateTime(request.getStartTime());
        LocalDateTime endTime = parseDateTime(request.getEndTime());
        if (startTime == null || endTime == null) {
            return createEmptySpaceReport();
        }

        // 校验时间参数
        validateTimeParams(startTime, endTime, request.getTimeType());

        // 生成列定义
        List<EnergyReportResponse.ColumnItem> columnList = generateSpaceReportColumns(request.getTimeType(), startTime, endTime);

        // 生成数据列表（按区域/空间分组的二维数组）
        List<List<Map<String, Object>>> valueList = generateSpaceReportData(request, startTime, endTime);

        // 计算总计
        Number total = calculateSpaceReportTotal(valueList);

        SpaceReportResponse response = new SpaceReportResponse();
        response.setColumnList(columnList);
        response.setValueList(valueList);
        response.setTotal(total);

        return response;
    }

    @Override
    public void exportEnergyReport(EnergyReportRequest request, HttpServletResponse response) {
        EnergyReportResponse report = getEnergyReport(request);
        List<EnergyReportResponse.ColumnItem> columns = report.getColumnList() != null
                ? report.getColumnList() : Collections.emptyList();
        List<List<EnergyReportResponse.ValueItem>> groupedRows = report.getValueList() != null
                ? report.getValueList() : Collections.emptyList();
        writeEnergyReportExcel(response, buildExportFileName("能源报表"), columns, groupedRows);
    }

    @Override
    public void exportSpaceReport(SpaceReportRequest request, HttpServletResponse response) {
        SpaceReportResponse report = getSpaceReport(request);
        List<EnergyReportResponse.ColumnItem> columns = report.getColumnList() != null
                ? report.getColumnList() : Collections.emptyList();
        List<List<Map<String, Object>>> groupedRows = report.getValueList() != null
                ? report.getValueList() : Collections.emptyList();
        writeSpaceReportExcel(response, buildExportFileName("区域报表"), columns, groupedRows);
    }

    // ==================== 能源报表私有方法 ====================

    /**
     * 生成能源报表列定义
     */
    private List<EnergyReportResponse.ColumnItem> generateEnergyReportColumns(String timeType, LocalDateTime startTime, LocalDateTime endTime) {
        List<EnergyReportResponse.ColumnItem> columns = new ArrayList<>();

        // 固定列
        columns.add(createColumn("设备名称", "deviceName", true));
        columns.add(createColumn("通讯地址", "commAddress", true));
        columns.add(createColumn("功能点", "identifier", true));
        columns.add(createColumn("合计", "total", true));

        // 自定义查询仅返回合计，不生成动态时间列
        if (isCustomTimeType(timeType)) {
            return columns;
        }

        // 动态时间列
        List<String> timeLabels = generateTimeLabels(timeType, startTime, endTime);
        for (String label : timeLabels) {
            columns.add(createColumn(label, "_" + label.replace("时", "").replace("日", "").replace("月", ""), false));
        }

        return columns;
    }

    /**
     * 生成能源报表数据（按设备分组的二维数组）
     * 外层 List 代表不同的设备
     * 内层 List 包含该设备下所有请求的功能点（属性）数据
     */
    private List<List<EnergyReportResponse.ValueItem>> generateEnergyReportData(EnergyReportRequest request, LocalDateTime startTime, LocalDateTime endTime) {
        List<List<EnergyReportResponse.ValueItem>> groupedValueList = new ArrayList<>();

        // 获取设备uuid列表
        List<Integer> deviceIds = request.getDeviceList().stream()
                .filter(device -> device != null && device.getId() != null)
                .map(EnergyReportRequest.DeviceItem::getId)
                .collect(Collectors.toList());
        List<String> deviceUuids = getUuidList(deviceIds);

        // 查询tsdb历史数据
        HistorysInnerRequestVO historyRequestVO = new HistorysInnerRequestVO();
        historyRequestVO.setDeviceuuid(deviceUuids);
        historyRequestVO.setStartTime(request.getStartTime());
        historyRequestVO.setEndTime(request.getEndTime());
        historyRequestVO.setMetrics(request.getFuncList() != null && !request.getFuncList().isEmpty()
                ? request.getFuncList().stream().map(EnergyReportRequest.FuncItem::getIdentifier)
                .map(String::toLowerCase)
                .collect(Collectors.toList())
                : new ArrayList<>());
        log.info("历史数据接口请求：{}", historyRequestVO);
        ApiResult<List<HistorysInnerResultVO>> listHistoryData = remoteTsdbProxyService.queryHistoryDeviceData(historyRequestVO);

        // 判断历史数据是否为空
        boolean hasData = !isHistoryDataEmpty(listHistoryData);
        //log.warn("历史数据为空，设备uuid：{}", deviceUuids);
        log.info("历史数据接口返回：{}", listHistoryData);

        // 如果有数据，解析TSDB返回的结果
        Map<String, Map<String, List<Map<String, Object>>>> tsdbDataMap = new HashMap<>();
        Map<String, String> deviceIdMap = new HashMap<>(); // deviceuuid -> device_id
        if (hasData) {
            tsdbDataMap = parseTsdbData(listHistoryData.getData());
            deviceIdMap = extractDeviceIdMap(listHistoryData.getData());
        }
        log.info("设备id映射：{}", deviceIdMap);
        log.info("tsdb数据：{}", tsdbDataMap);

        boolean customQuery = isCustomTimeType(request.getTimeType());

        for (EnergyReportRequest.DeviceItem device : request.getDeviceList()) {
            if (device == null || StringUtils.isEmpty(device.getId())) {
                continue;
            }

            // 为当前设备创建一个子列表
            List<EnergyReportResponse.ValueItem> deviceValueList = new ArrayList<>();

            // 获取设备UUID用于匹配TSDB数据
            DmpDevice dmpDevice = dmpDeviceMapper.selectById(device.getId());
            String deviceUuid = dmpDevice != null ? dmpDevice.getDeviceUuid() : null;

            // 如果没有功能点列表，为设备创建一个默认记录
            if (request.getFuncList() == null || request.getFuncList().isEmpty()) {
                if (customQuery) {
                    String total = ENERGY_NO_DATA;
                    if (hasData && deviceUuid != null && tsdbDataMap.containsKey(deviceUuid)) {
                        Map<String, List<Map<String, Object>>> deviceMetrics = tsdbDataMap.get(deviceUuid);
                        Map.Entry<String, List<Map<String, Object>>> firstMetric = deviceMetrics.entrySet().iterator().next();
                        total = calculateCustomPeriodTotal(
                                firstMetric.getValue(), firstMetric.getKey(), startTime, endTime);
                    }
                    deviceValueList.add(buildEnergyValueItem(device, deviceUuid, deviceIdMap, "-", total, null));
                } else {
                    Map<String, String> timeData;
                    String total;
                    if (hasData && deviceUuid != null && tsdbDataMap.containsKey(deviceUuid)) {
                        Map<String, List<Map<String, Object>>> deviceMetrics = tsdbDataMap.get(deviceUuid);
                        Map.Entry<String, List<Map<String, Object>>> firstMetric = deviceMetrics.entrySet().iterator().next();
                        timeData = buildEnergyTimeDataForMetricItems(
                                firstMetric.getValue(), firstMetric.getKey(),
                                request.getTimeType(), startTime, endTime);
                        total = calculateReportTotal(
                                timeData, firstMetric.getKey(), firstMetric.getValue(), startTime, endTime);
                    } else {
                        timeData = generateEmptyTimeData(request.getTimeType(), startTime, endTime);
                        total = ENERGY_NO_DATA;
                    }
                    deviceValueList.add(buildEnergyValueItem(device, deviceUuid, deviceIdMap, "-", total, timeData));
                }
            } else {
                for (EnergyReportRequest.FuncItem func : request.getFuncList()) {
                    if (func == null || StringUtils.isEmpty(func.getIdentifier())) {
                        continue;
                    }

                    String identifier = func.getIdentifierName() != null ? func.getIdentifierName() : func.getIdentifier();
                    if (customQuery) {
                        String total = ENERGY_NO_DATA;
                        if (hasData && deviceUuid != null && tsdbDataMap.containsKey(deviceUuid)) {
                            List<Map<String, Object>> metricItems = getMetricItemsIgnoreCase(
                                    tsdbDataMap.get(deviceUuid), func.getIdentifier());
                            total = calculateCustomPeriodTotal(
                                    metricItems, func.getIdentifier(), startTime, endTime);
                        }
                        deviceValueList.add(buildEnergyValueItem(device, deviceUuid, deviceIdMap, identifier, total, null));
                    } else {
                        Map<String, String> timeData;
                        String total;
                        List<Map<String, Object>> metricItems = null;
                        if (hasData && deviceUuid != null && tsdbDataMap.containsKey(deviceUuid)) {
                            metricItems = getMetricItemsIgnoreCase(tsdbDataMap.get(deviceUuid), func.getIdentifier());
                            timeData = buildTimeDataFromTsdbForMetric(
                                    tsdbDataMap.get(deviceUuid),
                                    func.getIdentifier(),
                                    request.getTimeType(),
                                    startTime,
                                    endTime);
                            total = calculateReportTotal(
                                    timeData, func.getIdentifier(), metricItems, startTime, endTime);
                        } else {
                            timeData = generateEmptyTimeData(request.getTimeType(), startTime, endTime);
                            total = ENERGY_NO_DATA;
                        }
                        deviceValueList.add(buildEnergyValueItem(device, deviceUuid, deviceIdMap, identifier, total, timeData));
                    }
                }
            }

            groupedValueList.add(deviceValueList);
        }

        return groupedValueList;
    }

    private EnergyReportResponse.ValueItem buildEnergyValueItem(EnergyReportRequest.DeviceItem device,
                                                                String deviceUuid,
                                                                Map<String, String> deviceIdMap,
                                                                String identifier,
                                                                String total,
                                                                Map<String, String> timeData) {
        EnergyReportResponse.ValueItem item = new EnergyReportResponse.ValueItem();
        item.setDeviceId(deviceIdMap.getOrDefault(deviceUuid, null));
        item.setDeviceName(device.getName());
        item.setCommAddress(device.getCommAddress());
        item.setIdentifier(identifier);
        item.setTotal(total);
        item.setTimeData(timeData);
        return item;
    }

    private boolean isCustomTimeType(String timeType) {
        return timeType != null && "custom".equalsIgnoreCase(timeType);
    }

    /**
     * 自定义查询合计：按指标类型聚合（累计类求各分段差值之和，瞬时类求全区间平均）
     */
    private String calculateCustomPeriodTotal(List<Map<String, Object>> metricItems,
                                              String metricIdentifier,
                                              LocalDateTime startTime,
                                              LocalDateTime endTime) {
        if (metricItems == null || metricItems.isEmpty()) {
            return ENERGY_NO_DATA;
        }
        String segmentTimeType = resolveSegmentTimeTypeForCustom(startTime, endTime);
        if ("custom_range".equals(segmentTimeType)) {
            return calculateEnergySegmentValue(metricItems, metricIdentifier, startTime, endTime, true);
        }
        Map<String, String> timeData = buildEnergyTimeDataForMetricItems(
                metricItems, metricIdentifier, segmentTimeType, startTime, endTime);
        return calculateReportTotal(timeData, metricIdentifier, metricItems, startTime, endTime);
    }

    /**
     * 自定义查询推断分段粒度，保证与 month/date/year 的合计逻辑一致
     */
    private String resolveSegmentTimeTypeForCustom(LocalDateTime startTime, LocalDateTime endTime) {
        if (startTime.toLocalDate().equals(endTime.toLocalDate())) {
            return "date";
        }
        if (startTime.getYear() == endTime.getYear() && startTime.getMonthValue() == endTime.getMonthValue()) {
            return "month";
        }
        if (startTime.getYear() == endTime.getYear()) {
            return "year";
        }
        return "custom_range";
    }

    private Map<String, String> buildEnergyTimeDataForMetricItems(List<Map<String, Object>> metricItems,
                                                                  String metricIdentifier,
                                                                  String timeType,
                                                                  LocalDateTime startTime,
                                                                  LocalDateTime endTime) {
        Map<String, String> timeData = new LinkedHashMap<>();
        List<TimeSegmentBound> segments = buildReportTimeSegments(timeType, startTime, endTime);
        for (TimeSegmentBound segment : segments) {
            String key = toEnergyTimeDataKey(segment.getLabel());
            timeData.put(key, calculateEnergySegmentValue(
                    metricItems, metricIdentifier,
                    segment.getStartInclusive(), segment.getEndExclusive(), false));
        }
        return timeData;
    }

    private String toEnergyTimeDataKey(String label) {
        return "_" + label.replace("时", "").replace("日", "").replace("月", "");
    }

    private MetricAggregateType resolveMetricAggregateType(String identifier) {
        if (!StringUtils.hasText(identifier)) {
            return MetricAggregateType.INSTANTANEOUS_AVG;
        }
        if (CUMULATIVE_ENERGY_METRICS.contains(identifier.toLowerCase())) {
            return MetricAggregateType.CUMULATIVE_DELTA;
        }
        return MetricAggregateType.INSTANTANEOUS_AVG;
    }

    /**
     * 单个时间段聚合：累计类取差值，瞬时类取平均（date 按小时、month 按天、year 按月）
     */
    private String calculateEnergySegmentValue(List<Map<String, Object>> metricItems,
                                               String metricIdentifier,
                                               LocalDateTime startInclusive,
                                               LocalDateTime endExclusive,
                                               boolean endInclusive) {
        TreeMap<LocalDateTime, BigDecimal> readings = endInclusive
                ? parseMetricReadingsInRangeInclusive(metricItems, startInclusive, endExclusive)
                : parseMetricReadingsInRange(metricItems, startInclusive, endExclusive);
        if (readings.isEmpty()) {
            return ENERGY_NO_DATA;
        }
        if (resolveMetricAggregateType(metricIdentifier) == MetricAggregateType.CUMULATIVE_DELTA) {
            return calculateCumulativeSegmentDelta(readings);
        }
        return calculateInstantaneousSegmentAverage(readings);
    }

    /** 累计表计：时段末次读数 - 首次读数（至少 2 条读数） */
    private String calculateCumulativeSegmentDelta(TreeMap<LocalDateTime, BigDecimal> readings) {
        if (readings.size() < 2) {
            return ENERGY_NO_DATA;
        }
        BigDecimal first = readings.firstEntry().getValue();
        BigDecimal last = readings.lastEntry().getValue();
        BigDecimal usage = last.subtract(first);
        if (usage.compareTo(BigDecimal.ZERO) < 0) {
            usage = last;
        }
        return formatEnergyReportValue(usage);
    }

    /** 瞬时量：时段内读数算术平均 */
    private String calculateInstantaneousSegmentAverage(TreeMap<LocalDateTime, BigDecimal> readings) {
        BigDecimal sum = BigDecimal.ZERO;
        for (BigDecimal value : readings.values()) {
            sum = sum.add(value);
        }
        return formatEnergyReportValue(sum.divide(BigDecimal.valueOf(readings.size()), 4, RoundingMode.HALF_UP));
    }

    private String formatEnergyReportValue(BigDecimal value) {
        return value.setScale(2, RoundingMode.HALF_UP).toPlainString();
    }

    private List<Map<String, Object>> getMetricItemsIgnoreCase(Map<String, List<Map<String, Object>>> metricDataMap,
                                                               String identifier) {
        if (metricDataMap == null || !StringUtils.hasText(identifier)) {
            return null;
        }
        List<Map<String, Object>> items = metricDataMap.get(identifier);
        if (items != null) {
            return items;
        }
        for (Map.Entry<String, List<Map<String, Object>>> entry : metricDataMap.entrySet()) {
            if (identifier.equalsIgnoreCase(entry.getKey())) {
                return entry.getValue();
            }
        }
        return null;
    }

    private TreeMap<LocalDateTime, BigDecimal> parseMetricReadingsInRange(List<Map<String, Object>> metricItems,
                                                                          LocalDateTime startInclusive,
                                                                          LocalDateTime endExclusive) {
        TreeMap<LocalDateTime, BigDecimal> readings = new TreeMap<>();
        if (metricItems == null || metricItems.isEmpty() || !startInclusive.isBefore(endExclusive)) {
            return readings;
        }

        for (Map<String, Object> item : metricItems) {
            if (item == null || !item.containsKey("timestamp") || !item.containsKey("value")) {
                continue;
            }
            try {
                LocalDateTime timestamp = parseMetricTimestamp(item.get("timestamp"));
                if (timestamp == null || timestamp.isBefore(startInclusive) || !timestamp.isBefore(endExclusive)) {
                    continue;
                }
                readings.put(timestamp, new BigDecimal(item.get("value").toString()));
            } catch (Exception e) {
                log.warn("解析能源报表历史读数失败：{}", item, e);
            }
        }
        return readings;
    }

    private TreeMap<LocalDateTime, BigDecimal> parseMetricReadingsInRangeInclusive(List<Map<String, Object>> metricItems,
                                                                                   LocalDateTime startInclusive,
                                                                                   LocalDateTime endInclusive) {
        TreeMap<LocalDateTime, BigDecimal> readings = new TreeMap<>();
        if (metricItems == null || metricItems.isEmpty()) {
            return readings;
        }

        for (Map<String, Object> item : metricItems) {
            if (item == null || !item.containsKey("timestamp") || !item.containsKey("value")) {
                continue;
            }
            try {
                LocalDateTime timestamp = parseMetricTimestamp(item.get("timestamp"));
                if (timestamp == null || timestamp.isBefore(startInclusive) || timestamp.isAfter(endInclusive)) {
                    continue;
                }
                readings.put(timestamp, new BigDecimal(item.get("value").toString()));
            } catch (Exception e) {
                log.warn("解析能源报表历史读数失败：{}", item, e);
            }
        }
        return readings;
    }

    private LocalDateTime parseMetricTimestamp(Object timestampObj) {
        if (timestampObj == null) {
            return null;
        }
        String timestampStr = timestampObj.toString();
        if (timestampStr.endsWith(".0")) {
            timestampStr = timestampStr.substring(0, timestampStr.length() - 2);
        }
        return parseDateTime(timestampStr);
    }

    /**
     * 生成空的时间数据结构（用于无数据时填充）
     */
    private Map<String, String> generateEmptyTimeData(String timeType, LocalDateTime startTime, LocalDateTime endTime) {
        Map<String, String> data = new LinkedHashMap<>();
        List<String> timeLabels = generateTimeLabels(timeType, startTime, endTime);
        for (String label : timeLabels) {
            data.put(toEnergyTimeDataKey(label), ENERGY_ZERO);
        }
        return data;
    }

    /**
     * 计算合计：累计类为各时段差值之和，瞬时类为查询区间内全量读数平均
     */
    private String calculateReportTotal(Map<String, String> timeData,
                                        String metricIdentifier,
                                        List<Map<String, Object>> metricItems,
                                        LocalDateTime startTime,
                                        LocalDateTime endTime) {
        if (resolveMetricAggregateType(metricIdentifier) == MetricAggregateType.CUMULATIVE_DELTA) {
            return calculateCumulativeTotal(timeData);
        }
        return calculateInstantaneousPeriodAverage(metricItems, startTime, endTime);
    }

    /**
     * 累计类合计：各时段差值之和
     */
    private String calculateCumulativeTotal(Map<String, String> timeData) {
        if (timeData == null || timeData.isEmpty()) {
            return ENERGY_NO_DATA;
        }

        BigDecimal sum = BigDecimal.ZERO;
        boolean hasNumeric = false;
        boolean allMissing = true;
        for (String value : timeData.values()) {
            if (ENERGY_NO_DATA.equals(value)) {
                continue;
            }
            allMissing = false;
            try {
                sum = sum.add(new BigDecimal(value));
                hasNumeric = true;
            } catch (NumberFormatException e) {
                // 忽略无效数据
            }
        }

        if (allMissing) {
            return ENERGY_NO_DATA;
        }
        return hasNumeric ? formatEnergyReportValue(sum) : ENERGY_NO_DATA;
    }

    /**
     * 瞬时类合计：查询区间内所有读数的算术平均
     */
    private String calculateInstantaneousPeriodAverage(List<Map<String, Object>> metricItems,
                                                       LocalDateTime startTime,
                                                       LocalDateTime endTime) {
        if (metricItems == null || metricItems.isEmpty()) {
            return ENERGY_NO_DATA;
        }
        TreeMap<LocalDateTime, BigDecimal> readings = parseMetricReadingsInRangeInclusive(
                metricItems, startTime, endTime);
        if (readings.isEmpty()) {
            return ENERGY_NO_DATA;
        }
        return calculateInstantaneousSegmentAverage(readings);
    }

    // ==================== TSDB数据解析方法 ====================

    /**
     * 解析TSDB返回的数据
     * 返回结构：Map<deviceuuid, Map<metric, List<metricItems>>>
     */
    private Map<String, Map<String, List<Map<String, Object>>>> parseTsdbData(List<HistorysInnerResultVO> tsdbResultList) {
        Map<String, Map<String, List<Map<String, Object>>>> dataMap = new HashMap<>();

        if (tsdbResultList == null || tsdbResultList.isEmpty()) {
            return dataMap;
        }

        for (HistorysInnerResultVO result : tsdbResultList) {
            if (result == null || !StringUtils.hasText(result.getDeviceuuid())) {
                continue;
            }

            String deviceUuid = result.getDeviceuuid();
            Map<String, List<Map<String, Object>>> metricMap = new HashMap<>();

            if (result.getMetrics() != null && !result.getMetrics().isEmpty()) {
                for (MetricVO metricVO : result.getMetrics()) {
                    if (metricVO != null && StringUtils.hasText(metricVO.getMetric())) {
                        metricMap.put(metricVO.getMetric(), metricVO.getMetricItems());
                    }
                }
            }

            dataMap.put(deviceUuid, metricMap);
        }

        return dataMap;
    }

    /**
     * 从TSDB返回结果中提取device_id映射
     * 返回结构：Map<deviceuuid, device_id>
     */
    private Map<String, String> extractDeviceIdMap(List<HistorysInnerResultVO> tsdbResultList) {
        Map<String, String> deviceIdMap = new HashMap<>();

        if (tsdbResultList == null || tsdbResultList.isEmpty()) {
            return deviceIdMap;
        }

        for (HistorysInnerResultVO result : tsdbResultList) {
            if (result == null || !StringUtils.hasText(result.getDeviceuuid())) {
                continue;
            }

            String deviceUuid = result.getDeviceuuid();
            // 从tags中获取device_id
            if (result.getTags() != null && result.getTags().containsKey("device_id")) {
                String deviceId = (String) result.getTags().get("device_id");
                deviceIdMap.put(deviceUuid, deviceId);
            }
        }

        return deviceIdMap;
    }

    /**
     * 从TSDB数据中构建时间数据（取第一个功能点）
     */
    private Map<String, String> buildTimeDataFromTsdb(Map<String, List<Map<String, Object>>> metricDataMap,
                                                      String timeType, LocalDateTime startTime, LocalDateTime endTime) {
        if (metricDataMap == null || metricDataMap.isEmpty()) {
            return generateEmptyTimeData(timeType, startTime, endTime);
        }
        Map.Entry<String, List<Map<String, Object>>> firstMetric = metricDataMap.entrySet().iterator().next();
        return buildEnergyTimeDataForMetricItems(
                firstMetric.getValue(), firstMetric.getKey(), timeType, startTime, endTime);
    }

    /**
     * 从TSDB数据中构建指定功能点的时间数据
     */
    private Map<String, String> buildTimeDataFromTsdbForMetric(Map<String, List<Map<String, Object>>> metricDataMap,
                                                               String metric,
                                                               String timeType,
                                                               LocalDateTime startTime,
                                                               LocalDateTime endTime) {
        List<Map<String, Object>> metricItems = getMetricItemsIgnoreCase(metricDataMap, metric);
        if (metricItems == null || metricItems.isEmpty()) {
            Map<String, String> empty = new LinkedHashMap<>();
            for (TimeSegmentBound segment : buildReportTimeSegments(timeType, startTime, endTime)) {
                empty.put(toEnergyTimeDataKey(segment.getLabel()), ENERGY_NO_DATA);
            }
            return empty;
        }
        return buildEnergyTimeDataForMetricItems(metricItems, metric, timeType, startTime, endTime);
    }

    // ==================== 分项报表私有方法 ====================

    /**
     * 生成分项报表标题列
     */
    private List<ItemReportResponse.TitleItem> generateItemReportTitles(String dateType, LocalDateTime startTime, LocalDateTime endTime) {
        List<ItemReportResponse.TitleItem> titles = new ArrayList<>();

        // 固定列
        titles.add(createTitleItem("name", true, "分项名称", null));
        titles.add(createTitleItem("total", true, "合计", null));

        // 动态时间列
        List<String> timeLabels = generateTimeLabels(dateType, startTime, endTime);
        int index = 1;
        for (String label : timeLabels) {
            titles.add(createTitleItem("_" + index, false, label, index));
            index++;
        }

        return titles;
    }

    /**
     * 生成分项报表数据
     */
    private List<Map<String, Object>> generateItemReportData(ItemReportRequest request, LocalDateTime startTime, LocalDateTime endTime) {
        List<Map<String, Object>> dataList = new ArrayList<>();

        for (ItemReportRequest.ItemCodeItem itemCode : request.getItemCodes()) {
            if (itemCode == null || StringUtils.isEmpty(itemCode.getCode())) {
                continue;
            }

            Map<String, Object> row = new LinkedHashMap<>();
            row.put("name", itemCode.getName());

            // 查询分项能耗数据（模拟）
            Map<String, Number> timeData = queryItemEnergyData(itemCode.getCode(), request.getSpaceId(),
                    startTime, endTime, request.getTimeType());

            // 计算合计
            Number total = calculateItemTotal(timeData);
            row.put("total", total);

            // 添加时间列数据
            int index = 1;
            for (Number value : timeData.values()) {
                row.put("_" + index, value);
                index++;
            }

            dataList.add(row);
        }

        return dataList;
    }

    /**
     * 查询分项能耗数据（模拟实现）
     */
    private Map<String, Number> queryItemEnergyData(String itemCode, Long spaceId,
                                                    LocalDateTime startTime, LocalDateTime endTime, String dateType) {
        Map<String, Number> data = new LinkedHashMap<>();

        // TODO: 实际应从数据库查询真实数据
        List<String> timeLabels = generateTimeLabels(dateType, startTime, endTime);
        for (String label : timeLabels) {
            data.put(label, 0); // 模拟数据为0
        }

        return data;
    }

    /**
     * 计算分项合计
     */
    private Number calculateItemTotal(Map<String, Number> timeData) {
        if (timeData == null || timeData.isEmpty()) {
            return 0;
        }
        return sumUsageValues(timeData.values());
    }

    // ==================== 区域报表私有方法 ====================

    /**
     * 生成区域报表列定义
     */
    private List<EnergyReportResponse.ColumnItem> generateSpaceReportColumns(String dateType, LocalDateTime startTime, LocalDateTime endTime) {
        List<EnergyReportResponse.ColumnItem> columns = new ArrayList<>();

        // 固定列
        columns.add(createColumn("区域名称", "spaceName", true));
        columns.add(createColumn("分项名称", "itemName", true));
        columns.add(createColumn("合计", "total", true));

        // 动态时间列（prop 与前端约定：date/month 保留「时/日」后缀，year 去掉「月」）
        List<String> timeLabels = generateTimeLabels(dateType, startTime, endTime);
        for (String label : timeLabels) {
            columns.add(createColumn(label, toSpaceReportColumnProp(label, dateType), false));
        }

        return columns;
    }

    /**
     * 生成区域报表数据（按区域/空间分组的二维数组）
     * 外层 List 代表不同的区域
     * 内层 List 包含该区域下所有设备的详细数据记录
     */
    private List<List<Map<String, Object>>> generateSpaceReportData(SpaceReportRequest request, LocalDateTime startTime, LocalDateTime endTime) {
        List<List<Map<String, Object>>> groupedValueList = new ArrayList<>();
        String timeType = request.getTimeType() != null ? request.getTimeType().toLowerCase() : "date";
        List<String> timeLabels = generateTimeLabels(timeType, startTime, endTime);

        for (SpaceReportRequest.SpaceItem space : request.getSpaces()) {
            if (space == null || space.getId() == null) {
                continue;
            }

            Map<String, Map<String, Number>> itemCodeTimeData = querySpaceItemEnergyData(space, request, startTime, endTime);
            List<Map<String, Object>> spaceValueList = new ArrayList<>();

            for (SpaceReportRequest.ItemCodeItem itemCode : request.getItemCodes()) {
                if (itemCode == null || StringUtils.isEmpty(itemCode.getCode())) {
                    continue;
                }

                Map<String, Number> timeData = itemCodeTimeData.getOrDefault(
                        buildSpaceItemKey(itemCode),
                        createEmptySpaceTimeData(timeType, startTime, endTime));

                Map<String, Object> row = new LinkedHashMap<>();
                row.put("spaceName", space.getName());
                row.put("itemName", itemCode.getName());

                List<Number> periodValues = new ArrayList<>();
                for (String label : timeLabels) {
                    Number value = timeData.getOrDefault(label, 0);
                    Number roundedValue = toUsageDecimal(value);
                    row.put(toSpaceReportValueKey(label), roundedValue.doubleValue());
                    periodValues.add(roundedValue);
                }
                row.put("total", sumUsageValues(periodValues));
                spaceValueList.add(row);
            }

            groupedValueList.add(spaceValueList);
        }

        return groupedValueList;
    }

    /**
     * 查询区域分项能耗数据，按分项（code+name）-> 时间标签 -> 用量 返回
     */
    private Map<String, Map<String, Number>> querySpaceItemEnergyData(SpaceReportRequest.SpaceItem space,
                                                                      SpaceReportRequest request,
                                                                      LocalDateTime startTime,
                                                                      LocalDateTime endTime) {
        String timeType = request.getTimeType() != null ? request.getTimeType().toLowerCase() : "date";
        List<TimeSegmentBound> timeSegments = buildReportTimeSegments(timeType, startTime, endTime);

        Map<String, Map<String, Number>> result = new LinkedHashMap<>();
        List<SpaceReportRequest.ItemCodeItem> requestedItems = request.getItemCodes() == null
                ? Collections.emptyList()
                : request.getItemCodes().stream()
                .filter(item -> item != null && StringUtils.hasText(item.getCode()))
                .collect(Collectors.toList());

        for (SpaceReportRequest.ItemCodeItem item : requestedItems) {
            result.put(buildSpaceItemKey(item), createEmptySpaceTimeData(timeType, startTime, endTime));
        }

        if (space == null || space.getId() == null || requestedItems.isEmpty()) {
            return result;
        }

        List<Long> spaceIds = spaceIds(Collections.singletonList(space.getId()));
        List<String> gatewayUuids = getGatewayUuidsBySpaceIds(spaceIds).stream()
                .map(BaseSpaceGateway::getGatewayUuid)
                .collect(Collectors.toList());
        List<String> spaceDeviceUuids = getDeviceUuidsByGatewayIds(gatewayUuids).stream()
                .map(DmpDevice::getDeviceUuid)
                .collect(Collectors.toList());

        if (spaceDeviceUuids.isEmpty()) {
            log.warn("区域下无设备，spaceId：{}", space.getId());
            return result;
        }

        Integer energyType = getEnergyTypeByDeviceUuid(spaceDeviceUuids.get(0));
        Long productId = getProductIdByEnergyType(energyType);
        if (productId != 360L) {
            throw new BusinessException("暂不支持该设备类型");
        }

        Map<String, List<String>> itemCodeToDeviceUuids = getItemCodeByDeviceUuid(spaceDeviceUuids);

        List<String> metrics = Collections.singletonList("totalactiveenergyf");
        HistorysInnerRequestVO historyRequestVO = new HistorysInnerRequestVO();
        historyRequestVO.setDeviceuuid(spaceDeviceUuids);
        historyRequestVO.setStartTime(request.getStartTime());
        historyRequestVO.setEndTime(request.getEndTime());
        historyRequestVO.setMetrics(metrics);
        log.info("历史数据接口请求：{}", historyRequestVO);
        ApiResult<List<HistorysInnerResultVO>> listHistoryData = remoteTsdbProxyService.queryHistoryDeviceData(historyRequestVO);
        log.info("历史数据接口返回：{}", listHistoryData);

        if (isHistoryDataEmpty(listHistoryData)) {
            log.warn("历史数据为空，设备uuid：{}", spaceDeviceUuids);
            return result;
        }

        Map<String, Map<String, TreeMap<LocalDateTime, Double>>> deviceMetricTimeValueMap =
                parseDeviceMetricTimeValueMap(listHistoryData.getData());

        for (SpaceReportRequest.ItemCodeItem itemCodeItem : requestedItems) {
            List<String> itemDeviceUuids = resolveSpaceItemDeviceUuids(spaceDeviceUuids, itemCodeToDeviceUuids, itemCodeItem);
            if (itemDeviceUuids.isEmpty()) {
                log.warn("分项未匹配到区域设备，spaceId：{}，itemCode：{}，itemName：{}",
                        space.getId(), itemCodeItem.getCode(), itemCodeItem.getName());
                continue;
            }

            Map<String, Number> timeUsageMap = result.get(buildSpaceItemKey(itemCodeItem));
            for (String deviceUuid : itemDeviceUuids) {
                Map<String, TreeMap<LocalDateTime, Double>> metricTimeValueMap = deviceMetricTimeValueMap.get(deviceUuid);
                if (metricTimeValueMap == null) {
                    continue;
                }
                TreeMap<LocalDateTime, Double> timeValueMap = metricTimeValueMap.get("totalactiveenergyf");
                if (timeValueMap == null || timeValueMap.isEmpty()) {
                    continue;
                }

                for (TimeSegmentBound segment : timeSegments) {
                    double usage = calculateSegmentUsage(timeValueMap, segment.getStartInclusive(), segment.getEndExclusive());
                    timeUsageMap.put(segment.getLabel(), addUsage(timeUsageMap.get(segment.getLabel()), usage).doubleValue());
                }
            }
        }

        return result;
    }

    private String buildSpaceItemKey(SpaceReportRequest.ItemCodeItem item) {
        return item.getCode() + "::" + (item.getName() != null ? item.getName() : "");
    }

    /**
     * 解析分项对应设备：优先按分项名称解析编码，再按 code 匹配，返回区域内全部关联设备
     */
    private List<String> resolveSpaceItemDeviceUuids(List<String> spaceDeviceUuids,
                                                     Map<String, List<String>> itemCodeToDeviceUuids,
                                                     SpaceReportRequest.ItemCodeItem itemCodeItem) {
        Set<String> spaceDeviceSet = new HashSet<>(spaceDeviceUuids);
        String requestCode = itemCodeItem.getCode().trim();

        String resolvedCode = resolveEnergyItemDbCode(requestCode, itemCodeItem.getName());
        if (StringUtils.hasText(resolvedCode)) {
            List<String> matched = findDevicesByItemCode(resolvedCode, itemCodeToDeviceUuids, spaceDeviceSet);
            if (!matched.isEmpty()) {
                return matched;
            }
        }

        List<String> matched = findDevicesByItemCode(requestCode, itemCodeToDeviceUuids, spaceDeviceSet);
        if (!matched.isEmpty()) {
            return matched;
        }

        return Collections.emptyList();
    }

    private List<String> findDevicesByItemCode(String itemCode,
                                               Map<String, List<String>> itemCodeToDeviceUuids,
                                               Set<String> spaceDeviceSet) {
        List<String> matched = filterDevicesInSpace(itemCodeToDeviceUuids.get(itemCode), spaceDeviceSet);
        if (!matched.isEmpty()) {
            return matched;
        }
        return filterDevicesInSpace(
                emsDeviceItemCodeMapper.selectDeviceUuidsByItemCode(SecurityUtils.getTenantId(), itemCode),
                spaceDeviceSet);
    }

    private List<String> filterDevicesInSpace(List<String> deviceUuids, Set<String> spaceDeviceSet) {
        if (deviceUuids == null || deviceUuids.isEmpty()) {
            return Collections.emptyList();
        }
        return deviceUuids.stream()
                .filter(StringUtils::hasText)
                .map(String::trim)
                .filter(spaceDeviceSet::contains)
                .distinct()
                .collect(Collectors.toList());
    }

    /**
     * 将请求中的 code/identifier/name 解析为 ems_energy_item_code 表中的 code
     */
    private String resolveEnergyItemDbCode(String requestCode, String itemName) {
        if (StringUtils.hasText(itemName)) {
            EmsEnergyItemCode byName = emsEnergyItemCodeMapper.selectOne(
                    Wrappers.lambdaQuery(EmsEnergyItemCode.class)
                            .eq(EmsEnergyItemCode::getName, itemName)
                            .last("LIMIT 1"));
            if (byName != null) {
                return byName.getCode();
            }
        }

        if (StringUtils.hasText(requestCode)) {
            EmsEnergyItemCode byCode = emsEnergyItemCodeMapper.selectOne(
                    Wrappers.lambdaQuery(EmsEnergyItemCode.class)
                            .eq(EmsEnergyItemCode::getCode, requestCode)
                            .last("LIMIT 1"));
            if (byCode != null) {
                return byCode.getCode();
            }

            EmsEnergyItemCode byIdentifier = emsEnergyItemCodeMapper.selectOne(
                    Wrappers.lambdaQuery(EmsEnergyItemCode.class)
                            .eq(EmsEnergyItemCode::getIdentifier, requestCode)
                            .last("LIMIT 1"));
            if (byIdentifier != null) {
                return byIdentifier.getCode();
            }
        }
        return null;
    }

    private TreeMap<LocalDateTime, Double> extractSegmentReadings(TreeMap<LocalDateTime, Double> source,
                                                                  LocalDateTime startInclusive,
                                                                  LocalDateTime endExclusive) {
        TreeMap<LocalDateTime, Double> result = new TreeMap<>();
        if (source == null || source.isEmpty() || !startInclusive.isBefore(endExclusive)) {
            return result;
        }
        for (Map.Entry<LocalDateTime, Double> entry : source.entrySet()) {
            LocalDateTime ts = entry.getKey();
            if (!ts.isBefore(startInclusive) && ts.isBefore(endExclusive)) {
                result.put(ts, entry.getValue());
            }
        }
        return result;
    }

    /**
     * 解析 TSDB 历史数据为 设备 -> 指标 -> 时间 -> 值 结构
     */
    private Map<String, Map<String, TreeMap<LocalDateTime, Double>>> parseDeviceMetricTimeValueMap(
            List<HistorysInnerResultVO> historyDataList) {
        Map<String, Map<String, TreeMap<LocalDateTime, Double>>> deviceMetricTimeValueMap = new HashMap<>();

        if (historyDataList == null || historyDataList.isEmpty()) {
            return deviceMetricTimeValueMap;
        }

        for (HistorysInnerResultVO result : historyDataList) {
            if (result == null || !StringUtils.hasText(result.getDeviceuuid())) {
                continue;
            }
            String deviceUuid = result.getDeviceuuid();
            Map<String, TreeMap<LocalDateTime, Double>> metricTimeValueMap =
                    deviceMetricTimeValueMap.computeIfAbsent(deviceUuid, k -> new HashMap<>());

            if (result.getMetrics() == null) {
                continue;
            }
            for (MetricVO metric : result.getMetrics()) {
                if (metric == null || !StringUtils.hasText(metric.getMetric()) || metric.getMetricItems() == null) {
                    continue;
                }
                String metricName = metric.getMetric();
                TreeMap<LocalDateTime, Double> timeValueMap =
                        metricTimeValueMap.computeIfAbsent(metricName, k -> new TreeMap<>());

                for (Map<String, Object> point : metric.getMetricItems()) {
                    try {
                        String timestampStr = point.get("timestamp").toString();
                        if (timestampStr.endsWith(".0")) {
                            timestampStr = timestampStr.substring(0, timestampStr.length() - 2);
                        }
                        LocalDateTime timestamp = LocalDateTime.parse(timestampStr, DATE_TIME_FORMATTER);
                        Double value = Double.parseDouble(point.get("value").toString());
                        timeValueMap.put(timestamp, value);
                    } catch (Exception e) {
                        log.error("解析历史数据失败，设备：{}，指标：{}，值：{}",
                                deviceUuid, metricName, point.get("value"), e);
                    }
                }
            }
        }
        return deviceMetricTimeValueMap;
    }

    /**
     * 时间段用量 = 该时间段内最后一个读数 - 第一个读数（至少 2 条读数）
     */
    private double calculateSegmentUsage(TreeMap<LocalDateTime, Double> timeValueMap,
                                         LocalDateTime startInclusive,
                                         LocalDateTime endExclusive) {
        if (timeValueMap == null || timeValueMap.isEmpty()) {
            return 0;
        }

        TreeMap<LocalDateTime, Double> segmentData = extractSegmentReadings(timeValueMap, startInclusive, endExclusive);
        if (segmentData.size() < 2) {
            return 0;
        }

        double firstValue = segmentData.firstEntry().getValue();
        double lastValue = segmentData.lastEntry().getValue();
        return formatUsageValue(calculateReadingDelta(firstValue, lastValue));
    }

    private double calculateReadingDelta(double firstValue, double lastValue) {
        double usage = lastValue - firstValue;
        if (usage < 0) {
            return lastValue;
        }
        return usage;
    }

    private double formatUsageValue(double usage) {
        return toUsageDecimal(usage).doubleValue();
    }

    private static final int USAGE_SCALE = 2;

    private BigDecimal toUsageDecimal(Number value) {
        if (value == null) {
            return BigDecimal.ZERO.setScale(USAGE_SCALE, RoundingMode.HALF_UP);
        }
        return BigDecimal.valueOf(value.doubleValue()).setScale(USAGE_SCALE, RoundingMode.HALF_UP);
    }

    private BigDecimal addUsage(Number a, Number b) {
        return toUsageDecimal(a).add(toUsageDecimal(b)).setScale(USAGE_SCALE, RoundingMode.HALF_UP);
    }

    private double sumUsageValues(Iterable<? extends Number> values) {
        BigDecimal sum = BigDecimal.ZERO;
        if (values != null) {
            for (Number value : values) {
                sum = sum.add(toUsageDecimal(value));
            }
        }
        return sum.setScale(USAGE_SCALE, RoundingMode.HALF_UP).doubleValue();
    }

    private Map<String, Number> createEmptySpaceTimeData(String timeType, LocalDateTime startTime, LocalDateTime endTime) {
        Map<String, Number> data = new LinkedHashMap<>();
        for (String label : generateTimeLabels(timeType, startTime, endTime)) {
            data.put(label, 0);
        }
        return data;
    }

    /**
     * 报表时间分段（左闭右开），与 columnList 标签一一对应
     */
    private static class TimeSegmentBound {
        private final String label;
        private final LocalDateTime startInclusive;
        private final LocalDateTime endExclusive;

        TimeSegmentBound(String label, LocalDateTime startInclusive, LocalDateTime endExclusive) {
            this.label = label;
            this.startInclusive = startInclusive;
            this.endExclusive = endExclusive;
        }

        String getLabel() {
            return label;
        }

        LocalDateTime getStartInclusive() {
            return startInclusive;
        }

        LocalDateTime getEndExclusive() {
            return endExclusive;
        }
    }

    private List<TimeSegmentBound> buildReportTimeSegments(String timeType, LocalDateTime startTime, LocalDateTime endTime) {
        List<TimeSegmentBound> segments = new ArrayList<>();
        if (startTime == null || endTime == null || !startTime.isBefore(endTime)) {
            return segments;
        }

        String type = timeType != null ? timeType.toLowerCase() : "date";
        switch (type) {
            case "date":
                buildHourSegments(segments, startTime, endTime);
                break;
            case "month":
                buildDaySegments(segments, startTime, endTime);
                break;
            case "year":
                buildMonthSegments(segments, startTime, endTime);
                break;
            case "custom":
                break;
            default:
                buildHourSegments(segments, startTime, endTime);
                break;
        }
        return segments;
    }

    /** date：按小时分段 [HH:00, HH+1:00)，与传参起止小时一致 */
    private void buildHourSegments(List<TimeSegmentBound> segments, LocalDateTime startTime, LocalDateTime endTime) {
        int startHour = startTime.getHour();
        int endHour = endTime.getHour();
        for (int hour = startHour; hour <= endHour; hour++) {
            String label = hour + "时";
            LocalDateTime hourStart = startTime.withHour(hour).withMinute(0).withSecond(0).withNano(0);
            LocalDateTime hourEndExclusive = hourStart.plusHours(1);
            LocalDateTime segmentStart = hourStart.isBefore(startTime) ? startTime : hourStart;
            LocalDateTime segmentEndExclusive = hourEndExclusive.isAfter(endTime) ? endTime : hourEndExclusive;
            if (segmentStart.isBefore(segmentEndExclusive)) {
                segments.add(new TimeSegmentBound(label, segmentStart, segmentEndExclusive));
            } else {
                segments.add(new TimeSegmentBound(label, segmentStart, segmentStart.plusNanos(1)));
            }
        }
    }

    /** month：按自然日分段 [00:00, 次日00:00) */
    private void buildDaySegments(List<TimeSegmentBound> segments, LocalDateTime startTime, LocalDateTime endTime) {
        LocalDate current = startTime.toLocalDate();
        LocalDate lastDate = resolveLastDateInRange(startTime, endTime);
        while (!current.isAfter(lastDate)) {
            String label = String.format("%02d日", current.getDayOfMonth());
            LocalDateTime dayStart = current.atStartOfDay();
            LocalDateTime dayEndExclusive = current.plusDays(1).atStartOfDay();
            LocalDateTime segmentStart = dayStart.isBefore(startTime) ? startTime : dayStart;
            LocalDateTime segmentEndExclusive = dayEndExclusive.isAfter(endTime) ? endTime : dayEndExclusive;
            if (segmentStart.isBefore(segmentEndExclusive)) {
                segments.add(new TimeSegmentBound(label, segmentStart, segmentEndExclusive));
            }
            current = current.plusDays(1);
        }
    }

    /** year：按自然月分段 [月初00:00, 下月初00:00) */
    private void buildMonthSegments(List<TimeSegmentBound> segments, LocalDateTime startTime, LocalDateTime endTime) {
        int year = startTime.getYear();
        int month = startTime.getMonthValue();
        int endYear = endTime.getYear();
        int endMonth = endTime.getMonthValue();

        while (year < endYear || (year == endYear && month <= endMonth)) {
            String label = month + "月";
            LocalDateTime monthStart = LocalDateTime.of(year, month, 1, 0, 0, 0);
            LocalDateTime monthEndExclusive = monthStart.plusMonths(1);
            LocalDateTime segmentStart = monthStart.isBefore(startTime) ? startTime : monthStart;
            LocalDateTime segmentEndExclusive = monthEndExclusive.isAfter(endTime) ? endTime : monthEndExclusive;
            if (segmentStart.isBefore(segmentEndExclusive)) {
                segments.add(new TimeSegmentBound(label, segmentStart, segmentEndExclusive));
            }
            month++;
            if (month > 12) {
                month = 1;
                year++;
            }
        }
    }

    /**
     * 结束时间为某日 00:00:00 时，表示不含该日，最后一天取前一日
     */
    private LocalDate resolveLastDateInRange(LocalDateTime startTime, LocalDateTime endTime) {
        LocalDate endDate = endTime.toLocalDate();
        if (endTime.toLocalTime().equals(LocalTime.MIDNIGHT) && endDate.isAfter(startTime.toLocalDate())) {
            return endDate.minusDays(1);
        }
        return endDate;
    }

    private String toSpaceReportColumnProp(String label, String timeType) {
        String type = timeType != null ? timeType.toLowerCase() : "date";
        if ("year".equals(type)) {
            return "_" + label.replace("月", "");
        }
        return "_" + label;
    }

    private String toSpaceReportValueKey(String label) {
        String numPart = label.replace("时", "").replace("日", "").replace("月", "");
        try {
            return "_" + Integer.parseInt(numPart);
        } catch (NumberFormatException e) {
            return "_" + numPart;
        }
    }

    /**
     * 查询所有区域id（包含子区域id）
     */
    private List<Long> spaceIds(List<Long> spaceId) {
        return baseSpaceMapper.selectList(
                new LambdaQueryWrapper<BaseSpace>()
                        .in(BaseSpace::getId, spaceId)
                        .or().in(BaseSpace::getParentId, spaceId)
        ).stream().map(BaseSpace::getId).collect(Collectors.toList());
    }

    /**
     * 通过energy_type获取product_id
     */
    private Long getProductIdByEnergyType(Integer energyType) {
        return emsProductEnergyTypeMapper.selectOne(
                new LambdaQueryWrapper<EmsProductEnergyType>()
                        .eq(EmsProductEnergyType::getEnergyType, energyType)
        ).getProductId();
    }

    /**
     * 通过device_uuid按item_code分组
     */
    private Map<String, List<String>> getItemCodeByDeviceUuid(List<String> deviceUuids) {
        return emsDeviceItemCodeMapper.selectList(
                new LambdaQueryWrapper<EmsDeviceItemCode>()
                        .in(EmsDeviceItemCode::getDeviceUuid, deviceUuids)
        ).stream().collect(Collectors.groupingBy(
                EmsDeviceItemCode::getItemCode,
                Collectors.mapping(
                        EmsDeviceItemCode::getDeviceUuid,
                        Collectors.toList()
                )
        ));
    }

    /**
     * 通过device_uuid获取energy_type
     */
    private Integer getEnergyTypeByDeviceUuid(String deviceUuid) {
        return emsDeviceItemCodeMapper.selectList(
                new LambdaQueryWrapper<EmsDeviceItemCode>()
                        .eq(EmsDeviceItemCode::getDeviceUuid, deviceUuid)
        ).get(0).getEnergyType();
    }

    /**
     * 根据网关id获取设备UUID列表
     */
    private List<DmpDevice> getDeviceUuidsByGatewayIds(List<String> gatewayIds) {
        return new ArrayList<>(dmpDeviceMapper.selectList(
                new LambdaQueryWrapper<DmpDevice>()
                        .in(DmpDevice::getGatewayUuid, gatewayIds)));
    }

    /**
     * 根据区域id获取网关UUID列表
     */
    private List<BaseSpaceGateway> getGatewayUuidsBySpaceIds(List<Long> spaceIds) {
        return new ArrayList<>(baseSpaceGatewayMapper.selectList(
                new LambdaQueryWrapper<BaseSpaceGateway>()
                        .in(BaseSpaceGateway::getSpaceId, spaceIds)));
    }

    /**
     * 计算区域报表总计（适配二维数组结构）
     */
    private Number calculateSpaceReportTotal(List<List<Map<String, Object>>> groupedValueList) {
        if (groupedValueList == null || groupedValueList.isEmpty()) {
            return 0;
        }

        List<Number> rowTotals = new ArrayList<>();
        for (List<Map<String, Object>> group : groupedValueList) {
            if (group == null || group.isEmpty()) {
                continue;
            }
            for (Map<String, Object> row : group) {
                Object total = row.get("total");
                if (total instanceof Number) {
                    rowTotals.add((Number) total);
                }
            }
        }
        return sumUsageValues(rowTotals);
    }

    // ==================== 通用工具方法 ====================

    /**
     * 校验时间参数
     * 1. 结束时间不得早于开始时间
     * 2. 按日查询：起止时间必须是同一天
     * 3. 按月查询：起止时间必须是同一月
     * 4. 按年查询：起止时间必须是同一年
     */
    private void validateTimeParams(LocalDateTime startTime, LocalDateTime endTime, String timeType) {
        // 基本校验：结束时间不得早于开始时间
        if (endTime.isBefore(startTime)) {
            throw new BusinessException("结束时间不得早于开始时间");
        }

        String type = timeType != null ? timeType.toLowerCase() : "date";

        switch (type) {
            case "date":
                // 按日查询：必须是同一天
                if (!startTime.toLocalDate().isEqual(endTime.toLocalDate())) {
                    throw new BusinessException("按日查询时，起止时间必须是同一天");
                }
                break;
            case "month":
                // 按月查询：必须是同一月
                if (startTime.getYear() != endTime.getYear() ||
                        startTime.getMonthValue() != endTime.getMonthValue()) {
                    throw new BusinessException("按月查询时，起止时间必须是同一月");
                }
                break;
            case "year":
                // 按年查询：必须是同一年
                if (startTime.getYear() != endTime.getYear()) {
                    throw new BusinessException("按年查询时，起止时间必须是同一年");
                }
                break;
            case "custom":
                // 自定义查询：不进行时间校验
                break;
            default:
                throw new BusinessException("查询时间类型错误！请重试");
        }
    }

    /**
     * 创建列定义
     */
    private EnergyReportResponse.ColumnItem createColumn(String label, String prop, Boolean fixed) {
        EnergyReportResponse.ColumnItem column = new EnergyReportResponse.ColumnItem();
        column.setLabel(label);
        column.setProp(prop);
        column.setFixed(fixed);
        column.setChildren(null);
        return column;
    }

    /**
     * 创建标题项
     */
    private ItemReportResponse.TitleItem createTitleItem(String prop, Boolean fixed, String label, Integer value) {
        ItemReportResponse.TitleItem item = new ItemReportResponse.TitleItem();
        item.setProp(prop);
        item.setFixed(fixed);
        item.setLabel(label);
        item.setValue(value);
        return item;
    }

    /**
     * 生成时间标签列表
     * 规则：
     * - 按日(date)查询 → 返回小时数据(根据起止时间的小时范围)
     * - 按月(month)查询 → 返回日数据(01日-31日)
     * - 按年(year)查询 → 返回月数据(1月-12月)
     */
    private List<String> generateTimeLabels(String timeType, LocalDateTime startTime, LocalDateTime endTime) {
        if (startTime == null || endTime == null) {
            return new ArrayList<>();
        }
        return buildReportTimeSegments(timeType, startTime, endTime).stream()
                .map(TimeSegmentBound::getLabel)
                .collect(Collectors.toList());
    }

    /**
     * 解析日期时间字符串
     */
    private LocalDateTime parseDateTime(String dateTimeStr) {
        if (!StringUtils.hasText(dateTimeStr)) {
            return null;
        }
        try {
            return LocalDateTime.parse(dateTimeStr, DATE_TIME_FORMATTER);
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * 获取设备UUID列表
     */
    private List<String> getUuidList(List<Integer> deviceIds) {
        return deviceIds.stream().map(deviceId -> {
            DmpDevice device = dmpDeviceMapper.selectById(deviceId);
            return device != null ? device.getDeviceUuid() : null;
        }).collect(Collectors.toList());
    }

    private boolean isHistoryDataEmpty(ApiResult<?> result) {
        return result == null
                || !result.isSuccess()
                || result.getStatus() != ApiResult.ResultStatus.SUCCESS
                || result.getData() == null;
    }

    // ==================== 报表 Excel 导出 ====================

    private String buildExportFileName(String prefix) {
        return prefix + "_" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
    }

    private String resolveEnergyCellValue(EnergyReportResponse.ValueItem item,
                                          EnergyReportResponse.ColumnItem column) {
        if (item == null || column == null || !StringUtils.hasText(column.getProp())) {
            return "";
        }
        String prop = column.getProp();
        switch (prop) {
            case "deviceName":
                return formatExportCellValue(item.getDeviceName());
            case "commAddress":
                return formatExportCellValue(item.getCommAddress());
            case "identifier":
                return formatExportCellValue(item.getIdentifier());
            case "total":
                return formatExportCellValue(item.getTotal());
            default:
                Map<String, String> timeData = item.getTimeData();
                if (timeData != null && timeData.containsKey(prop)) {
                    return formatExportCellValue(timeData.get(prop));
                }
                return ENERGY_NO_DATA;
        }
    }

    private String resolveSpaceCellValue(Map<String, Object> rowMap, EnergyReportResponse.ColumnItem column) {
        if (rowMap == null || column == null || !StringUtils.hasText(column.getProp())) {
            return "";
        }
        Object value = rowMap.get(column.getProp());
        if (value == null && StringUtils.hasText(column.getLabel())) {
            value = rowMap.get(toSpaceReportValueKey(column.getLabel()));
        }
        return formatExportCellValue(value);
    }

    private String formatExportCellValue(Object value) {
        return value == null ? "" : value.toString();
    }

    private void writeEnergyReportExcel(HttpServletResponse response,
                                        String fileName,
                                        List<EnergyReportResponse.ColumnItem> columns,
                                        List<List<EnergyReportResponse.ValueItem>> groupedRows) {
        try (Workbook workbook = new XSSFWorkbook()) {
            Sheet sheet = workbook.createSheet("能源报表");
            CellStyle headerStyle = createExportHeaderStyle(workbook);
            CellStyle textStyle = createExportDataStyle(workbook, HorizontalAlignment.LEFT);
            CellStyle numberStyle = createExportDataStyle(workbook, HorizontalAlignment.CENTER);

            writeExportHeaderRow(sheet, columns, headerStyle);

            int deviceNameCol = findColumnIndex(columns, "deviceName");
            int commAddressCol = findColumnIndex(columns, "commAddress");
            int rowIndex = 1;
            for (List<EnergyReportResponse.ValueItem> deviceRows : groupedRows) {
                if (deviceRows == null || deviceRows.isEmpty()) {
                    continue;
                }
                int groupStartRow = rowIndex;
                for (EnergyReportResponse.ValueItem item : deviceRows) {
                    Row row = sheet.createRow(rowIndex++);
                    writeEnergyReportDataRow(row, item, columns, textStyle, numberStyle);
                }
                int groupEndRow = rowIndex - 1;
                if (groupEndRow > groupStartRow) {
                    mergeExportColumn(sheet, groupStartRow, groupEndRow, deviceNameCol);
                    mergeExportColumn(sheet, groupStartRow, groupEndRow, commAddressCol);
                }
            }

            autoSizeExportColumns(sheet, columns.size());
            writeExcelToResponse(response, fileName, workbook);
        } catch (IOException e) {
            throw new BusinessException("导出失败", e);
        }
    }

    private void writeSpaceReportExcel(HttpServletResponse response,
                                       String fileName,
                                       List<EnergyReportResponse.ColumnItem> columns,
                                       List<List<Map<String, Object>>> groupedRows) {
        try (Workbook workbook = new XSSFWorkbook()) {
            Sheet sheet = workbook.createSheet("区域报表");
            CellStyle headerStyle = createExportHeaderStyle(workbook);
            CellStyle textStyle = createExportDataStyle(workbook, HorizontalAlignment.LEFT);
            CellStyle numberStyle = createExportDataStyle(workbook, HorizontalAlignment.CENTER);

            writeExportHeaderRow(sheet, columns, headerStyle);

            int spaceNameCol = findColumnIndex(columns, "spaceName");
            int rowIndex = 1;
            for (List<Map<String, Object>> spaceRows : groupedRows) {
                if (spaceRows == null || spaceRows.isEmpty()) {
                    continue;
                }
                int groupStartRow = rowIndex;
                for (Map<String, Object> rowMap : spaceRows) {
                    Row row = sheet.createRow(rowIndex++);
                    writeSpaceReportDataRow(row, rowMap, columns, textStyle, numberStyle);
                }
                int groupEndRow = rowIndex - 1;
                if (groupEndRow > groupStartRow) {
                    mergeExportColumn(sheet, groupStartRow, groupEndRow, spaceNameCol);
                }
            }

            autoSizeExportColumns(sheet, columns.size());
            writeExcelToResponse(response, fileName, workbook);
        } catch (IOException e) {
            throw new BusinessException("导出失败", e);
        }
    }

    private void writeExportHeaderRow(Sheet sheet,
                                      List<EnergyReportResponse.ColumnItem> columns,
                                      CellStyle headerStyle) {
        Row headerRow = sheet.createRow(0);
        for (int i = 0; i < columns.size(); i++) {
            Cell cell = headerRow.createCell(i);
            cell.setCellValue(columns.get(i).getLabel());
            cell.setCellStyle(headerStyle);
        }
    }

    private void writeEnergyReportDataRow(Row row,
                                          EnergyReportResponse.ValueItem item,
                                          List<EnergyReportResponse.ColumnItem> columns,
                                          CellStyle textStyle,
                                          CellStyle numberStyle) {
        for (int colIndex = 0; colIndex < columns.size(); colIndex++) {
            EnergyReportResponse.ColumnItem column = columns.get(colIndex);
            Cell cell = row.createCell(colIndex);
            cell.setCellValue(resolveEnergyCellValue(item, column));
            cell.setCellStyle(isExportTextColumn(column) ? textStyle : numberStyle);
        }
    }

    private void writeSpaceReportDataRow(Row row,
                                         Map<String, Object> rowMap,
                                         List<EnergyReportResponse.ColumnItem> columns,
                                         CellStyle textStyle,
                                         CellStyle numberStyle) {
        for (int colIndex = 0; colIndex < columns.size(); colIndex++) {
            EnergyReportResponse.ColumnItem column = columns.get(colIndex);
            Cell cell = row.createCell(colIndex);
            cell.setCellValue(resolveSpaceCellValue(rowMap, column));
            cell.setCellStyle(isExportTextColumn(column) ? textStyle : numberStyle);
        }
    }

    private boolean isExportTextColumn(EnergyReportResponse.ColumnItem column) {
        if (column == null || !StringUtils.hasText(column.getProp())) {
            return true;
        }
        switch (column.getProp()) {
            case "deviceName":
            case "commAddress":
            case "identifier":
            case "spaceName":
            case "itemName":
                return true;
            default:
                return false;
        }
    }

    private int findColumnIndex(List<EnergyReportResponse.ColumnItem> columns, String prop) {
        for (int i = 0; i < columns.size(); i++) {
            if (prop.equals(columns.get(i).getProp())) {
                return i;
            }
        }
        return -1;
    }

    private void mergeExportColumn(Sheet sheet, int firstRow, int lastRow, int columnIndex) {
        if (columnIndex < 0 || firstRow >= lastRow) {
            return;
        }
        sheet.addMergedRegion(new CellRangeAddress(firstRow, lastRow, columnIndex, columnIndex));
    }

    private void autoSizeExportColumns(Sheet sheet, int columnCount) {
        for (int i = 0; i < columnCount; i++) {
            sheet.autoSizeColumn(i);
            int width = sheet.getColumnWidth(i);
            sheet.setColumnWidth(i, Math.min(width + 512, 255 * 256));
        }
    }

    private void writeExcelToResponse(HttpServletResponse response, String fileName, Workbook workbook)
            throws IOException {
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("utf-8");
        response.setHeader("Content-Disposition",
                "attachment; filename=\"" + encodeExportFileName(fileName) + ".xlsx\"");
        workbook.write(response.getOutputStream());
        response.getOutputStream().flush();
    }

    private String encodeExportFileName(String fileName) {
        try {
            return URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()).replace("+", "%20");
        } catch (UnsupportedEncodingException e) {
            return fileName;
        }
    }

    private CellStyle createExportHeaderStyle(Workbook workbook) {
        CellStyle style = createBorderedCellStyle(workbook);
        Font font = workbook.createFont();
        font.setBold(true);
        style.setFont(font);
        style.setAlignment(HorizontalAlignment.CENTER);
        style.setVerticalAlignment(VerticalAlignment.CENTER);
        style.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
        style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
        return style;
    }

    private CellStyle createExportDataStyle(Workbook workbook, HorizontalAlignment alignment) {
        CellStyle style = createBorderedCellStyle(workbook);
        style.setAlignment(alignment);
        style.setVerticalAlignment(VerticalAlignment.CENTER);
        return style;
    }

    private CellStyle createBorderedCellStyle(Workbook workbook) {
        CellStyle style = workbook.createCellStyle();
        style.setBorderTop(BorderStyle.THIN);
        style.setBorderBottom(BorderStyle.THIN);
        style.setBorderLeft(BorderStyle.THIN);
        style.setBorderRight(BorderStyle.THIN);
        return style;
    }

    // ==================== 空响应创建方法 ====================

    private EnergyReportResponse createEmptyEnergyReport() {
        EnergyReportResponse response = new EnergyReportResponse();
        response.setColumnList(new ArrayList<>());
        response.setValueList(new ArrayList<>());
        return response;
    }

    private ItemReportResponse createEmptyItemReport() {
        ItemReportResponse response = new ItemReportResponse();
        response.setTitleList(new ArrayList<>());
        response.setDataList(new ArrayList<>());
        return response;
    }

    private SpaceReportResponse createEmptySpaceReport() {
        SpaceReportResponse response = new SpaceReportResponse();
        response.setColumnList(new ArrayList<>());
        response.setValueList(new ArrayList<>());
        response.setTotal(0);
        return response;
    }
}
