package com.evobms.project.system.controller;

import java.math.BigDecimal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import javax.servlet.http.HttpServletResponse;

import com.evobms.project.vehicledata.domain.VehicleLocation;
import com.evobms.project.vehicledata.service.IVehicleLocationService;
import io.swagger.annotations.ApiParam;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import com.evobms.framework.aspectj.lang.annotation.Log;
import com.evobms.framework.aspectj.lang.enums.BusinessType;
import com.evobms.project.system.domain.BmsDevices;
import com.evobms.project.system.service.IBmsDevicesService;
import com.evobms.framework.web.controller.BaseController;
import com.evobms.framework.web.domain.AjaxResult;
import com.evobms.framework.web.domain.R;
import com.evobms.common.constant.HttpStatus;
import com.evobms.common.utils.poi.ExcelUtil;
import com.evobms.framework.web.page.TableDataInfo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.evobms.project.vehicledata.dto.DeviceHistoryFlatRowDTO;
import com.evobms.project.vehicledata.dto.DeviceRealtimeDTO;
import com.evobms.project.vehicledata.dto.DeviceRealtimeAggregateDTO;
import com.evobms.project.vehicledata.dto.NameValueDTO;
import com.evobms.project.vehicledata.dto.DeviceRealtimeDTO.SystemStatusSection;
import com.evobms.project.vehicledata.dto.DeviceRealtimeDTO.RunStatusSection;
import com.evobms.project.vehicledata.dto.DeviceRealtimeDTO.ChargeSection;
import com.evobms.project.vehicledata.dto.DeviceRealtimeDTO.StatisticsSection;
import com.evobms.project.vehicledata.dto.LineSeriesDTO;
import com.evobms.project.vehicledata.dto.TimePointDTO;
import com.evobms.project.vehicledata.dto.ChargeDischargeMonthDTO;
import com.evobms.project.vehicledata.dto.ChargeDischargeRecordDTO;
import com.evobms.project.vehicledata.dto.ChargeDischargeHistoryDTO;
import com.evobms.project.vehicledata.service.DeviceRealtimeHistoryAssembler;
import com.evobms.project.vehicledata.service.IVehicleDataService;
import com.evobms.project.vehicledata.domain.VehicleData;
import com.evobms.project.battery.service.IExtremeValuesService;
import com.evobms.project.battery.service.ISubsystemVoltageService;
import com.evobms.project.battery.service.ISubsystemTemperatureService;
import com.evobms.project.battery.domain.ExtremeValues;
import com.evobms.project.battery.domain.SubsystemVoltage;
import com.evobms.project.battery.domain.SubsystemTemperature;
import com.evobms.project.bms.service.*;
import com.evobms.project.bms.domain.*;

/**
 * BBOX管理Controller
 *
 * @author 田志阳
 * @date 2025-11-15
 */
@RestController
@RequestMapping("/devices/devices")
@Api(tags = "BBOX设备管理")
public class BmsDevicesController extends BaseController
{
    @Autowired
    private IBmsDevicesService bmsDevicesService;

    @Autowired private IVehicleDataService vehicleDataService;
    @Autowired private IExtremeValuesService extremeValuesService;
    @Autowired private IBmsSystemStatusService bmsSystemStatusService;
    @Autowired private IBmsChargeDetailService bmsChargeDetailService;
    @Autowired private IBmsChargeTemperatureService bmsChargeTemperatureService;
    @Autowired private IBmsVoltageChannelDataService bmsVoltageChannelDataService;
    @Autowired private IBmsSopDataService bmsSopDataService;
    @Autowired private IVehicleLocationService vehicleLocationService;
    @Autowired private IBmsRuntimeDataService bmsRuntimeDataService;
    @Autowired private ISubsystemVoltageService subsystemVoltageService;
    @Autowired private ISubsystemTemperatureService subsystemTemperatureService;
    @Autowired private DeviceRealtimeHistoryAssembler deviceRealtimeHistoryAssembler;
    /**
     * 查询BBOX管理列表
     */
    @PreAuthorize("@ss.hasPermi('system:devices:list')")
    @GetMapping("/list")
    @ApiOperation("查询BBOX设备列表")
    public TableDataInfo list(BmsDevices bmsDevices)
    {
        startPage();
        List<BmsDevices> list = bmsDevicesService.selectBmsDevicesList(bmsDevices);
        return getDataTable(list);
    }

    @GetMapping("/history/{deviceId}")
    @ApiOperation(value = "运行历史计算器（充/放电统计）", response = ChargeDischargeHistoryDTO.class)
    public R<ChargeDischargeHistoryDTO> history(
            @ApiParam(value = "设备ID", required = true) @PathVariable("deviceId") String deviceId,
            @ApiParam(value = "开始时间戳(毫秒)", required = false) @RequestParam(value = "start", required = false) Long startMillis,
            @ApiParam(value = "结束时间戳(毫秒)", required = false) @RequestParam(value = "end", required = false) Long endMillis
    ) {
        Date end = endMillis == null ? new Date() : new Date(endMillis);
        Date start = startMillis == null ? new Date(end.getTime() - 30L * 86400_000L) : new Date(startMillis);
        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        SimpleDateFormat mf = new SimpleDateFormat("yyyy-MM");
        VehicleData q = new VehicleData();
        Map<String, Object> params = new HashMap<>();
        params.put("beginTime", df.format(start));
        params.put("endTime", df.format(end));
        q.setDeviceId(deviceId);
        q.setParams(params);
        List<VehicleData> all = vehicleDataService.selectVehicleDataList(q);
        all.removeIf(v -> v.getTimestamp() == null);
        all.sort(Comparator.comparing(VehicleData::getTimestamp));

        List<ChargeDischargeRecordDTO> records = new ArrayList<>();
        String currentStatus = null;
        int segmentStartIdx = -1;
        Integer prevSoc = null;
        for (int i = 0; i < all.size(); i++) {
            VehicleData v = all.get(i);
            Integer soc = v.getSoc();
            BigDecimal cur = v.getTotalCurrent();
            String status = null;
            if (cur != null) {
                int cmp = cur.compareTo(BigDecimal.ZERO);
                if (cmp > 0) status = "充电";
                else if (cmp < 0) status = "放电";
            }
            if (status == null && prevSoc != null && soc != null) {
                if (soc > prevSoc) status = "充电";
                else if (soc < prevSoc) status = "放电";
            }
            if (status == null && v.getChargingStatus() != null) {
                Integer st = v.getChargingStatus();
                if (st == 1 || st == 2) status = "充电";
                else if (st == 3) status = "放电";
            }
            if (status == null) {
                prevSoc = soc;
                continue;
            }
            if (currentStatus == null) {
                currentStatus = status;
                segmentStartIdx = i;
            } else if (!currentStatus.equals(status)) {
                // finalize previous segment
                ChargeDischargeRecordDTO rec = new ChargeDischargeRecordDTO();
                rec.setStatus(currentStatus);
                com.evobms.project.vehicledata.domain.VehicleData sv = all.get(segmentStartIdx);
                com.evobms.project.vehicledata.domain.VehicleData evd = all.get(i - 1);
                rec.setStartTime(df.format(sv.getTimestamp()));
                rec.setEndTime(df.format(evd.getTimestamp()));
                rec.setStartSoc(sv.getSoc());
                rec.setEndSoc(evd.getSoc());
                long seconds = (evd.getTimestamp().getTime() - sv.getTimestamp().getTime()) / 1000L;
                java.math.BigDecimal hours = new java.math.BigDecimal(seconds).divide(new java.math.BigDecimal(3600), 2, java.math.RoundingMode.HALF_UP);
                rec.setDurationHours(hours);
                // 忽略过短区段（小于5分钟）
                if (seconds >= 300) {
                    records.add(rec);
                }
                currentStatus = status;
                segmentStartIdx = i;
            }
            prevSoc = soc;
        }
        // finalize last
        if (currentStatus != null && segmentStartIdx >= 0 && all.size() - 1 >= segmentStartIdx) {
            ChargeDischargeRecordDTO rec = new ChargeDischargeRecordDTO();
            rec.setStatus(currentStatus);
            com.evobms.project.vehicledata.domain.VehicleData sv = all.get(segmentStartIdx);
            com.evobms.project.vehicledata.domain.VehicleData evd = all.get(all.size() - 1);
            rec.setStartTime(df.format(sv.getTimestamp()));
            rec.setEndTime(df.format(evd.getTimestamp()));
            rec.setStartSoc(sv.getSoc());
            rec.setEndSoc(evd.getSoc());
            long seconds = (evd.getTimestamp().getTime() - sv.getTimestamp().getTime()) / 1000L;
            java.math.BigDecimal hours = new java.math.BigDecimal(seconds).divide(new java.math.BigDecimal(3600), 2, java.math.RoundingMode.HALF_UP);
            rec.setDurationHours(hours);
            if (seconds >= 300) {
                records.add(rec);
            }
        }

        java.util.Map<String, java.math.BigDecimal[]> monthAgg = new java.util.HashMap<>();
        for (ChargeDischargeRecordDTO rec : records) {
            String m = rec.getStartTime().substring(0, 7);
            java.math.BigDecimal[] pair = monthAgg.computeIfAbsent(m, k -> new java.math.BigDecimal[]{java.math.BigDecimal.ZERO, java.math.BigDecimal.ZERO});
            if ("充电".equals(rec.getStatus())) {
                pair[0] = pair[0].add(rec.getDurationHours());
            } else {
                pair[1] = pair[1].add(rec.getDurationHours());
            }
        }
        java.util.List<ChargeDischargeMonthDTO> months = new java.util.ArrayList<>();
        java.util.List<String> keys = new java.util.ArrayList<>(monthAgg.keySet());
        keys.sort(String::compareTo);
        for (String k : keys) {
            ChargeDischargeMonthDTO m = new ChargeDischargeMonthDTO();
            m.setMonth(k);
            java.math.BigDecimal[] pair = monthAgg.get(k);
            m.setChargeHours(pair[0]);
            m.setDischargeHours(pair[1]);
            months.add(m);
        }

        ChargeDischargeHistoryDTO out = new ChargeDischargeHistoryDTO();
        out.setMonths(months);
        out.setRecords(records);
        return R.ok(out);
    }
    /**
     * 导出BBOX管理列表
     */
    @PreAuthorize("@ss.hasPermi('system:devices:export')")
    @Log(title = "BBOX管理", businessType = BusinessType.EXPORT)
    @PostMapping("/export")
    @ApiOperation("导出BBOX设备列表")
    public void export(HttpServletResponse response, BmsDevices bmsDevices)
    {
        List<BmsDevices> list = bmsDevicesService.selectBmsDevicesList(bmsDevices);
        ExcelUtil<BmsDevices> util = new ExcelUtil<BmsDevices>(BmsDevices.class);
        util.exportExcel(response, list, "BBOX管理数据");
    }

    /**
     * 获取BBOX管理详细信息
     */
    @PreAuthorize("@ss.hasPermi('system:devices:query')")
    @GetMapping(value = "/{id}")
    @ApiOperation("获取BBOX设备详细信息")
    public AjaxResult getInfo(@PathVariable("id") String id)
    {
        return success(bmsDevicesService.selectBmsDevicesById(id));
    }


    /** 子系统去重预取行数（按 timestamp 倒序，一般远小于子系统数×上报次数） */
    private static final int REALTIME_SUBSYSTEM_PREFETCH = 32;
    /** 24h 趋势曲线最多点数 */
    private static final int REALTIME_TREND_MAX_POINTS = 360;

    @GetMapping("/realtime/{deviceId}")
    @ApiOperation(value = "设备实时数据", notes = "默认不含 dashboard 趋势(24h)，避免超时；趋势请调 includeTrends=true 或 GET .../trends", response = DeviceRealtimeAggregateDTO.class)
    public R<DeviceRealtimeAggregateDTO> realtime(@PathVariable("deviceId") String deviceId, @ApiParam("是否包含 dashboard 24h 趋势（较慢，易触发前端 10s 超时）") @RequestParam(value = "includeTrends", required = false, defaultValue = "true") boolean includeTrends) {
        RealtimeSnapshot snap = loadRealtimeSnapshot(deviceId);
        DeviceRealtimeAggregateDTO agg = new DeviceRealtimeAggregateDTO();
        agg.setDeviceId(deviceId);
        fillAggregateFromSnapshot(agg, snap);
        if (includeTrends) {
            SimpleDateFormat tf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            Date now = new Date();
            Date trendBegin = new Date(now.getTime() - 24L * 3600_000L);
            agg.setDashboard(buildRealtimeDashboard(deviceId, snap.vehicleData, snap.extremeValues, snap.systemStatus, snap.chargeDetail, snap.chargeTemperatures, snap.voltageChannels, trendBegin, now, tf));
        }
        return R.ok(agg);
    }

    @GetMapping("/realtime/{deviceId}/trends")
    @ApiOperation(value = "设备实时 24h 趋势(dashboard)", notes = "与 realtime 拆分，避免与快照同请求导致 Axios 10s 超时", response = DeviceRealtimeDTO.class)
    public R<DeviceRealtimeDTO> realtimeTrends(
            @PathVariable("deviceId") String deviceId,
            @ApiParam("向前小时数，默认 24") @RequestParam(value = "hours", required = false, defaultValue = "24") int hours) {
        RealtimeSnapshot snap = loadRealtimeSnapshot(deviceId);
        SimpleDateFormat tf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date now = new Date();
        int h = hours <= 0 ? 24 : Math.min(hours, 168);
        Date trendBegin = new Date(now.getTime() - (long) h * 3600_000L);
        return R.ok(buildRealtimeDashboard(deviceId, snap.vehicleData, snap.extremeValues, snap.systemStatus,
                snap.chargeDetail, snap.chargeTemperatures, snap.voltageChannels, trendBegin, now, tf));
    }

    /**
     * 与 {@link #realtime(String)} 同源字段的历史平铺列表：时间范围内全部 vehicle_data 在库内分页（pageNum/pageSize），
     * 每页再与其它表按时间点合并；pageSize 仅支持 10、20、100、500。
     */
    @PreAuthorize("@ss.hasPermi('system:devices:query')")
    @GetMapping("/realtime/history/{deviceId}")
    @ApiOperation(value = "设备实时同源历史（平铺分页）", notes = "对 beginTime~endTime 内全部整车记录分页；参数 pageNum、pageSize(10|20|100|500)；total 为符合条件的总条数", response = DeviceHistoryFlatRowDTO.class)
    public TableDataInfo realtimeHistoryFlat(
            @ApiParam(value = "设备ID", required = true) @PathVariable("deviceId") String deviceId,
            @ApiParam(value = "开始时间戳(毫秒)，与 beginTime 二选一") @RequestParam(value = "start", required = false) Long startMillis,
            @ApiParam(value = "结束时间戳(毫秒)，与 endTime 二选一") @RequestParam(value = "end", required = false) Long endMillis,
            @ApiParam(value = "开始时间 yyyy-MM-dd HH:mm:ss") @RequestParam(value = "beginTime", required = false) String beginTimeStr,
            @ApiParam(value = "结束时间 yyyy-MM-dd HH:mm:ss") @RequestParam(value = "endTime", required = false) String endTimeStr,
            @ApiParam(value = "排序：desc 默认最新在前；asc 时间正序") @RequestParam(value = "order", required = false, defaultValue = "desc") String order,
            @ApiParam(value = "是否查询总条数 COUNT；大数据量可传 false 加快首屏") @RequestParam(value = "count", required = false, defaultValue = "true") boolean countTotal
    ) {
        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date end;
        Date start;
        boolean userRange = startMillis != null || (beginTimeStr != null && !beginTimeStr.isEmpty());
        try {
            end = parseHistoryEnd(endMillis, endTimeStr, df);
            start = parseHistoryStart(startMillis, beginTimeStr, df, end);
        } catch (ParseException e) {
            TableDataInfo err = new TableDataInfo();
            err.setCode(HttpStatus.ERROR);
            err.setMsg("时间解析失败，请使用 yyyy-MM-dd HH:mm:ss 或毫秒时间戳");
            err.setRows(Collections.emptyList());
            err.setTotal(0);
            return err;
        }
        boolean desc = !"asc".equalsIgnoreCase(order);
        if (!userRange) {
            start = clampHistoryStart(start, end);
        }
        int pageSize = com.evobms.common.utils.PageUtils.startHistoryPage(countTotal);
        List<VehicleData> vehicles = vehicleDataService.selectVehicleDataListForHistory(deviceId, start, end, desc);
        // total 必须来自 PageHelper 分页查询结果；不能用组装后的 rows（否则 total=当前页条数）
        long total = new com.github.pagehelper.PageInfo<>(vehicles).getTotal();
        SimpleDateFormat tf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        List<DeviceHistoryFlatRowDTO> rows = deviceRealtimeHistoryAssembler.build(deviceId, vehicles, tf, pageSize);
        TableDataInfo table = getDataTable(rows);
        table.setTotal(total);
        return table;
    }

    private static Date parseHistoryEnd(Long endMillis, String endTimeStr, SimpleDateFormat df) throws ParseException {
        if (endMillis != null) {
            return new Date(endMillis);
        }
        if (endTimeStr != null && !endTimeStr.isEmpty()) {
            return df.parse(endTimeStr);
        }
        return new Date();
    }

    private static Date parseHistoryStart(Long startMillis, String beginTimeStr, SimpleDateFormat df, Date end) throws ParseException {
        if (startMillis != null) {
            return new Date(startMillis);
        }
        if (beginTimeStr != null && !beginTimeStr.isEmpty()) {
            return df.parse(beginTimeStr);
        }
        return new Date(end.getTime() - 7L * 86400_000L);
    }

    /** 单次查询时间窗上限 7 天，避免一次扫过多行 */
    private static Date clampHistoryStart(Date start, Date end) {
        if (start == null || end == null) {
            return start;
        }
        long maxWindow = 7L * 86400_000L;
        if (end.getTime() - start.getTime() > maxWindow) {
            return new Date(end.getTime() - maxWindow);
        }
        return start;
    }

    /**
     * 新增BBOX管理
     */
    @PreAuthorize("@ss.hasPermi('system:devices:add')")
    @Log(title = "BBOX管理", businessType = BusinessType.INSERT)
    @PostMapping
    @ApiOperation("新增BBOX设备")
    public AjaxResult add(@RequestBody BmsDevices bmsDevices)
    {
        return toAjax(bmsDevicesService.insertBmsDevices(bmsDevices));
    }

    /**
     * 修改BBOX管理
     */
    @PreAuthorize("@ss.hasPermi('system:devices:edit')")
    @Log(title = "BBOX管理", businessType = BusinessType.UPDATE)
    @PutMapping
    @ApiOperation("修改BBOX设备")
    public AjaxResult edit(@RequestBody BmsDevices bmsDevices)
    {
        return toAjax(bmsDevicesService.updateBmsDevices(bmsDevices));
    }

    /**
     * 删除BBOX管理
     */
    @PreAuthorize("@ss.hasPermi('system:devices:remove')")
    @Log(title = "BBOX管理", businessType = BusinessType.DELETE)
    @DeleteMapping("/{ids}")
    @ApiOperation("删除BBOX设备")
    public AjaxResult remove(@PathVariable String[] ids)
    {
        return toAjax(bmsDevicesService.deleteBmsDevicesByIds(ids));
    }

    /** 扁平视图：与原始列表共用同一批查询结果，避免重复组装且趋势只在 dashboard 中暴露一份 */
    private DeviceRealtimeDTO buildRealtimeDashboard(
            String deviceId,
            VehicleData vd,
            ExtremeValues ev,
            BmsSystemStatus ss,
            BmsChargeDetail cd,
            List<BmsChargeTemperature> chargeTemps,
            List<BmsVoltageChannelData> voltageChs,
            Date trendBegin,
            Date trendEnd,
            SimpleDateFormat tf) {
        DeviceRealtimeDTO dto = new DeviceRealtimeDTO();
        dto.setDeviceId(deviceId);

        SystemStatusSection sys = new SystemStatusSection();
        if (vd != null) {
            sys.setTotalVoltage(vd.getTotalVoltage());
            sys.setTotalCurrent(vd.getTotalCurrent());
            sys.setSoc(vd.getSoc());
        }
        if (ev != null) {
            sys.setMaxCellVoltage(ev.getMaxVoltageValue());
            sys.setMaxCellPos(joinSubsysIndex(ev.getMaxVoltageSubsystemNo(), ev.getMaxVoltageBatteryNo()));
            sys.setMinCellVoltage(ev.getMinVoltageValue());
            sys.setMinCellPos(joinSubsysIndex(ev.getMinVoltageSubsystemNo(), ev.getMinVoltageBatteryNo()));
            sys.setMaxTemp(ev.getMaxTempValue());
            sys.setMaxTempPos(joinSubsysIndex(ev.getMaxTempSubsystemNo(), ev.getMaxTempProbeNo()));
            sys.setMinTemp(ev.getMinTempValue());
            sys.setMinTempPos(joinSubsysIndex(ev.getMinTempSubsystemNo(), ev.getMinTempProbeNo()));
        }
        dto.setSystemStatus(sys);

        RunStatusSection run = new RunStatusSection();
        if (vd != null) {
            run.setChargingStatus(vd.getChargingStatus());
        }
        if (ss != null) {
            run.setRelayStatus(ss.getRelayStatus());
            run.setOnaaccStatus(ss.getOnaaccStatus());
            run.setResetCount(ss.getResetCount());
            run.setCycleCount(ss.getCycleCount());
            run.setSample25v(ss.getSample25v());
            run.setSample5v(ss.getSample5v());
        }
        dto.setRunStatus(run);

        ChargeSection chg = new ChargeSection();
        if (cd != null) {
            chg.setAcChargeCurrent(cd.getAcChargeCurrent());
            chg.setAcChargeVoltage(cd.getAcChargeVoltage());
            chg.setDcChargeCurrent(cd.getDcChargeCurrent());
            chg.setDcChargeVoltage(cd.getDcChargeVoltage());
            chg.setChargeTime(cd.getChargeTime());
            chg.setAcStopCode(cd.getAcStopCode());
            chg.setDcStopCode(cd.getDcStopCode());
            chg.setAcCpValue(cd.getAcCpValue());
            chg.setAcGunResistance(cd.getAcGunResistance());
            chg.setChargingStage(cd.getChargingStage());
            chg.setAcStage(cd.getAcStage());
            chg.setDcStage(cd.getDcStage());
        }
        dto.setCharge(chg);

        StatisticsSection stat = new StatisticsSection();
        if (ss != null) {
            stat.setCycleCount(ss.getCycleCount());
        }
        stat.setTotalRunHours(0L);
        stat.setTotalChargeHours(0L);
        stat.setTotalDischargeHours(0L);
        dto.setStatistics(stat);

        dto.setTemperatures(toChargeTemperatureNameValues(chargeTemps, tf));
        dto.setVoltages(toVoltageChannelNameValues(voltageChs, tf));
        dto.setFaults(new ArrayList<>());
        dto.setVoltageTrend(buildRtp1VoltageTrendSeries(deviceId, trendBegin, trendEnd, tf));
        dto.setTemperatureTrend(buildT1TemperatureTrendSeries(deviceId, trendBegin, trendEnd, tf));

        List<VehicleData> vehicleTrendRows = listVehicleDataForTrend(deviceId, trendBegin, trendEnd);
        dto.setTotalVoltageTrend(Collections.singletonList(
                buildVehicleNumericTrendSeries(vehicleTrendRows, "总电压(V)", trendBegin, trendEnd, tf, VehicleData::getTotalVoltage)));
        dto.setTotalCurrentTrend(Collections.singletonList(
                buildVehicleNumericTrendSeries(vehicleTrendRows, "总电流(A)", trendBegin, trendEnd, tf, VehicleData::getTotalCurrent)));
        dto.setSocTrend(Collections.singletonList(
                buildVehicleNumericTrendSeries(vehicleTrendRows, "SOC(%)", trendBegin, trendEnd, tf,
                        v -> v.getSoc() == null ? null : BigDecimal.valueOf(v.getSoc()))));
        return dto;
    }

    /** 趋势图：DB 侧限量采样，避免 24h 全量拉取 */
    private List<VehicleData> listVehicleDataForTrend(String deviceId, Date begin, Date end) {
        return vehicleDataService.selectVehicleDataTrendInRange(deviceId, begin, end, REALTIME_TREND_MAX_POINTS);
    }

    private static Date vehicleTrendPointTime(VehicleData v) {
        if (v == null) {
            return null;
        }
        return v.getTimestamp() != null ? v.getTimestamp() : v.getCreateTime();
    }

    private static LineSeriesDTO buildVehicleNumericTrendSeries(
            List<VehicleData> rows,
            String seriesName,
            Date rangeBegin,
            Date rangeEnd,
            SimpleDateFormat tf,
            Function<VehicleData, BigDecimal> valueFn) {
        List<TimePointDTO> points = new ArrayList<>();
        if (rows != null) {
            for (VehicleData v : rows) {
                Date t = vehicleTrendPointTime(v);
                if (t == null || t.before(rangeBegin) || t.after(rangeEnd)) {
                    continue;
                }
                BigDecimal val = valueFn.apply(v);
                if (val == null) {
                    continue;
                }
                TimePointDTO p = new TimePointDTO();
                p.setTime(tf.format(t));
                p.setValue(val);
                points.add(p);
            }
        }
        LineSeriesDTO series = new LineSeriesDTO();
        series.setName(seriesName);
        series.setPoints(points);
        return series;
    }

    private static String joinSubsysIndex(Integer a, Integer b) {
        if (a == null || b == null) {
            return null;
        }
        return a + "-" + b;
    }

    private static List<NameValueDTO> toChargeTemperatureNameValues(List<BmsChargeTemperature> temps, SimpleDateFormat tf) {
        if (temps == null || temps.isEmpty()) {
            return new ArrayList<>();
        }
        List<NameValueDTO> out = new ArrayList<>(temps.size());
        for (BmsChargeTemperature t : temps) {
            NameValueDTO nv = new NameValueDTO();
            nv.setName("T" + t.getTempIndex());
            nv.setValue(t.getTemperature() == null ? 0 : t.getTemperature().longValue());
            nv.setCreateTime(formatTime(t.getCreateTime(), tf));
            out.add(nv);
        }
        return out;
    }

    private static List<NameValueDTO> toVoltageChannelNameValues(List<BmsVoltageChannelData> chs, SimpleDateFormat tf) {
        if (chs == null || chs.isEmpty()) {
            return new ArrayList<>();
        }
        List<NameValueDTO> out = new ArrayList<>(chs.size());
        for (BmsVoltageChannelData c : chs) {
            NameValueDTO nv = new NameValueDTO();
            nv.setName((c.getChannelType() == null ? "CH" : c.getChannelType()) + "-" + c.getChannelIndex());
            nv.setValue(c.getVoltage() == null ? 0 : c.getVoltage().longValue());
            nv.setCreateTime(formatTime(c.getCreateTime(), tf));
            out.add(nv);
        }
        return out;
    }

    private static String formatTime(Date d, SimpleDateFormat tf) {
        return d == null ? null : tf.format(d);
    }

    private List<LineSeriesDTO> buildRtp1VoltageTrendSeries(String deviceId, Date begin, Date end, SimpleDateFormat tf) {
        List<BmsVoltageChannelData> rows = bmsVoltageChannelDataService.lambdaQuery()
                .eq(BmsVoltageChannelData::getDeviceId, deviceId)
                .eq(BmsVoltageChannelData::getChannelType, "RTP")
                .eq(BmsVoltageChannelData::getChannelIndex, 1)
                .isNotNull(BmsVoltageChannelData::getTimestamp)
                .ge(BmsVoltageChannelData::getTimestamp, begin)
                .le(BmsVoltageChannelData::getTimestamp, end)
                .orderByDesc(BmsVoltageChannelData::getTimestamp)
                .last("limit " + REALTIME_TREND_MAX_POINTS)
                .list();
        Collections.reverse(rows);
        List<TimePointDTO> points = new ArrayList<>(rows.size());
        for (BmsVoltageChannelData it : rows) {
            TimePointDTO p = new TimePointDTO();
            Date t = it.getTimestamp() != null ? it.getTimestamp() : it.getCreateTime();
            p.setTime(formatTime(t, tf));
            p.setValue(it.getVoltage());
            points.add(p);
        }
        LineSeriesDTO series = new LineSeriesDTO();
        series.setName("RTP-1");
        series.setPoints(points);
        return Collections.singletonList(series);
    }

    private List<LineSeriesDTO> buildT1TemperatureTrendSeries(String deviceId, Date begin, Date end, SimpleDateFormat tf) {
        List<BmsChargeTemperature> rows = bmsChargeTemperatureService.lambdaQuery()
                .eq(BmsChargeTemperature::getDeviceId, deviceId)
                .isNotNull(BmsChargeTemperature::getTimestamp)
                .ge(BmsChargeTemperature::getTimestamp, begin)
                .le(BmsChargeTemperature::getTimestamp, end)
                .eq(BmsChargeTemperature::getTempIndex, 1)
                .orderByDesc(BmsChargeTemperature::getTimestamp)
                .last("limit " + REALTIME_TREND_MAX_POINTS)
                .list();
        Collections.reverse(rows);
        List<TimePointDTO> points = new ArrayList<>(rows.size());
        for (BmsChargeTemperature it : rows) {
            TimePointDTO p = new TimePointDTO();
            Date t = it.getTimestamp() != null ? it.getTimestamp() : it.getCreateTime();
            p.setTime(formatTime(t, tf));
            p.setValue(it.getTemperature());
            points.add(p);
        }
        LineSeriesDTO series = new LineSeriesDTO();
        series.setName("T1");
        series.setPoints(points);
        return Collections.singletonList(series);
    }

    /** 并行加载实时快照各表最新数据（默认不含 24h 趋势 SQL） */
    private RealtimeSnapshot loadRealtimeSnapshot(String deviceId) {
        CompletableFuture<VehicleData> vehicleF = CompletableFuture.supplyAsync(
                () -> vehicleDataService.selectLatestByDeviceId(deviceId));
        CompletableFuture<ExtremeValues> extremeF = CompletableFuture.supplyAsync(
                () -> extremeValuesService.selectLatestByDeviceId(deviceId));
        CompletableFuture<VehicleLocation> locationF = CompletableFuture.supplyAsync(
                () -> vehicleLocationService.selectLatestByDeviceId(deviceId));
        CompletableFuture<BmsSystemStatus> systemF = CompletableFuture.supplyAsync(
                () -> latestSystemStatus(deviceId));
        CompletableFuture<BmsChargeDetail> chargeDetailF = CompletableFuture.supplyAsync(
                () -> latestChargeDetail(deviceId));
        CompletableFuture<BmsRuntimeData> runtimeF = CompletableFuture.supplyAsync(
                () -> latestRuntimeData(deviceId));
        CompletableFuture<BmsSopData> sopF = CompletableFuture.supplyAsync(
                () -> latestSopData(deviceId));
        CompletableFuture<List<SubsystemVoltage>> svF = CompletableFuture.supplyAsync(
                () -> listLatestSubsystemVoltages(deviceId));
        CompletableFuture<List<SubsystemTemperature>> stF = CompletableFuture.supplyAsync(
                () -> listLatestSubsystemTemperatures(deviceId));
        CompletableFuture<List<BmsChargeTemperature>> chargeTempF = CompletableFuture.supplyAsync(
                () -> listLatestChargeTemperatures(deviceId));
        CompletableFuture<List<BmsVoltageChannelData>> voltageChF = CompletableFuture.supplyAsync(
                () -> listLatestVoltageChannels(deviceId));

        CompletableFuture.allOf(vehicleF, extremeF, locationF, systemF, chargeDetailF, runtimeF, sopF,
                svF, stF, chargeTempF, voltageChF).join();

        RealtimeSnapshot snap = new RealtimeSnapshot();
        snap.vehicleData = vehicleF.join();
        snap.extremeValues = extremeF.join();
        snap.latestLocation = locationF.join();
        snap.systemStatus = systemF.join();
        snap.chargeDetail = chargeDetailF.join();
        snap.runtimeData = runtimeF.join();
        snap.sopData = sopF.join();
        snap.subsystemVoltageRows = svF.join();
        snap.subsystemTemperatureRows = stF.join();
        snap.chargeTemperatures = chargeTempF.join();
        snap.voltageChannels = voltageChF.join();
        return snap;
    }

    private static void fillAggregateFromSnapshot(DeviceRealtimeAggregateDTO agg, RealtimeSnapshot snap) {
        agg.setVehicleData(snap.vehicleData);
        agg.setLatestLocation(snap.latestLocation);
        agg.setExtremeValues(snap.extremeValues);
        agg.setSystemStatus(snap.systemStatus);
        agg.setChargeDetail(snap.chargeDetail);
        agg.setRuntimeData(snap.runtimeData);
        agg.setSopData(snap.sopData);
        agg.setChargeTemperatures(snap.chargeTemperatures);
        agg.setVoltageChannels(snap.voltageChannels);
        List<SubsystemVoltage> svRows = snap.subsystemVoltageRows;
        agg.setSubsystemVoltage(svRows == null || svRows.isEmpty() ? null : svRows.get(0));
        agg.setSubsystemVoltages(latestOnePerSubsystem(svRows, SubsystemVoltage::getSubsystemNo));
        List<SubsystemTemperature> stRows = snap.subsystemTemperatureRows;
        agg.setSubsystemTemperature(stRows == null || stRows.isEmpty() ? null : stRows.get(0));
        agg.setSubsystemTemperatures(latestOnePerSubsystem(stRows, SubsystemTemperature::getSubsystemNo));
    }

    private BmsSystemStatus latestSystemStatus(String deviceId) {
        return bmsSystemStatusService.lambdaQuery()
                .eq(BmsSystemStatus::getDeviceId, deviceId)
                .isNotNull(BmsSystemStatus::getTimestamp)
                .orderByDesc(BmsSystemStatus::getTimestamp)
                .last("limit 1")
                .one();
    }

    private BmsChargeDetail latestChargeDetail(String deviceId) {
        return bmsChargeDetailService.lambdaQuery()
                .eq(BmsChargeDetail::getDeviceId, deviceId)
                .isNotNull(BmsChargeDetail::getTimestamp)
                .orderByDesc(BmsChargeDetail::getTimestamp)
                .last("limit 1")
                .one();
    }

    private BmsRuntimeData latestRuntimeData(String deviceId) {
        return bmsRuntimeDataService.lambdaQuery()
                .eq(BmsRuntimeData::getDeviceId, deviceId)
                .isNotNull(BmsRuntimeData::getTimestamp)
                .orderByDesc(BmsRuntimeData::getTimestamp)
                .last("limit 1")
                .one();
    }

    private BmsSopData latestSopData(String deviceId) {
        return bmsSopDataService.lambdaQuery()
                .eq(BmsSopData::getDeviceId, deviceId)
                .isNotNull(BmsSopData::getTimestamp)
                .orderByDesc(BmsSopData::getTimestamp)
                .last("limit 1")
                .one();
    }

    private List<SubsystemVoltage> listLatestSubsystemVoltages(String deviceId) {
        return subsystemVoltageService.lambdaQuery()
                .eq(SubsystemVoltage::getDeviceId, deviceId)
                .isNotNull(SubsystemVoltage::getTimestamp)
                .orderByDesc(SubsystemVoltage::getTimestamp)
                .last("limit " + REALTIME_SUBSYSTEM_PREFETCH)
                .list();
    }

    private List<SubsystemTemperature> listLatestSubsystemTemperatures(String deviceId) {
        return subsystemTemperatureService.lambdaQuery()
                .eq(SubsystemTemperature::getDeviceId, deviceId)
                .isNotNull(SubsystemTemperature::getTimestamp)
                .orderByDesc(SubsystemTemperature::getTimestamp)
                .last("limit " + REALTIME_SUBSYSTEM_PREFETCH)
                .list();
    }

    private List<BmsChargeTemperature> listLatestChargeTemperatures(String deviceId) {
        return bmsChargeTemperatureService.lambdaQuery()
                .eq(BmsChargeTemperature::getDeviceId, deviceId)
                .isNotNull(BmsChargeTemperature::getTimestamp)
                .orderByDesc(BmsChargeTemperature::getTimestamp)
                .last("limit 8")
                .list();
    }

    private List<BmsVoltageChannelData> listLatestVoltageChannels(String deviceId) {
        return bmsVoltageChannelDataService.lambdaQuery()
                .eq(BmsVoltageChannelData::getDeviceId, deviceId)
                .isNotNull(BmsVoltageChannelData::getTimestamp)
                .orderByDesc(BmsVoltageChannelData::getTimestamp)
                .last("limit 16")
                .list();
    }

    private static final class RealtimeSnapshot {
        VehicleData vehicleData;
        ExtremeValues extremeValues;
        VehicleLocation latestLocation;
        BmsSystemStatus systemStatus;
        BmsChargeDetail chargeDetail;
        BmsRuntimeData runtimeData;
        BmsSopData sopData;
        List<SubsystemVoltage> subsystemVoltageRows;
        List<SubsystemTemperature> subsystemTemperatureRows;
        List<BmsChargeTemperature> chargeTemperatures;
        List<BmsVoltageChannelData> voltageChannels;
    }

    /** 从按时间倒序的结果中取每个子系统号的第一条（即该子系统最新数据） */
    private static <T> List<T> latestOnePerSubsystem(List<T> orderedNewestFirst, Function<T, Integer> subsystemNoFn) {
        if (orderedNewestFirst == null || orderedNewestFirst.isEmpty()) {
            return Collections.emptyList();
        }
        Map<Integer, T> seen = new LinkedHashMap<>();
        for (T row : orderedNewestFirst) {
            Integer no = subsystemNoFn.apply(row);
            if (no != null && !seen.containsKey(no)) {
                seen.put(no, row);
            }
        }
        List<T> out = new ArrayList<>(seen.values());
        out.sort(Comparator.comparing(subsystemNoFn, Comparator.nullsLast(Integer::compareTo)));
        return out;
    }
}
