package com.lingniu.ingest.sink.kafka;

import com.lingniu.ingest.api.event.VehicleEvent;
import com.lingniu.ingest.api.sink.EventSink;
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.CompletableFuture;

/**
 * Kafka Sink：序列化为 Protobuf Envelope，按 vin 分区，异步发送。
 *
 * <p>失败处理：Resilience4j 熔断；熔断开启时事件转投 DLQ topic。
 */
public final class KafkaEventSink implements EventSink, AutoCloseable {

    private static final Logger log = LoggerFactory.getLogger(KafkaEventSink.class);

    private final KafkaProducer<String, byte[]> producer;
    private final EnvelopeMapper mapper;
    private final TopicRouter router;
    private final String dlqTopic;
    private final CircuitBreaker breaker;

    public KafkaEventSink(KafkaProducer<String, byte[]> producer,
                          EnvelopeMapper mapper,
                          TopicRouter router,
                          String dlqTopic,
                          CircuitBreaker breaker) {
        this.producer = producer;
        this.mapper = mapper;
        this.router = router;
        this.dlqTopic = dlqTopic;
        this.breaker = breaker;
    }

    @Override
    public String name() {
        return "kafka";
    }

    /**
     * Split-service mode uses Kafka for both normalized events and raw archive records.
     */
    @Override
    public boolean accepts(VehicleEvent event) {
        return true;
    }

    @Override
    public CompletableFuture<Void> publish(VehicleEvent event) {
        CompletableFuture<Void> cf = new CompletableFuture<>();
        byte[] payload;
        try {
            payload = mapper.toEnvelope(event).toByteArray();
        } catch (Exception e) {
            log.error("envelope build failed eventId={} vin={}", event.eventId(), event.vin(), e);
            cf.completeExceptionally(e);
            return cf;
        }

        String topic = breaker.tryAcquirePermission() ? router.route(event) : dlqTopic;
        ProducerRecord<String, byte[]> record = new ProducerRecord<>(topic, partitionKey(event), payload);
        record.headers().add("event-id", event.eventId().getBytes());
        record.headers().add("trace-id", event.traceId() == null ? new byte[0] : event.traceId().getBytes());
        record.headers().add("source", event.source().name().getBytes());

        producer.send(record, (metadata, ex) -> {
            if (ex != null) {
                breaker.onError(0, java.util.concurrent.TimeUnit.MILLISECONDS, ex);
                cf.completeExceptionally(ex);
            } else {
                breaker.onSuccess(0, java.util.concurrent.TimeUnit.MILLISECONDS);
                cf.complete(null);
            }
        });
        return cf;
    }

    private static String partitionKey(VehicleEvent event) {
        String vin = event.vin();
        if (isKnown(vin)) {
            return vin;
        }
        String vehicleKey = metadata(event, "vehicleKey");
        if (!isKnown(vehicleKey)) {
            vehicleKey = metadata(event, "vehicle_key");
        }
        if (isKnown(vehicleKey)) {
            return "vehicleKey:" + vehicleKey;
        }
        String phone = metadata(event, "phone");
        if (isKnown(phone)) {
            return "phone:" + phone;
        }
        return vin == null || vin.isBlank() ? "unknown" : vin;
    }

    private static String metadata(VehicleEvent event, String key) {
        if (event.metadata() == null) {
            return "";
        }
        return event.metadata().getOrDefault(key, "").trim();
    }

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

    @Override
    public void close() {
        producer.flush();
        producer.close();
    }
}
