package com.lingniu.ingest.eventhistory;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.lingniu.ingest.protocol.gb32960.codec.Gb32960MessageDecoder;
import com.lingniu.ingest.protocol.gb32960.model.Gb32960Message;
import com.lingniu.ingest.protocol.gb32960.model.InfoBlock;
import com.lingniu.ingest.tdenginehistory.TdengineHistoryReader;
import com.lingniu.ingest.tdenginehistory.TdenginePage;
import com.lingniu.ingest.tdenginehistory.TdenginePageCursor;
import com.lingniu.ingest.tdenginehistory.TdengineQueryOrder;
import com.lingniu.ingest.tdenginehistory.TdengineRawFrameQuery;
import com.lingniu.ingest.tdenginehistory.TdengineRawFrameRow;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;

/**
 * GB32960 RAW 历史帧查询和业务快照组装服务。
 *
 * <p>TDengine raw_frames 保存 archive:// URI。查询时先从 TDengine 找到 RAW 索引，
 * 再读取本地 RAW .bin，用当前协议解析器即时解码。
 * 这种设计让写入路径保持轻量，也允许后续修复解析器后直接重放历史 RAW 得到新字段。
 *
 * <p>snapshot 的聚合单位是 {@code vin + eventTime}。同一时刻可能有多个 32960 子包，
 * 比如广东燃料电池堆电压分帧；这里会把这些子包合并成前端可消费的单个逻辑快照。
 */
public final class Gb32960DecodedFrameService {

    private static final Logger log = LoggerFactory.getLogger(Gb32960DecodedFrameService.class);
    private static final TypeReference<Map<String, Object>> MAP_TYPE = new TypeReference<>() {};
    private static final int OUTPUT_DOUBLE_SCALE = 6;
    private static final ZoneId DEFAULT_ZONE = ZoneId.of("Asia/Shanghai");

    private final TdengineHistoryReader tdengineReader;
    private final Gb32960MessageDecoder decoder;
    private final Path archiveRoot;
    private final ObjectMapper objectMapper;

    public Gb32960DecodedFrameService(TdengineHistoryReader tdengineReader,
                                      Gb32960MessageDecoder decoder,
                                      Path archiveRoot,
                                      ObjectMapper objectMapper) {
        if (tdengineReader == null) {
            log.info("gb32960 decoded frame service has no history index backend; rawUri direct lookup remains available");
        } else {
            log.info("gb32960 decoded frame service using TDengine raw_frames index");
        }
        if (decoder == null) {
            throw new IllegalArgumentException("decoder must not be null");
        }
        if (archiveRoot == null) {
            throw new IllegalArgumentException("archiveRoot must not be null");
        }
        this.tdengineReader = tdengineReader;
        this.decoder = decoder;
        this.archiveRoot = archiveRoot.toAbsolutePath().normalize();
        this.objectMapper = objectMapper == null ? new ObjectMapper() : objectMapper;
    }

    public List<DecodedFrame> query(LocalDate dateFrom,
                                    LocalDate dateTo,
                                    HistoryQueryOrder order,
                                    int limit,
                                    String vin,
                                    String platformAccount) throws IOException {
        return query(dateFrom, dateTo, null, null, order, limit, vin, platformAccount);
    }

    public List<DecodedFrame> query(LocalDate dateFrom,
                                    LocalDate dateTo,
                                    java.time.Instant eventTimeFrom,
                                    java.time.Instant eventTimeTo,
                                    HistoryQueryOrder order,
                                    int limit,
                                    String vin,
                                    String platformAccount) throws IOException {
        int frameLimit = Math.max(1, Math.min(limit, 1000));
        if (tdengineReader == null) {
            return List.of();
        }
        return queryFromTdengine(dateFrom, dateTo, eventTimeFrom, eventTimeTo, order, frameLimit, vin, platformAccount);
    }

    public DecodedFramePage queryPage(QueryTimeRange range,
                                      HistoryQueryOrder order,
                                      int limit,
                                      String vin,
                                      String platformAccount,
                                      TdenginePageCursor cursor) throws IOException {
        int frameLimit = Math.max(1, Math.min(limit, 1000));
        if (tdengineReader == null || vin == null || vin.isBlank()) {
            return new DecodedFramePage(List.of(), null);
        }
        return queryPageFromTdengine(
                range.dateFrom(),
                range.dateTo(),
                range.eventTimeFrom(),
                range.eventTimeTo(),
                order,
                frameLimit,
                vin,
                platformAccount,
                cursor);
    }

    private List<DecodedFrame> queryFromTdengine(LocalDate dateFrom,
                                                 LocalDate dateTo,
                                                 java.time.Instant eventTimeFrom,
                                                 java.time.Instant eventTimeTo,
                                                 HistoryQueryOrder order,
                                                 int frameLimit,
                                                 String vin,
                                                 String platformAccount) throws IOException {
        String vehicleKey = vin == null ? "" : vin.trim();
        TdenginePage<TdengineRawFrameRow> page = tdengineReader.queryRawFrames(new TdengineRawFrameQuery(
                "GB32960",
                vehicleKey,
                fromInstant(dateFrom, eventTimeFrom),
                toExclusiveInstant(dateTo, eventTimeTo),
                tdengineOrder(order),
                frameLimit,
                null));
        List<DecodedFrame> out = new ArrayList<>(page.items().size());
        LinkedHashSet<String> seenUris = new LinkedHashSet<>();
        for (TdengineRawFrameRow row : page.items()) {
            String rawArchiveUri = row.rawUri();
            if (rawArchiveUri == null || rawArchiveUri.isBlank() || !seenUris.add(rawArchiveUri)) {
                continue;
            }
            DecodedFrame frame = decodeIfAvailable(row, platformAccount);
            if (frame != null) {
                out.add(frame);
            }
        }
        return out;
    }

    private DecodedFramePage queryPageFromTdengine(LocalDate dateFrom,
                                                   LocalDate dateTo,
                                                   java.time.Instant eventTimeFrom,
                                                   java.time.Instant eventTimeTo,
                                                   HistoryQueryOrder order,
                                                   int frameLimit,
                                                   String vin,
                                                   String platformAccount,
                                                   TdenginePageCursor cursor) throws IOException {
        TdenginePage<TdengineRawFrameRow> page = tdengineReader.queryRawFrames(new TdengineRawFrameQuery(
                "GB32960",
                vin.trim(),
                fromInstant(dateFrom, eventTimeFrom),
                toExclusiveInstant(dateTo, eventTimeTo),
                tdengineOrder(order),
                frameLimit,
                cursor));
        List<DecodedFrame> out = new ArrayList<>(page.items().size());
        LinkedHashSet<String> seenUris = new LinkedHashSet<>();
        for (TdengineRawFrameRow row : page.items()) {
            String rawArchiveUri = row.rawUri();
            if (rawArchiveUri == null || rawArchiveUri.isBlank() || !seenUris.add(rawArchiveUri)) {
                continue;
            }
            DecodedFrame frame = decodeIfAvailable(row, platformAccount);
            if (frame != null) {
                out.add(frame);
            }
        }
        CursorResponse next = page.nextCursor()
                .map(value -> new CursorResponse(value.ts().toString(), value.tieBreaker()))
                .orElse(null);
        return new DecodedFramePage(out, next);
    }

    public List<LogicalSnapshot> snapshots(QueryTimeRange range,
                                           HistoryQueryOrder order,
                                           int limit,
                                           String vin,
                                           String platformAccount) throws IOException {
        return snapshots(range.dateFrom(), range.dateTo(), range.eventTimeFrom(), range.eventTimeTo(),
                order, limit, vin, platformAccount);
    }

    public List<FieldSnapshot> snapshotFields(QueryTimeRange range,
                                              HistoryQueryOrder order,
                                              int limit,
                                              String vin,
                                              String platformAccount,
                                              String fields) throws IOException {
        List<String> selectedFields = parseSelectedFields(fields);
        if (selectedFields.isEmpty()) {
            throw new IllegalArgumentException("fields is required for gb32960 snapshot field query");
        }
        validateSelectedFields(selectedFields);
        return snapshots(range, order, limit, vin, platformAccount).stream()
                .map(snapshot -> FieldSnapshot.from(snapshot, selectedFields))
                .toList();
    }

    public String snapshotFieldsCsv(QueryTimeRange range,
                                    HistoryQueryOrder order,
                                    int limit,
                                    String vin,
                                    String platformAccount,
                                    String fields) throws IOException {
        List<String> selectedFields = parseSelectedFields(fields);
        if (selectedFields.isEmpty()) {
            throw new IllegalArgumentException("fields is required for gb32960 snapshot field query");
        }
        validateSelectedFields(selectedFields);
        List<FieldSnapshot> snapshots = snapshots(range, order, limit, vin, platformAccount).stream()
                .map(snapshot -> FieldSnapshot.from(snapshot, selectedFields))
                .toList();
        Map<String, String> headers = chineseHeadersByField();
        StringBuilder csv = new StringBuilder(256 + snapshots.size() * Math.max(128, selectedFields.size() * 32));
        csv.append("VIN,事件时间,首次接收时间,最后接收时间,原始包");
        for (String field : selectedFields) {
            csv.append(',').append(csv(headers.getOrDefault(field, field)));
        }
        csv.append('\n');
        for (FieldSnapshot snapshot : snapshots) {
            csv.append(csv(snapshot.vin())).append(',')
                    .append(csv(snapshot.eventTime())).append(',')
                    .append(csv(snapshot.ingestTimeFirst())).append(',')
                    .append(csv(snapshot.ingestTimeLast())).append(',')
                    .append(csv(sourceFrameUris(snapshot.sourceFrames())));
            for (String field : selectedFields) {
                csv.append(',').append(csv(csvValue(snapshot.fields().get(field))));
            }
            csv.append('\n');
        }
        return csv.toString();
    }

    public DecodedFrame findByRawArchiveUri(String rawArchiveUri, String platformAccount) throws IOException {
        if (rawArchiveUri == null || rawArchiveUri.isBlank()) {
            return null;
        }
        return decodeWithoutIndex(rawArchiveUri, platformAccount);
    }

    public List<LogicalSnapshot> snapshots(LocalDate dateFrom,
                                           LocalDate dateTo,
                                           HistoryQueryOrder order,
                                           int limit,
                                           String vin,
                                           String platformAccount) throws IOException {
        if (vin == null || vin.isBlank()) {
            throw new IllegalArgumentException("vin is required for gb32960 snapshots");
        }
        return snapshots(dateFrom, dateTo, null, null, order, limit, vin, platformAccount);
    }

    private List<LogicalSnapshot> snapshots(LocalDate dateFrom,
                                            LocalDate dateTo,
                                            java.time.Instant eventTimeFrom,
                                            java.time.Instant eventTimeTo,
                                            HistoryQueryOrder order,
                                            int limit,
                                            String vin,
                                            String platformAccount) throws IOException {
        if (vin == null || vin.isBlank()) {
            throw new IllegalArgumentException("vin is required for gb32960 snapshots");
        }
        int snapshotLimit = Math.max(1, Math.min(limit, 1000));
        // 经验上一个完整 snapshot 可能由多个 RAW 子包组成，查询帧数需要放大后再按 snapshot 截断。
        List<DecodedFrame> frames = query(dateFrom, dateTo, eventTimeFrom, eventTimeTo,
                order, snapshotLimit * 30, vin, platformAccount);
        LinkedHashMap<String, SnapshotBuilder> builders = new LinkedHashMap<>();
        for (DecodedFrame frame : frames) {
            if (frame.eventTime() == null || frame.eventTime().isBlank()) {
                continue;
            }
            String key = frame.vin() + "|" + frame.eventTime();
            builders.computeIfAbsent(key, ignored -> new SnapshotBuilder(frame)).add(frame);
        }
        List<LogicalSnapshot> out = new ArrayList<>(snapshotLimit);
        for (SnapshotBuilder builder : builders.values()) {
            out.add(builder.build());
            if (out.size() >= snapshotLimit) {
                break;
            }
        }
        return out;
    }

    private DecodedFrame decodeIfAvailable(TdengineRawFrameRow row, String platformAccount) throws IOException {
        try {
            return decode(row, platformAccount);
        } catch (NoSuchFileException e) {
            log.warn("skip gb32960 frame because raw archive is missing frameId={} rawArchiveUri={}",
                    row.frameId(), row.rawUri());
            return null;
        }
    }

    private DecodedFrame decode(TdengineRawFrameRow row, String platformAccount) throws IOException {
        Path rawPath = archivePath(row.rawUri());
        byte[] rawBytes = Files.readAllBytes(rawPath);
        Gb32960Message message = decoder.decode(ByteBuffer.wrap(rawBytes), platformAccount(row, platformAccount));
        List<Map<String, Object>> blocks = new ArrayList<>();
        for (InfoBlock block : message.infoBlocks()) {
            blocks.add(blockJson(block));
        }
        Instant ingestTime = row.receivedAt() == null ? Files.getLastModifiedTime(rawPath).toInstant() : row.receivedAt();
        return new DecodedFrame(
                row.frameId(),
                message.header().vin(),
                message.header().command().name(),
                message.header().responseFlag().name(),
                message.header().protocolVersion().name(),
                message.header().encryptType().name(),
                message.header().dataLength(),
                message.header().eventTime() == null ? null : message.header().eventTime().toString(),
                ingestTime.toString(),
                row.rawUri(),
                rawBytes.length,
                blocks);
    }

    private DecodedFrame decodeWithoutIndex(String rawArchiveUri, String platformAccount) throws IOException {
        Path rawPath = archivePath(rawArchiveUri);
        byte[] rawBytes = Files.readAllBytes(rawPath);
        Gb32960Message message = decoder.decode(ByteBuffer.wrap(rawBytes), platformAccount);
        List<Map<String, Object>> blocks = new ArrayList<>();
        for (InfoBlock block : message.infoBlocks()) {
            blocks.add(blockJson(block));
        }
        String ingestTime = Files.getLastModifiedTime(rawPath).toInstant().toString();
        return new DecodedFrame(
                rawArchiveEventId(rawArchiveUri),
                message.header().vin(),
                message.header().command().name(),
                message.header().responseFlag().name(),
                message.header().protocolVersion().name(),
                message.header().encryptType().name(),
                message.header().dataLength(),
                message.header().eventTime() == null ? null : message.header().eventTime().toString(),
                ingestTime,
                rawArchiveUri,
                rawBytes.length,
                blocks);
    }

    private Map<String, Object> blockJson(InfoBlock block) {
        Map<String, Object> json = new LinkedHashMap<>();
        json.put("type", block.type().name());
        json.put("className", block.getClass().getSimpleName());
        json.put("data", normalizeJsonValue(objectMapper.convertValue(block, MAP_TYPE)));
        if (block instanceof InfoBlock.Raw raw) {
            json.put("typeCode", raw.typeCode());
            json.put("bytesHex", hex(raw.bytes()));
        }
        return json;
    }

    private static List<String> parseSelectedFields(String fields) {
        if (fields == null || fields.isBlank()) {
            return List.of();
        }
        List<String> out = new ArrayList<>();
        for (String field : fields.split(",")) {
            String normalized = field.trim();
            if (!normalized.isBlank()) {
                out.add(normalized);
            }
        }
        return List.copyOf(out);
    }

    private static void validateSelectedFields(List<String> selectedFields) {
        LinkedHashSet<String> knownFields = new LinkedHashSet<>();
        for (Gb32960FieldDictionary.Packet packet : Gb32960FieldDictionary.packets()) {
            for (Gb32960FieldDictionary.Field field : packet.fields()) {
                knownFields.add(packet.code() + "." + field.key());
            }
        }
        List<String> unknown = selectedFields.stream()
                .filter(field -> !knownFields.contains(field))
                .toList();
        if (!unknown.isEmpty()) {
            throw new IllegalArgumentException("unknown gb32960 snapshot fields: " + String.join(",", unknown));
        }
    }

    private static Map<String, String> chineseHeadersByField() {
        Map<String, String> headers = new LinkedHashMap<>();
        for (Gb32960FieldDictionary.Packet packet : Gb32960FieldDictionary.packets()) {
            for (Gb32960FieldDictionary.Field field : packet.fields()) {
                String header = packet.nameZh() + "-" + field.nameZh();
                if (field.unit() != null && !field.unit().isBlank()) {
                    header += "(" + field.unit() + ")";
                }
                headers.put(packet.code() + "." + field.key(), header);
            }
        }
        return headers;
    }

    private static String sourceFrameUris(List<SourceFrame> sourceFrames) {
        if (sourceFrames == null || sourceFrames.isEmpty()) {
            return "";
        }
        return sourceFrames.stream()
                .map(SourceFrame::rawArchiveUri)
                .filter(uri -> uri != null && !uri.isBlank())
                .reduce((left, right) -> left + ";" + right)
                .orElse("");
    }

    private static String csvValue(Object value) {
        if (value == null) {
            return "";
        }
        if (value instanceof Iterable<?> iterable) {
            List<String> values = new ArrayList<>();
            for (Object item : iterable) {
                values.add(String.valueOf(item));
            }
            return String.join(";", values);
        }
        return String.valueOf(value);
    }

    private static String csv(String value) {
        if (value == null) {
            return "";
        }
        boolean quote = value.indexOf(',') >= 0
                || value.indexOf('"') >= 0
                || value.indexOf('\n') >= 0
                || value.indexOf('\r') >= 0;
        if (!quote) {
            return value;
        }
        return "\"" + value.replace("\"", "\"\"") + "\"";
    }

    static Object normalizeJsonValue(Object value) {
        if (value instanceof Double d) {
            return normalizeFloatingPoint(d);
        }
        if (value instanceof Float f) {
            return normalizeFloatingPoint(f.doubleValue());
        }
        if (value instanceof Map<?, ?> map) {
            Map<String, Object> normalized = new LinkedHashMap<>();
            for (Map.Entry<?, ?> entry : map.entrySet()) {
                normalized.put(String.valueOf(entry.getKey()), normalizeJsonValue(entry.getValue()));
            }
            return normalized;
        }
        if (value instanceof List<?> list) {
            List<Object> normalized = new ArrayList<>(list.size());
            for (Object item : list) {
                normalized.add(normalizeJsonValue(item));
            }
            return normalized;
        }
        return value;
    }

    private static BigDecimal normalizeFloatingPoint(double value) {
        if (!Double.isFinite(value)) {
            return BigDecimal.ZERO;
        }
        // 协议缩放后的 double 容易出现 12.300000000000002；对外统一保留 6 位并去掉尾零。
        BigDecimal normalized = BigDecimal.valueOf(value)
                .setScale(OUTPUT_DOUBLE_SCALE, RoundingMode.HALF_UP)
                .stripTrailingZeros();
        return normalized.scale() < 0 ? normalized.setScale(0) : normalized;
    }

    @SuppressWarnings("unchecked")
    private Map<String, Object> deepCopy(Map<String, Object> value) {
        return objectMapper.convertValue(value, MAP_TYPE);
    }

    private Path archivePath(String rawArchiveUri) throws IOException {
        String key = rawArchiveUri.startsWith("archive://")
                ? rawArchiveUri.substring("archive://".length())
                : rawArchiveUri;
        Path path = archiveRoot.resolve(key.replace("..", "_").replaceAll("^/+", "")).normalize();
        // archive:// 是外部输入，必须限制在 archiveRoot 内，避免通过 ../ 读取任意文件。
        if (!path.startsWith(archiveRoot)) {
            throw new IOException("raw archive path escapes root: " + rawArchiveUri);
        }
        return path;
    }

    private String platformAccount(TdengineRawFrameRow row, String override) {
        if (override != null && !override.isBlank()) {
            return override;
        }
        if (row.metadataJson() == null || row.metadataJson().isBlank()) {
            return null;
        }
        try {
            return objectMapper.readValue(row.metadataJson(), MAP_TYPE).getOrDefault("platformAccount", "").toString();
        } catch (IOException ignored) {
            return null;
        }
    }

    private static Instant fromInstant(LocalDate dateFrom, Instant eventTimeFrom) {
        return eventTimeFrom != null ? eventTimeFrom : dateFrom.atStartOfDay(DEFAULT_ZONE).toInstant();
    }

    private static Instant toExclusiveInstant(LocalDate dateTo, Instant eventTimeTo) {
        Instant inclusive = eventTimeTo != null
                ? eventTimeTo
                : dateTo.plusDays(1).atStartOfDay(DEFAULT_ZONE).toInstant().minusMillis(1);
        return inclusive.plusMillis(1);
    }

    private static TdengineQueryOrder tdengineOrder(HistoryQueryOrder order) {
        return order == HistoryQueryOrder.ASC ? TdengineQueryOrder.ASC : TdengineQueryOrder.DESC;
    }

    private static String rawArchiveEventId(String rawArchiveUri) {
        if (rawArchiveUri == null || rawArchiveUri.isBlank()) {
            return "";
        }
        String key = rawArchiveUri.startsWith("archive://")
                ? rawArchiveUri.substring("archive://".length())
                : rawArchiveUri;
        int slash = key.lastIndexOf('/');
        String fileName = slash >= 0 ? key.substring(slash + 1) : key;
        return fileName.endsWith(".bin") ? fileName.substring(0, fileName.length() - 4) : fileName;
    }

    private static String hex(byte[] bytes) {
        if (bytes == null || bytes.length == 0) {
            return "";
        }
        StringBuilder out = new StringBuilder(bytes.length * 2);
        for (byte b : bytes) {
            out.append(Character.forDigit((b >> 4) & 0xF, 16));
            out.append(Character.forDigit(b & 0xF, 16));
        }
        return out.toString();
    }

    public record DecodedFrame(
            String eventId,
            String vin,
            String command,
            String responseFlag,
            String protocolVersion,
            String encryptType,
            int dataLength,
            String eventTime,
            String ingestTime,
            String rawArchiveUri,
            int rawSizeBytes,
            List<Map<String, Object>> blocks) {
    }

    public record CursorResponse(String ts, String id) {
    }

    public record DecodedFramePage(
            List<DecodedFrame> items,
            CursorResponse nextCursor) {
    }

    public record SourceFrame(
            String eventId,
            String ingestTime,
            String rawArchiveUri,
            int rawSizeBytes,
            List<String> blockTypes) {
    }

    public record LogicalSnapshot(
            String vin,
            String command,
            String responseFlag,
            String protocolVersion,
            String encryptType,
            String eventTime,
            String ingestTimeFirst,
            String ingestTimeLast,
            List<SourceFrame> sourceFrames,
            List<Map<String, Object>> blocks) {
    }

    public record FieldSnapshot(
            String vin,
            String eventTime,
            String ingestTimeFirst,
            String ingestTimeLast,
            List<SourceFrame> sourceFrames,
            Map<String, Object> fields) {

        private static FieldSnapshot from(LogicalSnapshot snapshot, List<String> selectedFields) {
            Map<String, Object> values = new LinkedHashMap<>();
            for (String selectedField : selectedFields) {
                values.put(selectedField, findField(snapshot.blocks(), selectedField));
            }
            return new FieldSnapshot(
                    snapshot.vin(),
                    snapshot.eventTime(),
                    snapshot.ingestTimeFirst(),
                    snapshot.ingestTimeLast(),
                    snapshot.sourceFrames(),
                    values);
        }

        private static Object findField(List<Map<String, Object>> blocks, String selectedField) {
            int dot = selectedField.indexOf('.');
            if (dot <= 0 || dot == selectedField.length() - 1) {
                return null;
            }
            String type = selectedField.substring(0, dot);
            String path = selectedField.substring(dot + 1);
            for (Map<String, Object> block : blocks) {
                if (type.equals(block.get("type"))) {
                    return valueAt(block.get("data"), path.split("\\."));
                }
            }
            return null;
        }

        private static Object valueAt(Object current, String[] path) {
            Object value = current;
            for (String part : path) {
                value = step(value, part);
                if (value == null) {
                    return null;
                }
            }
            return value;
        }

        @SuppressWarnings("unchecked")
        private static Object step(Object value, String part) {
            if (value instanceof Map<?, ?> map) {
                return map.get(part);
            }
            if (value instanceof List<?> list) {
                if (part.matches("\\d+")) {
                    int index = Integer.parseInt(part);
                    return index >= 0 && index < list.size() ? list.get(index) : null;
                }
                // 对数组字段支持 "stacks.cellVoltages" 这类投影，返回每个元素上对应属性的列表。
                List<Object> out = new ArrayList<>(list.size());
                for (Object item : list) {
                    if (item instanceof Map<?, ?> itemMap) {
                        out.add(((Map<String, Object>) itemMap).get(part));
                    }
                }
                return out;
            }
            return null;
        }
    }

    private final class SnapshotBuilder {
        private final DecodedFrame first;
        private final List<SourceFrame> sourceFrames = new ArrayList<>();
        private final LinkedHashMap<String, Map<String, Object>> blocks = new LinkedHashMap<>();

        private SnapshotBuilder(DecodedFrame first) {
            this.first = first;
        }

        private void add(DecodedFrame frame) {
            sourceFrames.add(new SourceFrame(
                    frame.eventId(),
                    frame.ingestTime(),
                    frame.rawArchiveUri(),
                    frame.rawSizeBytes(),
                    frame.blocks().stream().map(block -> String.valueOf(block.get("type"))).toList()));
            for (Map<String, Object> block : frame.blocks()) {
                String type = String.valueOf(block.get("type"));
                if ("GD_FC_STACK".equals(type)) {
                    // 广东燃料电池堆电压可能按帧号拆成多个 RAW，这里按 cell 起始序号合并。
                    mergeStack(block);
                } else {
                    // 非分帧块同一 snapshot 内只保留第一次出现的块，避免后到空值覆盖前面完整值。
                    blocks.putIfAbsent(type, deepCopy(block));
                }
            }
        }

        private void mergeStack(Map<String, Object> block) {
            Map<String, Object> current = blocks.get("GD_FC_STACK");
            if (current == null) {
                blocks.put("GD_FC_STACK", deepCopy(block));
                return;
            }
            Map<String, Object> currentData = map(current.get("data"));
            Map<String, Object> incomingData = map(block.get("data"));
            List<Map<String, Object>> currentStacks = listOfMaps(currentData.get("stacks"));
            List<Map<String, Object>> incomingStacks = listOfMaps(incomingData.get("stacks"));
            for (int i = 0; i < incomingStacks.size(); i++) {
                if (i >= currentStacks.size()) {
                    currentStacks.add(deepCopy(incomingStacks.get(i)));
                } else {
                    mergeStackItem(currentStacks.get(i), incomingStacks.get(i));
                }
            }
            currentData.put("stackCount", currentStacks.size());
        }

        private void mergeStackItem(Map<String, Object> target, Map<String, Object> incoming) {
            int targetStart = intValue(target.get("frameCellStart"), 1);
            int incomingStart = intValue(incoming.get("frameCellStart"), 1);
            List<Double> targetVoltages = doubleList(target.get("frameCellVoltagesV"));
            List<Double> incomingVoltages = doubleList(incoming.get("frameCellVoltagesV"));
            // GB/T 分帧的起始序号是 1-based；内部 List 是 0-based，所以写入时统一减一。
            int cellCount = Math.max(intValue(target.get("cellCount"), 0), intValue(incoming.get("cellCount"), 0));
            int needed = Math.max(cellCount, Math.max(
                    targetStart - 1 + targetVoltages.size(),
                    incomingStart - 1 + incomingVoltages.size()));
            List<Double> merged = new ArrayList<>(needed);
            for (int i = 0; i < needed; i++) {
                merged.add(null);
            }
            putVoltages(merged, targetStart, targetVoltages);
            putVoltages(merged, incomingStart, incomingVoltages);
            while (!merged.isEmpty() && merged.getLast() == null) {
                merged.removeLast();
            }
            target.put("frameCellStart", 1);
            target.put("frameCellCount", merged.size());
            target.put("frameCellVoltagesV", merged);
            target.put("cellCount", Math.max(cellCount, merged.size()));
            normalizeStackItem(target);
        }

        private LogicalSnapshot build() {
            List<SourceFrame> orderedFrames = sourceFrames.stream()
                    .sorted(Comparator.comparing(SourceFrame::ingestTime))
                    .toList();
            Map<String, Object> stackBlock = blocks.get("GD_FC_STACK");
            if (stackBlock != null) {
                normalizeStackBlock(stackBlock);
            }
            List<Map<String, Object>> orderedBlocks = new ArrayList<>(blocks.values());
            orderedBlocks.sort(Comparator.comparingInt(block -> blockOrder(String.valueOf(block.get("type")))));
            List<Map<String, Object>> normalizedBlocks = orderedBlocks.stream()
                    .map(block -> map(normalizeJsonValue(block)))
                    .toList();
            return new LogicalSnapshot(
                    first.vin(),
                    first.command(),
                    first.responseFlag(),
                    first.protocolVersion(),
                    first.encryptType(),
                    first.eventTime(),
                    orderedFrames.isEmpty() ? null : orderedFrames.getFirst().ingestTime(),
                    orderedFrames.isEmpty() ? null : orderedFrames.getLast().ingestTime(),
                    sourceFrames,
                    normalizedBlocks);
        }
    }

    private static void normalizeStackBlock(Map<String, Object> stackBlock) {
        Map<String, Object> data = map(stackBlock.get("data"));
        for (Map<String, Object> stack : listOfMaps(data.get("stacks"))) {
            normalizeStackItem(stack);
        }
    }

    private static void normalizeStackItem(Map<String, Object> target) {
        int start = intValue(target.get("frameCellStart"), 1);
        List<Double> voltages = doubleList(target.get("frameCellVoltagesV"));
        int cellCount = intValue(target.get("cellCount"), 0);
        int needed = Math.max(cellCount, start - 1 + voltages.size());
        List<Double> normalized = new ArrayList<>(needed);
        for (int i = 0; i < needed; i++) {
            normalized.add(null);
        }
        putVoltages(normalized, start, voltages);
        while (cellCount == 0 && !normalized.isEmpty() && normalized.getLast() == null) {
            normalized.removeLast();
        }
        long missing = normalized.stream().filter(item -> item == null).count();
        // 给前端明确的完整性标记，避免只能靠数组长度猜测是否缺帧。
        target.put("frameCellStart", 1);
        target.put("frameCellCount", normalized.size());
        target.put("frameCellVoltagesV", normalized);
        target.put("cellCount", Math.max(cellCount, normalized.size()));
        target.put("frameCellComplete", missing == 0 && (cellCount == 0 || normalized.size() >= cellCount));
        target.put("frameCellMissingCount", missing);
    }

    @SuppressWarnings("unchecked")
    private static Map<String, Object> map(Object value) {
        return value instanceof Map<?, ?> m ? (Map<String, Object>) m : new LinkedHashMap<>();
    }

    @SuppressWarnings("unchecked")
    private static List<Map<String, Object>> listOfMaps(Object value) {
        return value instanceof List<?> list ? (List<Map<String, Object>>) list : new ArrayList<>();
    }

    private static int intValue(Object value, int fallback) {
        return value instanceof Number n ? n.intValue() : fallback;
    }

    private static List<Double> doubleList(Object value) {
        if (!(value instanceof List<?> list)) {
            return List.of();
        }
        List<Double> out = new ArrayList<>(list.size());
        for (Object item : list) {
            out.add(item instanceof Number n ? n.doubleValue() : null);
        }
        return out;
    }

    private static void putVoltages(List<Double> target, int start, List<Double> values) {
        int offset = Math.max(0, start - 1);
        for (int i = 0; i < values.size(); i++) {
            int index = offset + i;
            if (index >= target.size()) {
                target.add(values.get(i));
            } else if (values.get(i) != null) {
                target.set(index, values.get(i));
            }
        }
    }

    private static int blockOrder(String type) {
        return switch (type) {
            case "VEHICLE" -> 10;
            case "DRIVE_MOTOR" -> 20;
            case "FUEL_CELL_V2016", "FUEL_CELL_V2025" -> 30;
            case "ENGINE" -> 40;
            case "POSITION_V2016", "POSITION_V2025" -> 50;
            case "EXTREME_V2016" -> 60;
            case "ALARM_V2016", "ALARM_V2025" -> 70;
            case "VOLTAGE_V2016", "MIN_PARALLEL_VOLTAGE_V2025" -> 80;
            case "TEMPERATURE_V2016", "BATTERY_TEMPERATURE_V2025" -> 90;
            case "FUEL_CELL_STACK", "GD_FC_STACK" -> 100;
            case "SUPER_CAPACITOR" -> 110;
            case "SUPER_CAPACITOR_EXTREME" -> 120;
            case "GD_FC_AUXILIARY" -> 130;
            case "GD_FC_DCDC" -> 140;
            case "GD_FC_AIR_CONDITIONER" -> 150;
            case "GD_FC_VEHICLE_INFO" -> 160;
            case "GD_FC_DEMO_EXTENSION" -> 170;
            case "GD_FC_VENDOR_TLV" -> 180;
            default -> 1000;
        };
    }
}
