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

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.lingniu.ingest.api.ProtocolId;
import com.lingniu.ingest.api.pipeline.RawFrame;
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.jsatl12.codec.Jsatl12RawFrame;
import com.lingniu.ingest.sink.archive.ArchiveStore;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;

/**
 * JSATL12 入站处理器。
 *
 * <p>当前实现：把 DATA_CHUNK 帧写入 {@link ArchiveStore} 后派发统一事件引用，文件名按日期 + 对端 IP +
 * 时间戳生成；JT_MESSAGE 帧复用 JT808 decoder 后进入统一 Dispatcher。
 *
 * <p>路径模板：{@code yyyy/MM/dd/<peerIp>/<epochMillis>.bin}。
 */
public class Jsatl12ChannelHandler extends SimpleChannelInboundHandler<Jsatl12RawFrame> {

    private static final Logger log = LoggerFactory.getLogger(Jsatl12ChannelHandler.class);
    private static final DateTimeFormatter DAY_FMT = DateTimeFormatter.ofPattern("yyyy/MM/dd").withZone(ZoneId.systemDefault());

    /** 为每个 Channel 持久化一个文件 key，保证同一次传输追加到同一个文件。 */
    private static final Cache<ChannelHandlerContext, String> KEYS = Caffeine.newBuilder()
            .expireAfterAccess(Duration.ofMinutes(30))
            .maximumSize(10_000)
            .build();

    private final ArchiveStore archive;
    private final Jsatl12JtMessageBridge jtMessageBridge;
    private final Jsatl12AttachmentTracker attachmentTracker;
    private final VehicleIdentityResolver identityResolver;
    private final Consumer<RawFrame> dataChunkDispatcher;

    public Jsatl12ChannelHandler(ArchiveStore archive, Jsatl12JtMessageBridge jtMessageBridge) {
        this(archive, jtMessageBridge, new Jsatl12AttachmentTracker(), ignored -> {});
    }

    public Jsatl12ChannelHandler(ArchiveStore archive,
                                 Jsatl12JtMessageBridge jtMessageBridge,
                                 Consumer<RawFrame> dataChunkDispatcher) {
        this(archive, jtMessageBridge, new Jsatl12AttachmentTracker(), null, dataChunkDispatcher);
    }

    public Jsatl12ChannelHandler(ArchiveStore archive,
                                 Jsatl12JtMessageBridge jtMessageBridge,
                                 Jsatl12AttachmentTracker attachmentTracker,
                                 Consumer<RawFrame> dataChunkDispatcher) {
        this(archive, jtMessageBridge, attachmentTracker, null, dataChunkDispatcher);
    }

    public Jsatl12ChannelHandler(ArchiveStore archive,
                                 Jsatl12JtMessageBridge jtMessageBridge,
                                 Jsatl12AttachmentTracker attachmentTracker,
                                 VehicleIdentityResolver identityResolver,
                                 Consumer<RawFrame> dataChunkDispatcher) {
        this.archive = archive;
        this.jtMessageBridge = jtMessageBridge;
        this.attachmentTracker = attachmentTracker == null ? new Jsatl12AttachmentTracker() : attachmentTracker;
        this.identityResolver = identityResolver;
        this.dataChunkDispatcher = dataChunkDispatcher;
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Jsatl12RawFrame frame) {
        switch (frame.type()) {
            case JT_MESSAGE -> {
                jtMessageBridge.dispatch(frame.bytes(), addr(ctx))
                        .ifPresent(response -> ctx.writeAndFlush(io.netty.buffer.Unpooled.wrappedBuffer(response)));
            }
            case MALFORMED -> {
                byte[] raw = frame.bytes() == null ? new byte[0] : frame.bytes();
                Instant now = Instant.now();
                String peer = addr(ctx);
                String errorMessage = "malformed jsatl12 frame";
                Jsatl12MalformedFrame malformed = new Jsatl12MalformedFrame(raw, peer, errorMessage, now);
                dataChunkDispatcher.accept(new RawFrame(
                        ProtocolId.JSATL12,
                        Jsatl12MessageId.MALFORMED_FRAME,
                        0,
                        malformed,
                        raw,
                        unresolvedMeta(
                                "source", "jsatl12-frame",
                                "peer", peer,
                                "frameError", "true",
                                "frameErrorMessage", errorMessage,
                                "frameSizeBytes", Integer.toString(raw.length)),
                        now));
            }
            case DATA_CHUNK -> {
                String key = KEYS.get(ctx, k -> {
                    String day = DAY_FMT.format(Instant.now());
                    return "jsatl12/" + day + "/" + addr(ctx).replace(':', '_')
                            + "/" + Instant.now().toEpochMilli() + ".bin";
                });
                try {
                    byte[] all = frame.bytes();
                    Jsatl12DataPacket packet = attachmentTracker.recordDataPacket(null, all);
                    byte[] payload = packet.data();
                    int payloadLen = payload.length;
                    if (payloadLen > 0) {
                        String phone = attachmentTracker.ownerPhone(packet.name());
                        IdentityResolution identity = resolveIdentity(phone);
                        String archiveRef = archive.append(key, payload);
                        Instant now = Instant.now();
                        Jsatl12ArchivedChunk archived = new Jsatl12ArchivedChunk(
                                archiveRef,
                                key,
                                packet.name(),
                                packet.offset(),
                                packet.length(),
                                payloadLen,
                                addr(ctx),
                                now,
                                phone,
                                identity.identity().vin(),
                                identity.identity().resolved(),
                                identity.identity().source().name());
                        dataChunkDispatcher.accept(new RawFrame(
                                ProtocolId.JSATL12,
                                Jsatl12MessageId.DATA_CHUNK_ARCHIVED,
                                0,
                                archived,
                                null,
                                identityMeta(identity,
                                        "phone", phone,
                                        "source", "jsatl12-data-chunk",
                                        "peer", addr(ctx),
                                        "fileName", packet.name(),
                                        "fileOffset", Long.toString(packet.offset()),
                                        "declaredChunkSizeBytes", Long.toString(packet.length()),
                                        "archiveRef", archiveRef,
                                        "archiveKey", key,
                                        "chunkSizeBytes", Integer.toString(payloadLen)),
                                now));
                    }
                } catch (IOException | RuntimeException e) {
                    log.warn("[jsatl12] write chunk failed key={}", key, e);
                    byte[] all = frame.bytes();
                    int payloadLen = Math.max(0, all.length - 62);
                    Instant now = Instant.now();
                    Jsatl12ArchiveFailure failure = new Jsatl12ArchiveFailure(
                            all,
                            key,
                            payloadLen,
                            addr(ctx),
                            e.getMessage() == null ? "" : e.getMessage(),
                            now);
                    dataChunkDispatcher.accept(new RawFrame(
                            ProtocolId.JSATL12,
                            Jsatl12MessageId.DATA_CHUNK_ARCHIVE_FAILED,
                            0,
                            failure,
                            all,
                            unresolvedMeta(
                                    "source", "jsatl12-data-chunk",
                                    "peer", addr(ctx),
                                    "archiveKey", key,
                                    "archiveError", "true",
                                    "archiveErrorMessage", failure.errorMessage(),
                                    "chunkSizeBytes", Long.toString(payloadLen)),
                            now));
                }
            }
        }
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) {
        String key = KEYS.getIfPresent(ctx);
        if (key != null) {
            log.info("[jsatl12] channel closed, archived key={}", key);
            KEYS.invalidate(ctx);
        }
    }

    private static Map<String, String> unresolvedMeta(String... kv) {
        HashMap<String, String> out = new HashMap<>();
        out.put("vin", "unknown");
        out.put("identityResolved", "false");
        out.put("identitySource", "UNKNOWN");
        for (int i = 0; i + 1 < kv.length; i += 2) {
            out.put(kv[i], kv[i + 1] == null ? "" : kv[i + 1]);
        }
        return Map.copyOf(out);
    }

    private IdentityResolution resolveIdentity(String phone) {
        if (identityResolver != null) {
            try {
                return new IdentityResolution(
                        identityResolver.resolve(new VehicleIdentityLookup(ProtocolId.JT808, "", phone, "", "")),
                        null);
            } catch (RuntimeException e) {
                return new IdentityResolution(
                        new VehicleIdentity("unknown", false, VehicleIdentitySource.UNKNOWN),
                        e.getMessage() == null ? "" : e.getMessage());
            }
        }
        if (phone != null && !phone.isBlank()) {
            return new IdentityResolution(new VehicleIdentity(phone, false, VehicleIdentitySource.FALLBACK_PHONE), null);
        }
        return new IdentityResolution(new VehicleIdentity("unknown", false, VehicleIdentitySource.UNKNOWN), null);
    }

    private static Map<String, String> identityMeta(IdentityResolution resolution, String... kv) {
        VehicleIdentity identity = resolution.identity();
        HashMap<String, String> out = new HashMap<>();
        out.put("vin", identity.vin());
        out.put("identityResolved", Boolean.toString(identity.resolved()));
        out.put("identitySource", identity.source().name());
        if (resolution.errorMessage() != null) {
            out.put("identityError", "true");
            out.put("identityErrorMessage", resolution.errorMessage());
        }
        for (int i = 0; i + 1 < kv.length; i += 2) {
            out.put(kv[i], kv[i + 1] == null ? "" : kv[i + 1]);
        }
        return Map.copyOf(out);
    }

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