package com.seer.teach.netty.im.utils;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.stereotype.Component;

import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Objects;
import java.util.concurrent.*;

@Slf4j
@Component
@RequiredArgsConstructor
public class RedisPubSub implements InitializingBean, DisposableBean {

    private final RedisConnectionFactory connectionFactory;

    private final StringRedisTemplate redis;

    private RedisMessageListenerContainer container;
    private final ScheduledExecutorService scheduler =
            Executors.newSingleThreadScheduledExecutor(r -> {
                Thread t = new Thread(r, "redis-await-timeout");
                t.setDaemon(true);
                return t;
            });

    @Override
    public void afterPropertiesSet() {
        container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        container.afterPropertiesSet();
        container.start();
    }

    @Override
    public void destroy() {
        if (container != null) container.stop();
        scheduler.shutdownNow();
    }

    public void publish(String channel, String payload) {
        Objects.requireNonNull(channel, "channel");
        redis.convertAndSend(channel, payload == null ? "" : payload);
    }

    public String await(String channel, Duration timeout)
            throws InterruptedException, ExecutionException, TimeoutException {
        Objects.requireNonNull(channel, "channel");
        Objects.requireNonNull(timeout, "timeout");
        CompletableFuture<String> future = new CompletableFuture<>();
        ChannelTopic topic = new ChannelTopic(channel);
        MessageListener listener = (Message msg, byte[] pattern) ->
                future.complete(new String(msg.getBody(), StandardCharsets.UTF_8));

        container.addMessageListener(listener, topic);
        ScheduledFuture<?> timeoutTask = scheduler.schedule(
                () -> future.completeExceptionally(new TimeoutException("Await timeout: " + channel)),
                timeout.toMillis(), TimeUnit.MILLISECONDS);

        try {
            return future.get();
        } finally {
            container.removeMessageListener(listener, topic);
            timeoutTask.cancel(true);
        }
    }
}
