package com.lingniu.ingest.core.pipeline.builtin;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.lingniu.ingest.api.pipeline.IngestContext;
import com.lingniu.ingest.api.pipeline.IngestInterceptor;
import com.lingniu.ingest.api.pipeline.RawFrame;
import org.springframework.core.Ordered;

import java.util.Arrays;
import java.time.Duration;
import java.util.Map;

/**
 * 基于 Caffeine 的本地幂等去重。
 *
 * <p>Key 构造：{@code protocolId + vehicle identity + command + seq/fingerprint}。vehicle identity
 * 优先使用已解析 VIN；VIN 未解析时使用 phone/deviceId/terminalId/plate，避免 JT808 多终端在
 * {@code vin=unknown} 时同流水号互相去重。如果上游已经在 {@link RawFrame#sourceMeta()} 里带了
 * {@code seq}，优先用真实流水号；否则退化为 raw bytes fingerprint，避免设备重连或 TCP 重发导致
 * 同一帧重复进入 Archive/Kafka/历史索引。
 *
 * <p>当前缓存是进程内的，只能保证单实例去重。32960 若做多副本水平扩容，需要把这个位置替换成
 * Redis/集中式幂等键，否则不同实例仍可能各自接收一次相同原始包。
 */
public class DedupInterceptor implements IngestInterceptor, Ordered {

    private final Cache<String, Boolean> seen;

    public DedupInterceptor(int maxSize, long ttlSeconds) {
        this.seen = Caffeine.newBuilder()
                .maximumSize(maxSize)
                .expireAfterWrite(Duration.ofSeconds(ttlSeconds))
                .build();
    }

    @Override
    public boolean before(RawFrame frame, IngestContext ctx) {
        Map<String, String> meta = frame.sourceMeta() == null ? Map.of() : frame.sourceMeta();
        String identity = identityKey(frame, meta);
        String seq = meta.get("seq");
        if (seq == null || seq.isBlank()) {
            // 某些入口没有硬件流水号，使用原始字节哈希作为保底键，至少能挡住同连接内的重复帧。
            seq = rawFingerprint(frame);
        }
        String key = frame.protocolId() + ":" + identity + ":" + frame.command() + ":" + seq;
        if (seen.asMap().putIfAbsent(key, Boolean.TRUE) != null) {
            ctx.abort("duplicate:" + key);
            return false;
        }
        return true;
    }

    @Override
    public int getOrder() {
        return 100;
    }

    private static String identityKey(RawFrame frame, Map<String, String> meta) {
        String vin = meta.getOrDefault("vin", "unknown").trim();
        if (!vin.isBlank() && !"unknown".equalsIgnoreCase(vin)) {
            return "vin:" + vin;
        }
        for (String key : new String[]{"phone", "deviceId", "terminalId", "plate"}) {
            String value = meta.get(key);
            if (value != null && !value.isBlank()) {
                return key + ":" + value.trim();
            }
        }
        return "raw:" + rawFingerprint(frame);
    }

    private static String rawFingerprint(RawFrame frame) {
        byte[] rawBytes = frame.rawBytes();
        if (rawBytes != null && rawBytes.length > 0) {
            return "raw:" + Integer.toHexString(Arrays.hashCode(rawBytes));
        }
        return "receivedAt:" + frame.receivedAt();
    }
}
