package com.lingniu.ingest.sink.kafka;

import com.lingniu.ingest.api.event.AlarmPayload;
import com.lingniu.ingest.api.event.LocationPayload;
import com.lingniu.ingest.api.event.RawArchiveKeys;
import com.lingniu.ingest.api.event.RealtimePayload;
import com.lingniu.ingest.api.event.TelemetryFieldValue;
import com.lingniu.ingest.api.event.TelemetrySnapshot;
import com.lingniu.ingest.api.event.VehicleEvent;
import com.lingniu.ingest.api.event.VehicleEventTelemetrySnapshotMapper;
import com.lingniu.ingest.facts.FactIds;
import com.lingniu.ingest.facts.VehicleKey;
import com.lingniu.ingest.sink.kafka.proto.ParseStatusProto;
import com.lingniu.ingest.sink.kafka.proto.TelemetryField;
import com.lingniu.ingest.sink.kafka.proto.TelemetrySnapshot.Builder;
import com.lingniu.ingest.sink.kafka.proto.VehicleEnvelope;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;

/**
 * 领域事件 → Protobuf Envelope 的纯函数映射。通过 switch 模式匹配穷尽处理，
 * 新增 {@link VehicleEvent} 子类型时编译期就会提示补齐分支。
 */
public final class EnvelopeMapper {

    private static final String SCHEMA_VERSION = "1.0";
    private final String nodeId;

    public EnvelopeMapper(String nodeId) {
        this.nodeId = nodeId;
    }

    public VehicleEnvelope toEnvelope(VehicleEvent event) {
        VehicleEnvelope.Builder b = VehicleEnvelope.newBuilder()
                .setSchemaVersion(SCHEMA_VERSION)
                .setEventId(event.eventId())
                .setTraceId(event.traceId() == null ? "" : event.traceId())
                .setVin(event.vin())
                .setSource(event.source().name())
                .setEventTimeMs(event.eventTime().toEpochMilli())
                .setIngestTimeMs(event.ingestTime().toEpochMilli())
                .setIngestNodeId(nodeId);
        if (event.metadata() != null) b.putAllMetadata(event.metadata());
        // telemetrySnapshot 是跨事件类型的统一字段视图，消费者可先读它，必要时再读具体 payload。
        VehicleEventTelemetrySnapshotMapper.toSnapshot(event)
                .map(EnvelopeMapper::buildTelemetrySnapshot)
                .ifPresent(b::setTelemetrySnapshot);

        switch (event) {
            case VehicleEvent.Realtime r -> b.setRealtime(buildRealtime(r.payload()));
            case VehicleEvent.Location l -> b.setLocation(buildLocation(l.payload()));
            case VehicleEvent.Alarm a    -> b.setAlarm(buildAlarm(a.payload()));
            case VehicleEvent.Login lg   -> b.setLogin(
                    com.lingniu.ingest.sink.kafka.proto.LoginPayload.newBuilder()
                            .setIccid(nullToEmpty(lg.iccid()))
                            .setProtocolVersion(nullToEmpty(lg.protocolVersion()))
                            .build());
            case VehicleEvent.Logout ignored -> b.setLogout(
                    com.lingniu.ingest.sink.kafka.proto.LogoutPayload.getDefaultInstance());
            case VehicleEvent.Heartbeat ignored -> b.setHeartbeat(
                    com.lingniu.ingest.sink.kafka.proto.HeartbeatPayload.getDefaultInstance());
            case VehicleEvent.MediaMeta m -> b.setMediaMeta(
                    com.lingniu.ingest.sink.kafka.proto.MediaMetaPayload.newBuilder()
                            .setMediaId(nullToEmpty(m.mediaId()))
                            .setMediaType(nullToEmpty(m.mediaType()))
                            .setSizeBytes(m.sizeBytes())
                            .setArchiveRef(nullToEmpty(m.archiveRef()))
                            .build());
            case VehicleEvent.Passthrough p -> b.setPassthrough(
                    com.lingniu.ingest.sink.kafka.proto.PassthroughPayload.newBuilder()
                            .setPassthroughType(p.passthroughType())
                            .setData(com.google.protobuf.ByteString.copyFrom(
                                    p.data() == null ? new byte[0] : p.data()))
                            .build());
            case VehicleEvent.RawArchive ra -> {
                byte[] rawBytes = ra.rawBytes() == null ? new byte[0] : ra.rawBytes();
                int size = rawBytes.length;
                String key = rawArchiveKey(ra);
                String uri = rawArchiveUri(ra, key);
                String phone = metadataValue(ra, "phone");
                String vehicleKey = VehicleKey.derive(ra.source(), ra.vin(), phone, ra.eventId());
                String checksum = sha256(rawBytes);
                String frameId = FactIds.rawFrameId(
                        ra.source(), vehicleKey, ra.command(), ra.infoType(), ra.ingestTime(), checksum);
                // RAW envelope 只携带 URI/size 引用信息，不把完整原始字节塞进 Kafka，避免 topic 膨胀。
                b.putMetadata(RawArchiveKeys.META_KEY, key);
                b.putMetadata(RawArchiveKeys.META_URI, uri);
                b.putMetadata(RawArchiveKeys.META_EVENT_ID, ra.eventId());
                b.setRawArchive(com.lingniu.ingest.sink.kafka.proto.RawArchiveRef.newBuilder()
                        .setUri(uri)
                        .setChecksum(checksum)
                        .setSizeBytes(size)
                        .setParsedJson(nullToEmpty(ra.parsedJson()))
                        .build());
                b.setRawFrameFact(com.lingniu.ingest.sink.kafka.proto.RawFrameFactPayload.newBuilder()
                        .setFrameId(frameId)
                        .setVehicleKey(vehicleKey)
                        .setVin(nullToEmpty(ra.vin()))
                        .setPhone(phone)
                        .setMessageId(ra.command())
                        .setSubType(ra.infoType())
                        .setRawUri(uri)
                        .setChecksum(checksum)
                        .setRawSizeBytes(size)
                        .setParseStatus(parseStatus(ra))
                        .setParseError(parseError(ra))
                        .setPeer(metadataValue(ra, "peer"))
                        .putAllMetadata(ra.metadata() == null ? java.util.Map.of() : ra.metadata())
                        .build());
            }
        }
        return b.build();
    }

    private static com.lingniu.ingest.sink.kafka.proto.TelemetrySnapshot buildTelemetrySnapshot(TelemetrySnapshot snapshot) {
        Builder b = com.lingniu.ingest.sink.kafka.proto.TelemetrySnapshot.newBuilder()
                .setEventType(snapshot.eventType())
                .setRawArchiveUri(snapshot.rawArchiveUri());
        for (TelemetryFieldValue field : snapshot.fields()) {
            b.addFields(TelemetryField.newBuilder()
                    .setKey(field.key())
                    .setValueType(field.valueType().name())
                    .setValue(field.value())
                    .setUnit(field.unit())
                    .setQuality(field.quality().name())
                    .setSourcePath(field.sourcePath())
                    .build());
        }
        return b.build();
    }

    private static com.lingniu.ingest.sink.kafka.proto.RealtimePayload buildRealtime(RealtimePayload p) {
        var b = com.lingniu.ingest.sink.kafka.proto.RealtimePayload.newBuilder();
        if (p.speedKmh() != null) b.setSpeedKmh(p.speedKmh());
        if (p.totalMileageKm() != null) b.setTotalMileageKm(p.totalMileageKm());
        if (p.batterySoc() != null) b.setBatterySoc(p.batterySoc());
        if (p.batteryVoltageV() != null) b.setBatteryVoltageV(p.batteryVoltageV());
        if (p.batteryCurrentA() != null) b.setBatteryCurrentA(p.batteryCurrentA());
        if (p.fcVoltageV() != null) b.setFcVoltageV(p.fcVoltageV());
        if (p.fcCurrentA() != null) b.setFcCurrentA(p.fcCurrentA());
        if (p.fcTempC() != null) b.setFcTempC(p.fcTempC());
        if (p.hydrogenRemainingKg() != null) b.setHydrogenRemainingKg(p.hydrogenRemainingKg());
        if (p.hydrogenHighPressureMpa() != null) b.setHydrogenHighPressureMpa(p.hydrogenHighPressureMpa());
        if (p.hydrogenLowPressureMpa() != null) b.setHydrogenLowPressureMpa(p.hydrogenLowPressureMpa());
        if (p.vehicleState() != null) b.setVehicleState(p.vehicleState().name());
        if (p.chargingState() != null) b.setChargingState(p.chargingState().name());
        if (p.runningMode() != null) b.setRunningMode(p.runningMode().name());
        if (p.gearLevel() != null) b.setGearLevel(p.gearLevel());
        if (p.acceleratorPedal() != null) b.setAcceleratorPedal(p.acceleratorPedal());
        if (p.brakePedal() != null) b.setBrakePedal(p.brakePedal());
        if (p.longitude() != null) b.setLongitude(p.longitude());
        if (p.latitude() != null) b.setLatitude(p.latitude());
        if (p.altitudeM() != null) b.setAltitudeM(p.altitudeM());
        if (p.directionDeg() != null) b.setDirectionDeg(p.directionDeg());
        if (p.ambientTempC() != null) b.setAmbientTempC(p.ambientTempC());
        if (p.coolantTempC() != null) b.setCoolantTempC(p.coolantTempC());
        return b.build();
    }

    private static com.lingniu.ingest.sink.kafka.proto.LocationPayload buildLocation(LocationPayload p) {
        return com.lingniu.ingest.sink.kafka.proto.LocationPayload.newBuilder()
                .setLongitude(p.longitude())
                .setLatitude(p.latitude())
                .setAltitudeM(p.altitudeM())
                .setSpeedKmh(p.speedKmh())
                .setDirectionDeg(p.directionDeg())
                .setAlarmFlag(p.alarmFlag())
                .setStatusFlag(p.statusFlag())
                .build();
    }

    private static com.lingniu.ingest.sink.kafka.proto.AlarmPayload buildAlarm(AlarmPayload p) {
        var b = com.lingniu.ingest.sink.kafka.proto.AlarmPayload.newBuilder()
                .setLevel(p.level().name())
                .setAlarmTypeCode(p.alarmTypeCode())
                .setAlarmTypeName(nullToEmpty(p.alarmTypeName()));
        if (p.faultCodes() != null) b.addAllFaultCodes(p.faultCodes());
        if (p.activeBits() != null) b.addAllActiveBits(p.activeBits());
        if (p.longitude() != null) b.setLongitude(p.longitude());
        if (p.latitude() != null) b.setLatitude(p.latitude());
        return b.build();
    }

    private static String nullToEmpty(String s) {
        return s == null ? "" : s;
    }

    private static String rawArchiveKey(VehicleEvent.RawArchive raw) {
        String key = raw.metadata() == null ? "" : raw.metadata().getOrDefault(RawArchiveKeys.META_KEY, "");
        return key == null || key.isBlank() ? RawArchiveKeys.key(raw) : key;
    }

    private static String rawArchiveUri(VehicleEvent.RawArchive raw, String key) {
        String uri = raw.metadata() == null ? "" : raw.metadata().getOrDefault(RawArchiveKeys.META_URI, "");
        return uri == null || uri.isBlank() ? RawArchiveKeys.logicalUri(key) : uri;
    }

    private static String metadataValue(VehicleEvent event, String key) {
        if (event.metadata() == null) {
            return "";
        }
        return nullToEmpty(event.metadata().get(key));
    }

    private static ParseStatusProto parseStatus(VehicleEvent.RawArchive raw) {
        if (hasTruthyMetadata(raw, "frameError")
                || hasTruthyMetadata(raw, "parseError")
                || hasTruthyMetadata(raw, "processingError")) {
            return ParseStatusProto.PARSE_STATUS_FAILED;
        }
        return ParseStatusProto.PARSE_STATUS_NOT_PARSED;
    }

    private static String parseError(VehicleEvent.RawArchive raw) {
        String frameError = metadataValue(raw, "frameErrorMessage");
        if (!frameError.isBlank()) {
            return frameError;
        }
        String parseError = metadataValue(raw, "parseErrorMessage");
        if (!parseError.isBlank()) {
            return parseError;
        }
        return metadataValue(raw, "processingErrorMessage");
    }

    private static boolean hasTruthyMetadata(VehicleEvent event, String key) {
        return Boolean.parseBoolean(metadataValue(event, key));
    }

    private static String sha256(byte[] bytes) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            return "sha256:" + HexFormat.of().formatHex(digest.digest(bytes == null ? new byte[0] : bytes));
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException("SHA-256 is unavailable", e);
        }
    }
}
