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 io.github.resilience4j.ratelimiter.RateLimiter;
import io.github.resilience4j.ratelimiter.RateLimiterConfig;
import org.springframework.core.Ordered;

import java.time.Duration;

/**
 * 单 VIN 速率限制。每个 VIN 一个独立的 Resilience4j RateLimiter，由 Caffeine 按 LRU 管理。
 *
 * <p>这里限的是进入业务 Handler 前的原始帧，不区分命令类型；32960 生产侧如需允许登入/登出穿透，
 * 应在此处扩展白名单或改为按命令配置，而不是在存储层丢弃。
 */
public class RateLimitInterceptor implements IngestInterceptor, Ordered {

    private final Cache<String, RateLimiter> limiters;
    private final RateLimiterConfig defaultConfig;

    public RateLimitInterceptor(int perVinQps, int maxVins) {
        this.defaultConfig = RateLimiterConfig.custom()
                .limitForPeriod(perVinQps)
                .limitRefreshPeriod(Duration.ofSeconds(1))
                .timeoutDuration(Duration.ZERO)
                .build();
        this.limiters = Caffeine.newBuilder()
                .maximumSize(maxVins)
                .expireAfterAccess(Duration.ofMinutes(10))
                .build();
    }

    @Override
    public boolean before(RawFrame frame, IngestContext ctx) {
        String limiterKey = limiterKey(frame);
        // 车辆粒度隔离：VIN 可用时按 VIN；JT808 等未映射 VIN 的协议按 vehicleKey/phone 兜底，
        // 避免所有 unknown 终端挤在同一个限流桶里。
        RateLimiter rl = limiters.get(limiterKey, k -> RateLimiter.of("vehicle-" + k, defaultConfig));
        if (!rl.acquirePermission()) {
            ctx.abort("rate-limited:" + limiterKey);
            return false;
        }
        return true;
    }

    private static String limiterKey(RawFrame frame) {
        String vin = meta(frame, "vin");
        if (isKnown(vin)) {
            return "vin:" + vin;
        }
        String vehicleKey = meta(frame, "vehicleKey");
        if (isKnown(vehicleKey)) {
            return "vehicleKey:" + vehicleKey;
        }
        String phone = meta(frame, "phone");
        if (isKnown(phone)) {
            return "phone:" + phone;
        }
        return "unknown";
    }

    private static String meta(RawFrame frame, String key) {
        if (frame == null || frame.sourceMeta() == null) {
            return "";
        }
        return frame.sourceMeta().getOrDefault(key, "").trim();
    }

    private static boolean isKnown(String value) {
        return value != null && !value.isBlank() && !"unknown".equalsIgnoreCase(value.trim());
    }

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