package com.lingniu.ingest.protocol.gb32960.mapper;

import com.lingniu.ingest.api.ProtocolId;
import com.lingniu.ingest.api.event.AlarmPayload;
import com.lingniu.ingest.api.event.LocationPayload;
import com.lingniu.ingest.api.event.RealtimePayload;
import com.lingniu.ingest.api.event.TelemetryMetadataValues;
import com.lingniu.ingest.api.event.VehicleEvent;
import com.lingniu.ingest.api.spi.EventMapper;
import com.lingniu.ingest.protocol.gb32960.model.CommandBody;
import com.lingniu.ingest.protocol.gb32960.model.CommandType;
import com.lingniu.ingest.protocol.gb32960.model.Gb32960Message;
import com.lingniu.ingest.protocol.gb32960.model.GeneralAlarmFlagBit;
import com.lingniu.ingest.protocol.gb32960.model.InfoBlock;

import java.time.Instant;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;

/**
 * 32960 → 领域事件 Adapter。
 *
 * <p>实时/补发上报产出 {@code Realtime + Location}（可选）+ {@code Alarm}（可选）；
 * 车辆登入/登出/心跳产出对应的会话事件。
 *
 * <p>该 Mapper 只生成统一领域事件，字段是面向实时总线的精简视图。32960 的完整包字段、
 * 厂商扩展字段和 snapshot 合并字段由历史查询服务从 raw archive 重新解码，不在这里展开。
 */
public final class Gb32960EventMapper implements EventMapper<Gb32960Message> {

    @Override
    public List<VehicleEvent> toEvents(Gb32960Message message) {
        var header = message.header();
        Instant eventTime = header.eventTime() != null ? header.eventTime() : Instant.now();
        Instant ingestTime = Instant.now();

        List<VehicleEvent> out = new ArrayList<>(3);
        Map<String, String> baseMeta = Map.of(
                "command", header.command().name(),
                "protocolVersion", header.protocolVersion().name());

        CommandType cmd = header.command();
        if (cmd == CommandType.REALTIME_REPORT || cmd == CommandType.RESEND_REPORT) {
            // 补发包与实时包进入同一套实时事件模型，差异保留在 metadata.command 里供下游判断。
            buildRealtime(message, header, baseMeta, eventTime, ingestTime, out);
            return out;
        }
        if (cmd == CommandType.VEHICLE_LOGIN) {
            String iccid = "";
            if (message.commandBody() instanceof CommandBody.VehicleLogin vl) iccid = vl.iccid();
            out.add(new VehicleEvent.Login(
                    UUID.randomUUID().toString(),
                    header.vin(), ProtocolId.GB32960, eventTime, ingestTime,
                    null, baseMeta, iccid, header.protocolVersion().name()));
        } else if (cmd == CommandType.VEHICLE_LOGOUT) {
            out.add(new VehicleEvent.Logout(
                    UUID.randomUUID().toString(),
                    header.vin(), ProtocolId.GB32960, eventTime, ingestTime,
                    null, baseMeta));
        } else if (cmd == CommandType.HEARTBEAT) {
            out.add(new VehicleEvent.Heartbeat(
                    UUID.randomUUID().toString(),
                    header.vin(), ProtocolId.GB32960, eventTime, ingestTime,
                    null, baseMeta));
        } else if (cmd == CommandType.PLATFORM_LOGIN) {
            // 平台登入 0x05：映射为 VehicleEvent.Login + metadata.kind=platform。
            // 使用 "platform:" + username 作为 vin 字段，既能送到 vehicle.session topic
            // 下游消费者又能靠 metadata.kind 区分车辆/平台登入。
            if (message.commandBody() instanceof CommandBody.PlatformLogin pl) {
                Map<String, String> meta = new java.util.HashMap<>(baseMeta);
                meta.put("kind", "platform");
                meta.put("username", pl.username() == null ? "" : pl.username());
                meta.put("encryptRule", pl.encryptRule() == null ? "" : pl.encryptRule().name());
                meta.put("serial", String.valueOf(pl.serialNo()));
                out.add(new VehicleEvent.Login(
                        UUID.randomUUID().toString(),
                        "platform:" + (pl.username() == null ? "" : pl.username()),
                        ProtocolId.GB32960,
                        pl.loginTime() != null ? pl.loginTime() : eventTime,
                        ingestTime,
                        null, meta, "", header.protocolVersion().name()));
            }
        } else if (cmd == CommandType.PLATFORM_LOGOUT) {
            if (message.commandBody() instanceof CommandBody.PlatformLogout plo) {
                Map<String, String> meta = new java.util.HashMap<>(baseMeta);
                meta.put("kind", "platform");
                meta.put("serial", String.valueOf(plo.serialNo()));
                out.add(new VehicleEvent.Logout(
                        UUID.randomUUID().toString(),
                        "platform:unknown",
                        ProtocolId.GB32960,
                        plo.logoutTime() != null ? plo.logoutTime() : eventTime,
                        ingestTime,
                        null, meta));
            }
        }
        return out;
    }

    private void buildRealtime(Gb32960Message message,
                                com.lingniu.ingest.protocol.gb32960.model.Gb32960Header header,
                                Map<String, String> meta,
                                Instant eventTime, Instant ingestTime,
                                List<VehicleEvent> out) {
        // 整车数据：先查 V2016，再 V2025；每帧只会命中其一，extracted 出来变成统一
        // 视图（VehicleView）供后面 payload 构造使用
        VehicleView v = extractVehicleView(message);
        // 位置数据：同样两版本都查
        PositionView p = extractPositionView(message);
        // 燃料电池：V2016 字段更全（带电压/电流），V2025 字段更少
        InfoBlock.Gb32960V2016.FuelCell fc16 =
                message.findBlock(InfoBlock.Gb32960V2016.FuelCell.class).orElse(null);
        InfoBlock.Gb32960V2025.FuelCell fc25 =
                message.findBlock(InfoBlock.Gb32960V2025.FuelCell.class).orElse(null);
        // 报警
        AlarmView alarm = extractAlarmView(message);

        if (v != null) {
            Double fcVoltageV = fc16 != null ? fc16.fcVoltageV() : null;
            Double fcCurrentA = fc16 != null ? fc16.fcCurrentA() : null;
            Double fcTempC = fc16 != null ? fc16.maxHydrogenSystemTempC()
                    : (fc25 != null ? fc25.maxHydrogenSystemTempC() : null);
            Double hydrogenHighPressure = fc16 != null ? fc16.maxHydrogenPressureMpa()
                    : (fc25 != null ? fc25.maxHydrogenPressureMpa() : null);
            Double hydrogenRemaining = fc25 != null && fc25.remainingHydrogenPercent() != null
                    ? fc25.remainingHydrogenPercent().doubleValue() : null;

            RealtimePayload payload = new RealtimePayload(
                    v.speedKmh,
                    v.totalMileageKm,
                    v.socPercent != null ? v.socPercent.doubleValue() : null,
                    v.totalVoltageV,
                    v.totalCurrentA,
                    fcVoltageV,
                    fcCurrentA,
                    fcTempC,
                    hydrogenRemaining,
                    hydrogenHighPressure,
                    null,
                    mapVehicleState(v.vehicleState),
                    mapChargingState(v.chargingState),
                    mapRunningMode(v.runningMode),
                    extractGearLevel(v.gearRaw),
                    v.acceleratorPedal != null ? v.acceleratorPedal.doubleValue() : null,
                    v.brakePedal != null ? v.brakePedal.doubleValue() : null,
                    p != null ? p.longitude : null,
                    p != null ? p.latitude : null,
                    null,
                    null,
                    null,
                    null);
            out.add(new VehicleEvent.Realtime(
                    UUID.randomUUID().toString(),
                    header.vin(), ProtocolId.GB32960, eventTime, ingestTime,
                    null, meta, payload));
        }
        if (p != null) {
            // 位置事件只从 POSITION 子包生成。没有 POSITION 时不从整车数据推断经纬度，避免制造假定位。
            LocationPayload loc = new LocationPayload(
                    p.longitude, p.latitude, 0.0,
                    v != null && v.speedKmh != null ? v.speedKmh : 0.0,
                    0.0, 0, p.statusFlag);
            Map<String, String> locationMeta = v != null && v.totalMileageKm != null
                    ? withMeta(meta, "total_mileage_km", TelemetryMetadataValues.doubleValue(v.totalMileageKm))
                    : meta;
            out.add(new VehicleEvent.Location(
                    UUID.randomUUID().toString(),
                    header.vin(), ProtocolId.GB32960, eventTime, ingestTime,
                    null, locationMeta, loc));
        }
        if (alarm != null && shouldEmitAlarm(alarm)) {
            // 只在存在告警等级或氢安全关键位时发 Alarm，减少正常高频帧对告警消费者的噪声。
            List<String> faultCodes = new ArrayList<>();
            addFaultCodes(faultCodes, "BAT", alarm.batteryFaults);
            addFaultCodes(faultCodes, "MOT", alarm.motorFaults);
            addFaultCodes(faultCodes, "ENG", alarm.engineFaults);
            addFaultCodes(faultCodes, "OTH", alarm.otherFaults);
            Set<GeneralAlarmFlagBit> activeBitEnums = GeneralAlarmFlagBit.parse(alarm.generalAlarmFlag);
            Set<String> activeBits = activeBitEnums.stream()
                    .map(Enum::name)
                    .collect(Collectors.toCollection(java.util.LinkedHashSet::new));
            boolean hydrogenLeakDetected = activeBitEnums.contains(GeneralAlarmFlagBit.HYDROGEN_LEAK);
            AlarmPayload ap = new AlarmPayload(
                    mapAlarmLevel(alarm.maxLevel, hydrogenLeakDetected),
                    (int) (alarm.generalAlarmFlag & 0xFFFF),
                    "GB32960_ALARM",
                    faultCodes,
                    activeBits,
                    p != null ? p.longitude : null,
                    p != null ? p.latitude : null,
                    safetyCategory(activeBitEnums),
                    hydrogenLeakDetected,
                    hydrogenLeakDetected
                            ? AlarmPayload.HydrogenLeakLevel.CRITICAL
                            : AlarmPayload.HydrogenLeakLevel.NONE,
                    hydrogenLeakDetected);
            out.add(new VehicleEvent.Alarm(
                    UUID.randomUUID().toString(),
                    header.vin(), ProtocolId.GB32960, eventTime, ingestTime,
                    null, meta, ap));
        }
    }

    /** 跨版本的整车数据视图，把 V2016 的踏板字段（V2025 没有）放在一起。 */
    private record VehicleView(
            Integer vehicleState, Integer chargingState, Integer runningMode,
            Double speedKmh, Double totalMileageKm, Double totalVoltageV, Double totalCurrentA,
            Integer socPercent, Integer gearRaw,
            Integer acceleratorPedal, Integer brakePedal) {}

    private static VehicleView extractVehicleView(Gb32960Message m) {
        var v16 = m.findBlock(InfoBlock.Gb32960V2016.Vehicle.class).orElse(null);
        if (v16 != null) {
            return new VehicleView(v16.vehicleState(), v16.chargingState(), v16.runningMode(),
                    v16.speedKmh(), v16.totalMileageKm(), v16.totalVoltageV(), v16.totalCurrentA(),
                    v16.socPercent(), v16.gearRaw(),
                    v16.acceleratorPedal(), v16.brakePedal());
        }
        var v25 = m.findBlock(InfoBlock.Gb32960V2025.Vehicle.class).orElse(null);
        if (v25 != null) {
            return new VehicleView(v25.vehicleState(), v25.chargingState(), v25.runningMode(),
                    v25.speedKmh(), v25.totalMileageKm(), v25.totalVoltageV(), v25.totalCurrentA(),
                    v25.socPercent(), v25.gearRaw(),
                    null, null);
        }
        return null;
    }

    /** 跨版本的位置数据视图。 */
    private record PositionView(int statusFlag, double longitude, double latitude) {}

    private static PositionView extractPositionView(Gb32960Message m) {
        var p16 = m.findBlock(InfoBlock.Gb32960V2016.Position.class).orElse(null);
        if (p16 != null) return new PositionView(p16.statusFlag(), p16.longitude(), p16.latitude());
        var p25 = m.findBlock(InfoBlock.Gb32960V2025.Position.class).orElse(null);
        if (p25 != null) return new PositionView(p25.statusFlag(), p25.longitude(), p25.latitude());
        return null;
    }

    /** 跨版本的报警视图（仅暴露公共字段，2025 的 generalAlarmLevels 暂未上送下游）。 */
    private record AlarmView(Integer maxLevel, long generalAlarmFlag,
                             List<Long> batteryFaults, List<Long> motorFaults,
                             List<Long> engineFaults, List<Long> otherFaults) {}

    private static AlarmView extractAlarmView(Gb32960Message m) {
        var a16 = m.findBlock(InfoBlock.Gb32960V2016.Alarm.class).orElse(null);
        if (a16 != null) {
            return new AlarmView(a16.maxLevel(), a16.generalAlarmFlag(),
                    a16.batteryFaults(), a16.motorFaults(), a16.engineFaults(), a16.otherFaults());
        }
        var a25 = m.findBlock(InfoBlock.Gb32960V2025.Alarm.class).orElse(null);
        if (a25 != null) {
            return new AlarmView(a25.maxLevel(), a25.generalAlarmFlag(),
                    a25.batteryFaults(), a25.motorFaults(), a25.engineFaults(), a25.otherFaults());
        }
        return null;
    }

    /** 挡位字节按附录 A.1 解析：低 4 位为挡位值 0000-1111。 */
    private static Integer extractGearLevel(Integer gearRaw) {
        if (gearRaw == null) return null;
        if ((gearRaw & 0x80) != 0) return null; // bit7: 挡位无效
        return gearRaw & 0x0F;
    }

    private static RealtimePayload.VehicleState mapVehicleState(Integer code) {
        if (code == null) return null;
        return switch (code) {
            case 0x01 -> RealtimePayload.VehicleState.STARTED;
            case 0x02 -> RealtimePayload.VehicleState.SHUTDOWN;
            case 0x03 -> RealtimePayload.VehicleState.OTHER;
            default -> RealtimePayload.VehicleState.INVALID;
        };
    }

    private static RealtimePayload.ChargingState mapChargingState(Integer code) {
        if (code == null) return null;
        return switch (code) {
            case 0x01 -> RealtimePayload.ChargingState.PARKED_CHARGING;
            case 0x02 -> RealtimePayload.ChargingState.DRIVING_CHARGING;
            case 0x03 -> RealtimePayload.ChargingState.UNCHARGED;
            case 0x04 -> RealtimePayload.ChargingState.CHARGED;
            default -> RealtimePayload.ChargingState.INVALID;
        };
    }

    private static RealtimePayload.RunningMode mapRunningMode(Integer code) {
        if (code == null) return null;
        return switch (code) {
            case 0x01 -> RealtimePayload.RunningMode.ELECTRIC;
            case 0x02 -> RealtimePayload.RunningMode.HYBRID;
            case 0x03 -> RealtimePayload.RunningMode.FUEL;
            default -> RealtimePayload.RunningMode.INVALID;
        };
    }

    private static boolean shouldEmitAlarm(AlarmView alarm) {
        if (alarm.maxLevel != null && alarm.maxLevel > 0) return true;
        return GeneralAlarmFlagBit.HYDROGEN_LEAK.isSet(alarm.generalAlarmFlag)
                || GeneralAlarmFlagBit.HYDROGEN_PRESSURE_ABNORMAL.isSet(alarm.generalAlarmFlag)
                || GeneralAlarmFlagBit.HYDROGEN_TEMP_ABNORMAL.isSet(alarm.generalAlarmFlag);
    }

    private static AlarmPayload.AlarmLevel mapAlarmLevel(Integer code, boolean hydrogenLeakDetected) {
        if (hydrogenLeakDetected) return AlarmPayload.AlarmLevel.CRITICAL;
        if (code == null) return AlarmPayload.AlarmLevel.INFO;
        return switch (code) {
            case 1 -> AlarmPayload.AlarmLevel.MINOR;
            case 2 -> AlarmPayload.AlarmLevel.MAJOR;
            case 3, 4 -> AlarmPayload.AlarmLevel.CRITICAL;
            default -> AlarmPayload.AlarmLevel.INFO;
        };
    }

    private static AlarmPayload.SafetyCategory safetyCategory(Set<GeneralAlarmFlagBit> activeBits) {
        if (activeBits.contains(GeneralAlarmFlagBit.HYDROGEN_LEAK)) {
            return AlarmPayload.SafetyCategory.HYDROGEN_LEAK;
        }
        if (activeBits.contains(GeneralAlarmFlagBit.HYDROGEN_PRESSURE_ABNORMAL)) {
            return AlarmPayload.SafetyCategory.TANK_PRESSURE;
        }
        if (activeBits.contains(GeneralAlarmFlagBit.HYDROGEN_TEMP_ABNORMAL)) {
            return AlarmPayload.SafetyCategory.TANK_TEMPERATURE;
        }
        return AlarmPayload.SafetyCategory.GENERAL;
    }

    private static void addFaultCodes(List<String> out, String prefix, List<Long> codes) {
        if (codes == null || codes.isEmpty()) return;
        for (Long c : codes) out.add(prefix + "-" + Long.toHexString(c));
    }

    private static Map<String, String> withMeta(Map<String, String> meta, String key, String value) {
        java.util.HashMap<String, String> copy = new java.util.HashMap<>(meta);
        copy.put(key, value == null ? "" : value);
        return Map.copyOf(copy);
    }

}
