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

import com.lingniu.ingest.api.ProtocolId;
import com.lingniu.ingest.api.event.LocationPayload;
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.identity.VehicleIdentity;
import com.lingniu.ingest.identity.VehicleIdentityLookup;
import com.lingniu.ingest.identity.VehicleIdentityResolver;
import com.lingniu.ingest.identity.VehicleIdentitySource;
import com.lingniu.ingest.protocol.jt808.model.Jt808Body;
import com.lingniu.ingest.protocol.jt808.model.Jt808Header;
import com.lingniu.ingest.protocol.jt808.model.Jt808LocationAdditionalInfo;
import com.lingniu.ingest.protocol.jt808.model.Jt808LocationFlagInfo;
import com.lingniu.ingest.protocol.jt808.model.Jt808Message;

import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;

/**
 * JT808 消息 → 领域事件映射。sealed body 允许 switch 穷尽处理。
 *
 * <p>备注：JT808 里没有强制的 VIN 字段，事件 VIN 统一通过 {@link VehicleIdentityResolver}
 * 解析；未绑定时才降级为终端手机号，metadata 同步保留内部 vin 与外部 phone。
 */
public final class Jt808EventMapper implements EventMapper<Jt808Message> {

    private static final String PROCESSING_ERROR_PREFIX = "processing failed: ";

    private final VehicleIdentityResolver identityResolver;

    public Jt808EventMapper(VehicleIdentityResolver identityResolver) {
        this.identityResolver = identityResolver;
    }

    @Override
    public List<VehicleEvent> toEvents(Jt808Message message) {
        var header = message.header();
        Instant ingestTime = Instant.now();
        IdentityResolution identity = resolveIdentity(message);
        Map<String, String> meta = baseMeta(header, identity);
        String vin = identity.vin();

        return switch (message.body()) {
            case Jt808Body.Location loc -> List.of(new VehicleEvent.Location(
                    UUID.randomUUID().toString(),
                    vin, ProtocolId.JT808, loc.deviceTime(), ingestTime, null, locationMeta(meta, loc),
                    new LocationPayload(loc.longitude(), loc.latitude(),
                            loc.altitudeM(), loc.speedKmh(), loc.directionDeg(),
                            loc.alarmFlag(), loc.statusFlag(), totalMileageKm(loc))));

            case Jt808Body.Register reg -> List.of(new VehicleEvent.Login(
                    UUID.randomUUID().toString(),
                    vin, ProtocolId.JT808, ingestTime, ingestTime, null, registerMeta(meta, reg),
                    reg.deviceId(), header.version().name()));

            case Jt808Body.Auth auth -> List.of(new VehicleEvent.Login(
                    UUID.randomUUID().toString(),
                    vin, ProtocolId.JT808, ingestTime, ingestTime, null, meta,
                    auth.imei() == null ? "" : auth.imei(), header.version().name()));

            case Jt808Body.Heartbeat _ -> List.of(new VehicleEvent.Heartbeat(
                    UUID.randomUUID().toString(),
                    vin, ProtocolId.JT808, ingestTime, ingestTime, null, meta));

            case Jt808Body.Unregister _ -> List.of(new VehicleEvent.Logout(
                    UUID.randomUUID().toString(),
                    vin, ProtocolId.JT808, ingestTime, ingestTime, null, meta));

            case Jt808Body.LocationBatch batch -> locationBatchEvents(vin, meta, batch, ingestTime);

            case Jt808Body.ParamsReport params -> List.of(new VehicleEvent.Passthrough(
                    UUID.randomUUID().toString(),
                    vin, ProtocolId.JT808, ingestTime, ingestTime, null,
                    withMeta(meta, "paramsCount", Integer.toString(params.parameters().size())),
                    header.messageId(),
                    encodeParamsSummary(params)));

            case Jt808Body.TerminalAttrs attrs -> List.of(new VehicleEvent.Login(
                    UUID.randomUUID().toString(),
                    vin, ProtocolId.JT808, ingestTime, ingestTime, null,
                    withMeta(meta, "terminalId", attrs.terminalId()),
                    attrs.iccid(), header.version().name()));

            case Jt808Body.MediaEvent media -> List.of(new VehicleEvent.MediaMeta(
                    UUID.randomUUID().toString(),
                    vin, ProtocolId.JT808, ingestTime, ingestTime, null,
                    withMeta(meta, "mediaEventCode", Integer.toString(media.eventCode())),
                    Long.toString(media.mediaId()),
                    mediaType(media.mediaType(), media.mediaFormat(), media.channelId()),
                    0,
                    ""));

            case Jt808Body.MediaUpload media -> mediaUploadEvents(vin, meta, media, ingestTime);

            case Jt808Body.MediaAttributes attrs -> List.of(new VehicleEvent.MediaMeta(
                    UUID.randomUUID().toString(),
                    vin, ProtocolId.JT808, ingestTime, ingestTime, null,
                    withMeta(withMeta(meta, "jt1078Message", "mediaAttributes"),
                            "maxVideoChannels", Integer.toString(attrs.maxVideoChannels())),
                    "jt1078-attrs-" + header.phone(),
                    mediaAttributesType(attrs),
                    attrs.raw() == null ? 0 : attrs.raw().length,
                    ""));

            case Jt808Body.FileUploadComplete done -> List.of(new VehicleEvent.Passthrough(
                    UUID.randomUUID().toString(),
                    vin, ProtocolId.JT808, ingestTime, ingestTime, null,
                    withMeta(withMeta(withMeta(meta, "jt1078Message", "fileUploadComplete"),
                                    "responseSerialNo", Integer.toString(done.responseSerialNo())),
                            "uploadResult", Integer.toString(done.result())),
                    header.messageId(),
                    done.raw() == null ? new byte[0] : done.raw()));

            case Jt808Body.FileList list -> List.of(new VehicleEvent.Passthrough(
                    UUID.randomUUID().toString(),
                    vin, ProtocolId.JT808, ingestTime, ingestTime, null,
                    withMeta(withMeta(withMeta(meta, "jt1078Message", "fileList"),
                                    "responseSerialNo", Integer.toString(list.responseSerialNo())),
                            "fileCount", Long.toString(list.fileCount())),
                    header.messageId(),
                    list.raw() == null ? new byte[0] : list.raw()));

            case Jt808Body.PassengerVolume volume -> List.of(new VehicleEvent.Passthrough(
                    UUID.randomUUID().toString(),
                    vin, ProtocolId.JT808, ingestTime, ingestTime, null,
                    passengerVolumeMeta(meta, volume),
                    header.messageId(),
                    volume.raw() == null ? new byte[0] : volume.raw()));

            case Jt808Body.Passthrough passthrough -> List.of(new VehicleEvent.Passthrough(
                    UUID.randomUUID().toString(),
                    vin, ProtocolId.JT808, ingestTime, ingestTime, null,
                    withMeta(meta, "passthroughType", "0x" + Integer.toHexString(passthrough.passthroughType())),
                    header.messageId(),
                    passthrough.data()));

            case Jt808Body.Raw raw -> List.of(new VehicleEvent.Passthrough(
                    UUID.randomUUID().toString(),
                    vin, ProtocolId.JT808, ingestTime, ingestTime, null,
                    withMeta(meta, "rawBody", "true"),
                    raw.messageId(),
                    raw.bytes()));

            case Jt808Body.Malformed malformed -> {
                String errorMessage = malformed.errorMessage() == null ? "" : malformed.errorMessage();
                boolean processingError = isProcessingError(errorMessage);
                boolean frameError = isFrameError(errorMessage);
                String publicErrorMessage = processingError
                        ? errorMessage.substring(PROCESSING_ERROR_PREFIX.length())
                        : errorMessage;
                Map<String, String> errorMeta = withMeta(meta,
                        processingError ? "processingError" : frameError ? "frameError" : "parseError", "true");
                errorMeta = withMeta(errorMeta, "peer", malformed.peer() == null ? "" : malformed.peer());
                errorMeta = withMeta(errorMeta,
                        processingError ? "processingErrorMessage" : frameError ? "frameErrorMessage" : "parseErrorMessage",
                        publicErrorMessage);
                yield List.of(new VehicleEvent.Passthrough(
                        UUID.randomUUID().toString(),
                        vin, ProtocolId.JT808, ingestTime, ingestTime, null,
                        errorMeta,
                        header.messageId(),
                        malformed.bytes() == null ? new byte[0] : malformed.bytes()));
            }
        };
    }

    private IdentityResolution resolveIdentity(Jt808Message message) {
        if (message.body() instanceof Jt808Body.Malformed) {
            // 坏帧没有可信 header/body，统一 unknown，原始字节仍由 passthrough 事件保留。
            return IdentityResolution.ok(new VehicleIdentity("unknown", false, VehicleIdentitySource.UNKNOWN));
        }
        var header = message.header();
        try {
            return IdentityResolution.ok(normalizeUnresolvedIdentity(identityResolver.resolve(new VehicleIdentityLookup(
                    ProtocolId.JT808, "", header.phone(), deviceId(message.body()), plate(message.body())))));
        } catch (RuntimeException e) {
            return new IdentityResolution(
                    new VehicleIdentity("unknown", false, VehicleIdentitySource.UNKNOWN),
                    e.getMessage() == null ? "" : e.getMessage());
        }
    }

    private static VehicleIdentity normalizeUnresolvedIdentity(VehicleIdentity identity) {
        if (identity == null) {
            return new VehicleIdentity("unknown", false, VehicleIdentitySource.UNKNOWN);
        }
        if (identity.resolved()) {
            return identity;
        }
        return new VehicleIdentity("unknown", false, identity.source());
    }

    private static Map<String, String> baseMeta(Jt808Header header, IdentityResolution identity) {
        java.util.HashMap<String, String> meta = new java.util.HashMap<>();
        meta.put("messageId", "0x" + Integer.toHexString(header.messageId()));
        meta.put("protocolVersion", header.version().name());
        meta.put("vin", identity.vin());
        meta.put("phone", header.phone());
        meta.put("identityResolved", Boolean.toString(identity.identity().resolved()));
        meta.put("identitySource", identity.identity().source().name());
        if (identity.errorMessage() != null) {
            meta.put("identityError", "true");
            meta.put("identityErrorMessage", identity.errorMessage());
        }
        return Map.copyOf(meta);
    }

    private static boolean isFrameError(String errorMessage) {
        return errorMessage != null && (errorMessage.startsWith("jt808 missing frame boundary")
                || errorMessage.startsWith("jt808 leading bytes before frame boundary")
                || errorMessage.startsWith("jt808 unclosed frame too large"));
    }

    private static boolean isProcessingError(String errorMessage) {
        return errorMessage != null && errorMessage.startsWith(PROCESSING_ERROR_PREFIX);
    }

    private static List<VehicleEvent> locationBatchEvents(String vin,
                                                          Map<String, String> meta,
                                                          Jt808Body.LocationBatch batch,
                                                          Instant ingestTime) {
        List<VehicleEvent> out = new ArrayList<>(batch.locations().size());
        Map<String, String> batchMeta = withMeta(meta, "batchType", Integer.toString(batch.batchType()));
        // 批量位置拆成多条 Location 事件，方便历史库按单点时间查询和导出。
        for (Jt808Body.Location loc : batch.locations()) {
            out.add(locationEvent(vin, batchMeta, loc, ingestTime));
        }
        return out;
    }

    private static List<VehicleEvent> mediaUploadEvents(String vin,
                                                        Map<String, String> meta,
                                                        Jt808Body.MediaUpload media,
                                                        Instant ingestTime) {
        List<VehicleEvent> out = new ArrayList<>(2);
        if (media.location() != null) {
            // 多媒体上传消息内嵌位置时同时产出 Location，避免只看到媒体元数据看不到拍摄位置。
            out.add(locationEvent(vin, withMeta(meta, "mediaId", Long.toString(media.mediaId())),
                    media.location(), ingestTime));
        }
        out.add(new VehicleEvent.MediaMeta(
                UUID.randomUUID().toString(),
                vin, ProtocolId.JT808, ingestTime, ingestTime, null,
                withMeta(meta, "mediaEventCode", Integer.toString(media.eventCode())),
                Long.toString(media.mediaId()),
                mediaType(media.mediaType(), media.mediaFormat(), media.channelId()),
                media.data() == null ? 0 : media.data().length,
                ""));
        return out;
    }

    private static Map<String, String> passengerVolumeMeta(Map<String, String> meta,
                                                           Jt808Body.PassengerVolume volume) {
        java.util.HashMap<String, String> copy = new java.util.HashMap<>(meta);
        copy.put("jt1078Message", "passengerVolume");
        copy.put("channelId", Integer.toString(volume.channelId()));
        copy.put("startTime", volume.startTime() == null ? "" : volume.startTime());
        copy.put("endTime", volume.endTime() == null ? "" : volume.endTime());
        copy.put("passengerGetOn", Integer.toString(volume.passengerGetOn()));
        copy.put("passengerGetOff", Integer.toString(volume.passengerGetOff()));
        return Map.copyOf(copy);
    }

    private static VehicleEvent.Location locationEvent(String vin,
                                                       Map<String, String> meta,
                                                       Jt808Body.Location loc,
                                                       Instant ingestTime) {
        return new VehicleEvent.Location(
                UUID.randomUUID().toString(),
                vin, ProtocolId.JT808, loc.deviceTime(), ingestTime, null, locationMeta(meta, loc),
                new LocationPayload(loc.longitude(), loc.latitude(),
                        loc.altitudeM(), loc.speedKmh(), loc.directionDeg(),
                        loc.alarmFlag(), loc.statusFlag(), totalMileageKm(loc)));
    }

    private static Map<String, String> locationMeta(Map<String, String> meta, Jt808Body.Location loc) {
        java.util.HashMap<String, String> copy = new java.util.HashMap<>(meta);
        copy.putAll(Jt808LocationFlagInfo.decodeStatus(loc.statusFlag(), "jt808.status."));
        copy.putAll(Jt808LocationFlagInfo.decodeAlarm(loc.alarmFlag(), "jt808.alarm."));
        if (!loc.extensionItems().isEmpty()) {
            loc.extensionItems().forEach((id, bytes) ->
                    copy.put("jt808.extra.0x" + String.format("%02X", id), hex(bytes)));
            copy.putAll(Jt808LocationAdditionalInfo.decode(loc.extensionItems()));
            Double totalMileageKm = totalMileageKm(loc);
            if (totalMileageKm != null) {
                copy.put("total_mileage_km", TelemetryMetadataValues.doubleValue(totalMileageKm));
            }
        }
        return Map.copyOf(copy);
    }

    private static Map<String, String> registerMeta(Map<String, String> meta, Jt808Body.Register reg) {
        java.util.HashMap<String, String> copy = new java.util.HashMap<>(meta);
        copy.put("jt808.register.province", Integer.toString(reg.province()));
        copy.put("jt808.register.city", Integer.toString(reg.city()));
        copy.put("jt808.register.maker", nullToEmpty(reg.maker()));
        copy.put("jt808.register.deviceType", nullToEmpty(reg.deviceType()));
        copy.put("jt808.register.deviceId", nullToEmpty(reg.deviceId()));
        copy.put("jt808.register.plateColor", Integer.toString(reg.plateColor()));
        copy.put("jt808.register.plate", nullToEmpty(reg.plate()));
        return Map.copyOf(copy);
    }

    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);
    }

    private static Double totalMileageKm(Jt808Body.Location loc) {
        return Jt808LocationAdditionalInfo.mileageKm(loc.extensionItems());
    }

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

    private static byte[] encodeParamsSummary(Jt808Body.ParamsReport params) {
        return ("responseSerialNo=" + params.responseSerialNo()
                + ",parameters=" + params.parameters().keySet()).getBytes(java.nio.charset.StandardCharsets.UTF_8);
    }

    private static String mediaType(int mediaType, int mediaFormat, int channelId) {
        return "type=" + mediaType + ",format=" + mediaFormat + ",channel=" + channelId;
    }

    private static String mediaAttributesType(Jt808Body.MediaAttributes attrs) {
        return "audioEncoding=" + attrs.inputAudioEncoding()
                + ",audioChannels=" + attrs.inputAudioChannels()
                + ",audioSampleRate=" + attrs.inputAudioSampleRate()
                + ",audioBitDepth=" + attrs.inputAudioBitDepth()
                + ",audioFrameLength=" + attrs.audioFrameLength()
                + ",audioOutput=" + attrs.audioOutputSupported()
                + ",videoEncoding=" + attrs.videoEncoding()
                + ",maxAudioChannels=" + attrs.maxAudioChannels()
                + ",maxVideoChannels=" + attrs.maxVideoChannels();
    }

    private static String deviceId(Jt808Body body) {
        if (body instanceof Jt808Body.Register reg) {
            return reg.deviceId();
        }
        if (body instanceof Jt808Body.Auth auth) {
            return auth.imei();
        }
        return "";
    }

    private static String plate(Jt808Body body) {
        return body instanceof Jt808Body.Register reg ? reg.plate() : "";
    }

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

    private record IdentityResolution(VehicleIdentity identity, String errorMessage) {
        private static IdentityResolution ok(VehicleIdentity identity) {
            return new IdentityResolution(identity, null);
        }

        private String vin() {
            return identity.vin();
        }
    }
}
