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

import com.lingniu.ingest.api.ProtocolId;
import com.lingniu.ingest.api.pipeline.RawFrame;
import com.lingniu.ingest.core.dispatcher.Dispatcher;
import com.lingniu.ingest.identity.VehicleIdentity;
import com.lingniu.ingest.identity.VehicleIdentityBinding;
import com.lingniu.ingest.identity.VehicleIdentityLookup;
import com.lingniu.ingest.identity.VehicleIdentityRegistry;
import com.lingniu.ingest.identity.VehicleIdentityResolver;
import com.lingniu.ingest.identity.VehicleIdentitySource;
import com.lingniu.ingest.protocol.jt808.codec.Jt808FrameEncoder;
import com.lingniu.ingest.protocol.jt808.codec.Jt808MalformedFrame;
import com.lingniu.ingest.protocol.jt808.codec.Jt808MessageDecoder;
import com.lingniu.ingest.protocol.jt808.downlink.Jt808Commands;
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 com.lingniu.ingest.protocol.jt808.model.Jt808MessageId;
import com.lingniu.ingest.protocol.jt808.session.Jt808ChannelRegistry;
import com.lingniu.ingest.protocol.jt808.session.Jt808PendingRequests;
import com.lingniu.ingest.session.DeviceSession;
import com.lingniu.ingest.session.SessionStore;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

/**
 * JT808 Netty 入站处理器。职责：
 * <ol>
 *   <li>解码 → 构造 RawFrame → 交给 {@link Dispatcher}
 *   <li>对 0x0100 注册 / 0x0102 鉴权 → 创建 / 更新 {@link DeviceSession} 并绑定 Channel
 *   <li>对 0x0001 终端通用应答 → 唤醒 {@link Jt808PendingRequests} 里等待的 future
 *   <li>对 0x0002 心跳 → 下发 0x8001 通用应答（若平台要求）
 *   <li>连接断开 → 清理 Channel 绑定
 * </ol>
 */
public class Jt808ChannelHandler extends SimpleChannelInboundHandler<Object> {

    private static final Logger log = LoggerFactory.getLogger(Jt808ChannelHandler.class);
    private static final String PROCESSING_ERROR_PREFIX = "processing failed: ";

    private final Jt808MessageDecoder decoder;
    private final Dispatcher dispatcher;
    private final SessionStore sessions;
    private final VehicleIdentityRegistry identityRegistry;
    private final VehicleIdentityResolver identityResolver;
    private final Jt808ChannelRegistry channelRegistry;
    private final Jt808PendingRequests pendingRequests;

    public Jt808ChannelHandler(Jt808MessageDecoder decoder, Dispatcher dispatcher,
                               SessionStore sessions, VehicleIdentityRegistry identityRegistry,
                               Jt808ChannelRegistry channelRegistry,
                               Jt808PendingRequests pendingRequests) {
        this(decoder, dispatcher, sessions, identityRegistry,
                identityRegistry instanceof VehicleIdentityResolver resolver ? resolver : null,
                channelRegistry, pendingRequests);
    }

    public Jt808ChannelHandler(Jt808MessageDecoder decoder, Dispatcher dispatcher,
                               SessionStore sessions, VehicleIdentityRegistry identityRegistry,
                               VehicleIdentityResolver identityResolver,
                               Jt808ChannelRegistry channelRegistry,
                               Jt808PendingRequests pendingRequests) {
        this.decoder = decoder;
        this.dispatcher = dispatcher;
        this.sessions = sessions;
        this.identityRegistry = identityRegistry;
        this.identityResolver = identityResolver;
        this.channelRegistry = channelRegistry;
        this.pendingRequests = pendingRequests;
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) {
        if (msg instanceof byte[] frame) {
            handleDecodedFrame(ctx, frame);
        } else if (msg instanceof Jt808MalformedFrame malformed) {
            dispatchFrameError(ctx, malformed);
        } else {
            ctx.fireChannelRead(msg);
        }
    }

    private void handleDecodedFrame(ChannelHandlerContext ctx, byte[] frame) {
        Jt808Message msg;
        try {
            msg = decoder.decode(ByteBuffer.wrap(frame));
        } catch (Exception e) {
            log.warn("[jt808] decode failed peer={} len={} error={}", addr(ctx), frame.length, e.getMessage());
            log.debug("[jt808] decode failed stack peer={}", addr(ctx), e);
            dispatchMalformed(ctx, frame, e.getMessage(), false);
            return;
        }

        try {
            processDecodedFrame(ctx, frame, msg);
        } catch (RuntimeException e) {
            // 解码成功但业务处理失败时仍派发一条 malformed RawFrame，
            // 保证原始帧能被 archive、Kafka 和历史索引留痕，方便事后排查。
            log.warn("[jt808] processing failed peer={} phone={} msgId=0x{} error={}",
                    addr(ctx), msg.header().phone(), Integer.toHexString(msg.header().messageId()), e.getMessage());
            log.debug("[jt808] processing failed stack peer={}", addr(ctx), e);
            dispatchProcessingError(ctx, frame, msg, e.getMessage());
        }
    }

    private void processDecodedFrame(ChannelHandlerContext ctx, byte[] frame, Jt808Message msg) {
        // ====== 会话维护 & 同步应答匹配 ======
        String phone = msg.header().phone();
        IdentityResolution identity = resolveIdentity(msg);
        if (msg.body() instanceof Jt808Body.Register reg) {
            identity = registerIdentity(msg, reg, identity);
        }
        byte[] registerAck = null;
        switch (msg.header().messageId()) {
            case Jt808MessageId.TERMINAL_REGISTER -> {
                DeviceSession session = upsertSession(ctx, msg, identity.identity());
                channelRegistry.bind(phone, ctx.channel());
                var command = Jt808Commands.registerAck(msg.header().serialNo(), 0, session.token());
                registerAck = Jt808FrameEncoder.encode(
                        command.messageId(),
                        phone,
                        channelRegistry.nextSerial(phone),
                        command.body());
            }
            case Jt808MessageId.TERMINAL_AUTH -> {
                upsertSession(ctx, msg, identity.identity());
                channelRegistry.bind(phone, ctx.channel());
                ackTerminal(ctx, phone, msg.header().serialNo(), msg.header().messageId(), 0);
            }
            case Jt808MessageId.TERMINAL_HEARTBEAT -> {
                channelRegistry.bind(phone, ctx.channel());
                ackTerminal(ctx, phone, msg.header().serialNo(), msg.header().messageId(), 0);
            }
            case Jt808MessageId.TERMINAL_GENERAL_RESPONSE -> {
                // body: ackSerial(2) + ackMsgId(2) + result(1)
                if (msg.body() instanceof Jt808Body.Raw raw && raw.bytes().length >= 5) {
                    int ackSerial = ((raw.bytes()[0] & 0xFF) << 8) | (raw.bytes()[1] & 0xFF);
                    pendingRequests.complete(phone, ackSerial, msg);
                }
            }
            default -> {
                // 定位/其他上行：也可能是对同步命令的应答
                pendingRequests.complete(phone, msg.header().serialNo(), msg);
            }
        }

        // ====== 派发到 Dispatcher ======
        Map<String, String> meta = new HashMap<>(32);
        meta.put("vin", identity.identity().vin());
        meta.put("phone", 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());
        }
        meta.put("seq", Integer.toString(msg.header().serialNo()));
        meta.put("messageId", "0x" + Jt808MessageId.hex(msg.header().messageId()));
        meta.put("messageName", Jt808MessageId.nameOf(msg.header().messageId()));
        meta.put("reportType", Jt808MessageId.reportTypeOf(msg.header().messageId()));
        putBodyMetadata(meta, msg.body());
        meta.put("peer", addr(ctx));
        RawFrame rf = new RawFrame(
                ProtocolId.JT808,
                msg.header().messageId(),
                0,
                msg,
                frame,
                meta,
                Instant.now());
        if (registerAck != null) {
            byte[] ack = registerAck;
            dispatcher.dispatchAndAwait(rf)
                    .thenRun(() -> ctx.writeAndFlush(Unpooled.wrappedBuffer(ack)))
                    .exceptionally(ex -> {
                        log.warn("[jt808] register ack withheld because dispatch durability failed peer={} phone={} seq={} error={}",
                                addr(ctx), phone, msg.header().serialNo(),
                                ex.getMessage() == null ? ex.getClass().getSimpleName() : ex.getMessage());
                        return null;
                    });
            return;
        }
        dispatcher.dispatch(rf);
    }

    private IdentityResolution resolveIdentity(Jt808Message msg) {
        String phone = msg.header().phone();
        if (identityResolver == null) {
            // 没有绑定表时不伪造 VIN；phone 会保留在 metadata/session 中用于下行定位。
            return new IdentityResolution(
                    new VehicleIdentity("unknown", false, VehicleIdentitySource.FALLBACK_PHONE),
                    null);
        }
        try {
            // 优先把 JT808 phone/device/plate 映射到内部 VIN，后续所有事件都用内部 VIN 做主键。
            return new IdentityResolution(
                    normalizeUnresolvedIdentity(identityResolver.resolve(new VehicleIdentityLookup(
                            ProtocolId.JT808,
                            "",
                            phone,
                            deviceId(msg.body()),
                            plate(msg.body())))),
                    null);
        } 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 IdentityResolution registerIdentity(Jt808Message msg,
                                                Jt808Body.Register reg,
                                                IdentityResolution current) {
        VehicleIdentity currentIdentity = current.identity();
        boolean alreadyResolved = currentIdentity != null && currentIdentity.resolved();
        String vin = alreadyResolved ? currentIdentity.vin() : generatedInternalVin(msg.header().phone(), reg);
        identityRegistry.bind(new VehicleIdentityBinding(
                ProtocolId.JT808,
                vin,
                msg.header().phone(),
                reg.deviceId(),
                reg.plate(),
                Map.of(
                        "province", Integer.toString(reg.province()),
                        "city", Integer.toString(reg.city()),
                        "maker", reg.maker(),
                        "deviceType", reg.deviceType(),
                        "plateColor", Integer.toString(reg.plateColor()))));
        VehicleIdentitySource source = alreadyResolved ? currentIdentity.source() : VehicleIdentitySource.REGISTERED;
        return new IdentityResolution(new VehicleIdentity(vin, true, source), current.errorMessage());
    }

    private void dispatchFrameError(ChannelHandlerContext ctx, Jt808MalformedFrame malformed) {
        String peer = malformed.peer() == null || malformed.peer().isBlank() ? addr(ctx) : malformed.peer();
        dispatchMalformed(peer, malformed.rawBytes(), malformed.reason(), true);
    }

    private void dispatchMalformed(ChannelHandlerContext ctx, byte[] frame, String errorMessage, boolean frameError) {
        dispatchMalformed(addr(ctx), frame, errorMessage, frameError);
    }

    private void dispatchMalformed(String peer, byte[] frame, String errorMessage, boolean frameError) {
        String message = errorMessage == null ? "" : errorMessage;
        // 无法解出 header 的坏帧也进入 Dispatcher，后续 archive sink 可以保留 raw bytes。
        Jt808Message malformed = new Jt808Message(
                new Jt808Header(0, frame == null ? 0 : frame.length, 0, false,
                        Jt808Header.ProtocolVersion.V2013, "unknown", 0, 0, 0),
                new Jt808Body.Malformed(frame == null ? new byte[0] : frame, peer, message));
        Map<String, String> meta = new HashMap<>(8);
        meta.put("vin", "unknown");
        meta.put("peer", peer);
        meta.put("identityResolved", "false");
        meta.put("identitySource", VehicleIdentitySource.UNKNOWN.name());
        if (frameError) {
            meta.put("frameError", "true");
            meta.put("frameErrorMessage", message);
        } else {
            meta.put("parseError", "true");
            meta.put("parseErrorMessage", message);
        }
        putBodyMetadata(meta, malformed.body());
        dispatcher.dispatch(new RawFrame(
                ProtocolId.JT808,
                0,
                0,
                malformed,
                frame == null ? new byte[0] : frame,
                meta,
                Instant.now()));
    }

    private void dispatchProcessingError(ChannelHandlerContext ctx, byte[] frame, Jt808Message msg, String errorMessage) {
        String message = errorMessage == null ? "" : errorMessage;
        Jt808Message failed = new Jt808Message(
                new Jt808Header(0, frame == null ? 0 : frame.length, 0, false,
                        msg.header().version(), msg.header().phone(), msg.header().serialNo(), 0, 0),
                new Jt808Body.Malformed(frame == null ? new byte[0] : frame, addr(ctx),
                        PROCESSING_ERROR_PREFIX + message));
        Map<String, String> meta = new HashMap<>(16);
        meta.put("vin", "unknown");
        meta.put("phone", msg.header().phone());
        meta.put("peer", addr(ctx));
        meta.put("seq", Integer.toString(msg.header().serialNo()));
        meta.put("messageId", "0x" + Jt808MessageId.hex(msg.header().messageId()));
        meta.put("messageName", Jt808MessageId.nameOf(msg.header().messageId()));
        meta.put("reportType", Jt808MessageId.reportTypeOf(msg.header().messageId()));
        meta.put("identityResolved", "false");
        meta.put("identitySource", VehicleIdentitySource.UNKNOWN.name());
        meta.put("processingError", "true");
        meta.put("processingErrorMessage", message);
        putBodyMetadata(meta, failed.body());
        dispatcher.dispatch(new RawFrame(
                ProtocolId.JT808,
                0,
                0,
                failed,
                frame == null ? new byte[0] : frame,
                meta,
                Instant.now()));
    }

    private DeviceSession upsertSession(ChannelHandlerContext ctx, Jt808Message msg, VehicleIdentity identity) {
        String phone = msg.header().phone();
        String sessionId = "jt808-" + phone;
        DeviceSession session = sessions.findBySessionId(sessionId)
                .map(s -> s.touch(Instant.now()))
                .orElseGet(() -> new DeviceSession(
                        sessionId, ProtocolId.JT808, identity.vin(), phone,
                        generateToken(phone),
                        addr(ctx),
                        Instant.now(), Instant.now(),
                        Map.of("protocolVersion", msg.header().version().name())));
        session = session.withVin(identity.vin());
        sessions.put(session);
        return session;
    }

    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 void putBodyMetadata(Map<String, String> meta, Jt808Body body) {
        switch (body) {
            case Jt808Body.Register reg -> {
                put(meta, "body.province", reg.province());
                put(meta, "body.city", reg.city());
                put(meta, "body.maker", reg.maker());
                put(meta, "body.deviceType", reg.deviceType());
                put(meta, "body.deviceId", reg.deviceId());
                put(meta, "body.plateColor", reg.plateColor());
                put(meta, "body.plate", reg.plate());
            }
            case Jt808Body.Auth auth -> {
                put(meta, "body.token", auth.token());
                put(meta, "body.imei", auth.imei());
                put(meta, "body.softwareVersion", auth.softwareVersion());
            }
            case Jt808Body.Location location -> putLocationMetadata(meta, "body.", location);
            case Jt808Body.LocationBatch batch -> {
                put(meta, "body.batchType", batch.batchType());
                put(meta, "body.locationCount", batch.locations() == null ? 0 : batch.locations().size());
            }
            case Jt808Body.ParamsReport report -> {
                put(meta, "body.responseSerialNo", report.responseSerialNo());
                put(meta, "body.parameterCount", report.parameters() == null ? 0 : report.parameters().size());
                if (report.parameters() != null) {
                    report.parameters().forEach((id, bytes) ->
                            put(meta, "body.param.0x" + String.format("%08X", id), toHex(bytes)));
                }
            }
            case Jt808Body.TerminalAttrs attrs -> {
                put(meta, "body.terminalType", attrs.terminalType());
                put(meta, "body.maker", attrs.maker());
                put(meta, "body.terminalModel", attrs.terminalModel());
                put(meta, "body.terminalId", attrs.terminalId());
                put(meta, "body.iccid", attrs.iccid());
                put(meta, "body.hardwareVersion", attrs.hardwareVersion());
                put(meta, "body.firmwareVersion", attrs.firmwareVersion());
                put(meta, "body.gnssProperty", attrs.gnssProperty());
                put(meta, "body.commProperty", attrs.commProperty());
            }
            case Jt808Body.MediaEvent event -> {
                put(meta, "body.mediaId", event.mediaId());
                put(meta, "body.mediaType", event.mediaType());
                put(meta, "body.mediaFormat", event.mediaFormat());
                put(meta, "body.eventCode", event.eventCode());
                put(meta, "body.channelId", event.channelId());
            }
            case Jt808Body.MediaUpload upload -> {
                put(meta, "body.mediaId", upload.mediaId());
                put(meta, "body.mediaType", upload.mediaType());
                put(meta, "body.mediaFormat", upload.mediaFormat());
                put(meta, "body.eventCode", upload.eventCode());
                put(meta, "body.channelId", upload.channelId());
                put(meta, "body.dataSizeBytes", upload.data() == null ? 0 : upload.data().length);
                if (upload.location() != null) {
                    putLocationMetadata(meta, "body.location.", upload.location());
                }
            }
            case Jt808Body.MediaAttributes attrs -> {
                put(meta, "body.inputAudioEncoding", attrs.inputAudioEncoding());
                put(meta, "body.inputAudioChannels", attrs.inputAudioChannels());
                put(meta, "body.inputAudioSampleRate", attrs.inputAudioSampleRate());
                put(meta, "body.inputAudioBitDepth", attrs.inputAudioBitDepth());
                put(meta, "body.audioFrameLength", attrs.audioFrameLength());
                put(meta, "body.audioOutputSupported", attrs.audioOutputSupported());
                put(meta, "body.videoEncoding", attrs.videoEncoding());
                put(meta, "body.maxAudioChannels", attrs.maxAudioChannels());
                put(meta, "body.maxVideoChannels", attrs.maxVideoChannels());
                put(meta, "body.rawSizeBytes", attrs.raw() == null ? 0 : attrs.raw().length);
            }
            case Jt808Body.FileUploadComplete complete -> {
                put(meta, "body.responseSerialNo", complete.responseSerialNo());
                put(meta, "body.result", complete.result());
                put(meta, "body.rawSizeBytes", complete.raw() == null ? 0 : complete.raw().length);
            }
            case Jt808Body.FileList files -> {
                put(meta, "body.responseSerialNo", files.responseSerialNo());
                put(meta, "body.fileCount", files.fileCount());
                put(meta, "body.rawSizeBytes", files.raw() == null ? 0 : files.raw().length);
            }
            case Jt808Body.PassengerVolume volume -> {
                put(meta, "body.channelId", volume.channelId());
                put(meta, "body.startTime", volume.startTime());
                put(meta, "body.endTime", volume.endTime());
                put(meta, "body.passengerGetOn", volume.passengerGetOn());
                put(meta, "body.passengerGetOff", volume.passengerGetOff());
                put(meta, "body.rawSizeBytes", volume.raw() == null ? 0 : volume.raw().length);
            }
            case Jt808Body.Passthrough passthrough -> {
                put(meta, "body.passthroughType", "0x" + String.format("%02X", passthrough.passthroughType()));
                put(meta, "body.dataSizeBytes", passthrough.data() == null ? 0 : passthrough.data().length);
                put(meta, "body.dataHex", toHex(passthrough.data()));
            }
            case Jt808Body.Raw raw -> {
                put(meta, "body.rawMessageId", "0x" + Jt808MessageId.hex(raw.messageId()));
                put(meta, "body.rawSizeBytes", raw.bytes() == null ? 0 : raw.bytes().length);
                put(meta, "body.rawHex", toHex(raw.bytes()));
            }
            case Jt808Body.Malformed malformed -> {
                put(meta, "body.peer", malformed.peer());
                put(meta, "body.errorMessage", malformed.errorMessage());
                put(meta, "body.rawSizeBytes", malformed.bytes() == null ? 0 : malformed.bytes().length);
            }
            case Jt808Body.Heartbeat ignored -> {
            }
            case Jt808Body.Unregister ignored -> {
            }
        }
    }

    private static void putLocationMetadata(Map<String, String> meta, String prefix, Jt808Body.Location location) {
        put(meta, prefix + "alarmFlag", location.alarmFlag());
        put(meta, prefix + "statusFlag", location.statusFlag());
        meta.putAll(Jt808LocationFlagInfo.decodeStatus(location.statusFlag(), prefix + "status."));
        meta.putAll(Jt808LocationFlagInfo.decodeAlarm(location.alarmFlag(), prefix + "alarm."));
        put(meta, prefix + "longitude", location.longitude());
        put(meta, prefix + "latitude", location.latitude());
        put(meta, prefix + "altitudeM", location.altitudeM());
        put(meta, prefix + "speedKmh", location.speedKmh());
        put(meta, prefix + "directionDeg", location.directionDeg());
        put(meta, prefix + "deviceTime", location.deviceTime());
        if (location.extensionItems() != null) {
            location.extensionItems().forEach((id, bytes) ->
                    put(meta, prefix + "extra.0x" + String.format("%02X", id), toHex(bytes)));
            meta.putAll(Jt808LocationAdditionalInfo.decode(location.extensionItems(), prefix + "extra."));
        }
    }

    private static void put(Map<String, String> meta, String key, Object value) {
        if (value != null) {
            meta.put(key, String.valueOf(value));
        }
    }

    private static String toHex(byte[] bytes) {
        if (bytes == null || bytes.length == 0) {
            return "";
        }
        StringBuilder out = new StringBuilder(bytes.length * 2);
        for (byte b : bytes) {
            out.append(String.format("%02X", b & 0xFF));
        }
        return out.toString();
    }

    private void ackTerminal(ChannelHandlerContext ctx, String phone, int ackSerial, int ackMsgId, int result) {
        var cmd = Jt808Commands.platformAck(ackSerial, ackMsgId, result);
        byte[] frame = Jt808FrameEncoder.encode(cmd.messageId(), phone, channelRegistry.nextSerial(phone), cmd.body());
        ctx.writeAndFlush(Unpooled.wrappedBuffer(frame));
    }

    private static String generateToken(String phone) {
        return UUID.nameUUIDFromBytes(("jt808:" + phone).getBytes()).toString().replace("-", "").substring(0, 16);
    }

    private static String generatedInternalVin(String phone, Jt808Body.Register reg) {
        String material = String.join("|", phone, reg.deviceId(), reg.plate(), reg.maker(), reg.deviceType());
        try {
            byte[] digest = MessageDigest.getInstance("SHA-256").digest(material.getBytes(StandardCharsets.UTF_8));
            StringBuilder suffix = new StringBuilder(12);
            for (int i = 0; i < digest.length && suffix.length() < 12; i++) {
                suffix.append(String.format("%02X", digest[i] & 0xFF));
            }
            return "JT808" + suffix.substring(0, 12);
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException("SHA-256 not available", e);
        }
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) {
        channelRegistry.unbind(ctx.channel())
                .ifPresent(phone -> sessions.remove("jt808-" + phone));
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        log.warn("[jt808] channel error peer={}", addr(ctx), cause);
        ctx.close();
    }

    private static String addr(ChannelHandlerContext ctx) {
        if (ctx.channel().remoteAddress() instanceof InetSocketAddress i) {
            return i.getAddress().getHostAddress() + ":" + i.getPort();
        }
        return "unknown";
    }

    private record IdentityResolution(VehicleIdentity identity, String errorMessage) {}
}
