package tech.easyflow.common.mq.redis;

import jakarta.annotation.PreDestroy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.SmartLifecycle;
import org.springframework.data.domain.Range;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStreamCommands;
import org.springframework.data.redis.connection.stream.Consumer;
import org.springframework.data.redis.connection.stream.MapRecord;
import org.springframework.data.redis.connection.stream.PendingMessage;
import org.springframework.data.redis.connection.stream.RecordId;
import org.springframework.data.redis.connection.stream.StreamOffset;
import org.springframework.data.redis.connection.stream.StreamReadOptions;
import org.springframework.data.redis.core.StringRedisTemplate;
import tech.easyflow.common.mq.config.MQProperties;
import tech.easyflow.common.mq.core.MQAcknowledger;
import tech.easyflow.common.mq.core.MQConsumerContainer;
import tech.easyflow.common.mq.core.MQConsumerHandler;
import tech.easyflow.common.mq.core.MQDeadLetterService;
import tech.easyflow.common.mq.core.MQMessage;
import tech.easyflow.common.mq.core.MQMessageConverter;
import tech.easyflow.common.mq.core.MQSubscription;

import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Pattern;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class RedisMQConsumerContainer implements MQConsumerContainer, SmartLifecycle {

    private static final Logger LOG = LoggerFactory.getLogger(RedisMQConsumerContainer.class);
    private static final Pattern UNSAFE_CONSUMER_NAME_CHARS = Pattern.compile("[^A-Za-z0-9_.-]");

    private final RedisConnectionFactory redisConnectionFactory;
    private final StringRedisTemplate stringRedisTemplate;
    private final MQProperties properties;
    private final MQMessageConverter messageConverter;
    private final MQDeadLetterService deadLetterService;
    private final RedisStreamKeySupport keySupport;
    private final List<MQConsumerHandler> handlers;
    private final ExecutorService executorService;

    private volatile boolean running;

    public RedisMQConsumerContainer(RedisConnectionFactory redisConnectionFactory,
                                    StringRedisTemplate stringRedisTemplate,
                                    MQProperties properties,
                                    MQMessageConverter messageConverter,
                                    MQDeadLetterService deadLetterService,
                                    RedisStreamKeySupport keySupport,
                                    List<MQConsumerHandler> handlers) {
        this.redisConnectionFactory = redisConnectionFactory;
        this.stringRedisTemplate = stringRedisTemplate;
        this.properties = properties;
        this.messageConverter = messageConverter;
        this.deadLetterService = deadLetterService;
        this.keySupport = keySupport;
        this.handlers = handlers;
        this.executorService = createExecutor(properties, handlers);
    }

    @Override
    public void start() {
        if (running) {
            return;
        }
        running = true;
        for (MQConsumerHandler handler : handlers) {
            MQSubscription subscription = handler.subscription();
            for (int shard = 0; shard < Math.max(subscription.getShardCount(), 1); shard++) {
                int currentShard = shard;
                LOG.info("启动 MQ 消费线程: topic={}, group={}, shard={}, handler={}",
                    subscription.getTopic(), subscription.getConsumerGroup(), currentShard, handler.getClass().getSimpleName());
                try {
                    executorService.submit(() -> consumeLoop(handler, subscription, currentShard));
                } catch (RuntimeException e) {
                    running = false;
                    throw e;
                }
            }
        }
    }

    @Override
    public void stop() {
        running = false;
        executorService.shutdownNow();
        try {
            executorService.awaitTermination(5, TimeUnit.SECONDS);
        } catch (InterruptedException ignored) {
            Thread.currentThread().interrupt();
        }
    }

    @Override
    public boolean isRunning() {
        return running;
    }

    @Override
    public int getPhase() {
        return Integer.MAX_VALUE;
    }

    @PreDestroy
    public void destroy() {
        stop();
    }

    private ExecutorService createExecutor(MQProperties properties, List<MQConsumerHandler> handlers) {
        MQProperties.Redis.ConsumerExecutor config = properties.getRedis().getConsumerExecutor();
        int consumerTaskCount = handlers.stream()
            .map(MQConsumerHandler::subscription)
            .filter(Objects::nonNull)
            .mapToInt(subscription -> Math.max(subscription.getShardCount(), 1))
            .sum();
        if (config.getCoreSize() > config.getMaxSize()) {
            throw new IllegalStateException("Redis MQ 消费线程池配置错误：core-size 不能大于 max-size");
        }
        if (consumerTaskCount > config.getMaxSize()) {
            throw new IllegalStateException("Redis MQ 消费线程池配置错误：max-size="
                + config.getMaxSize() + " 小于消费循环数 " + consumerTaskCount
                + "，请调大 easyflow.mq.redis.consumer-executor.max-size");
        }
        int coreSize = Math.max(config.getCoreSize(), consumerTaskCount);
        int maxSize = config.getMaxSize();
        AtomicInteger threadIndex = new AtomicInteger(1);
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
            coreSize,
            maxSize,
            config.getKeepAliveSeconds(),
            TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(config.getQueueCapacity()),
            task -> {
                Thread thread = new Thread(task);
                thread.setName("redis-mq-consumer-" + threadIndex.getAndIncrement());
                thread.setDaemon(false);
                return thread;
            },
            new ThreadPoolExecutor.AbortPolicy()
        );
        executor.allowCoreThreadTimeOut(true);
        return executor;
    }

    private void consumeLoop(MQConsumerHandler handler, MQSubscription subscription, int shard) {
        String streamKey = keySupport.streamKey(subscription.getTopic(), shard);
        String consumerName = buildConsumerName(subscription.getConsumerGroup(), shard);
        ensureConsumerGroup(streamKey, subscription.getConsumerGroup());
        LOG.info("MQ 消费循环已启动: topic={}, group={}, shard={}, consumer={}, streamKey={}, handler={}",
            subscription.getTopic(), subscription.getConsumerGroup(), shard, consumerName, streamKey, handler.getClass().getSimpleName());
        while (running) {
            try {
                List<MapRecord<String, Object, Object>> pendingRecords =
                        reclaimPending(streamKey, subscription.getConsumerGroup(), consumerName);
                if (!pendingRecords.isEmpty()) {
                    List<MQMessage> pendingMessages = toMessages(streamKey, pendingRecords);
                    if (!pendingMessages.isEmpty()) {
                        LOG.info("MQ 收到重领 pending 消息批次: topic={}, group={}, shard={}, consumer={}, streamKey={}, count={}",
                            subscription.getTopic(), subscription.getConsumerGroup(), shard, consumerName,
                            streamKey, pendingMessages.size());
                        handleMessages(handler, subscription, streamKey, subscription.getConsumerGroup(), pendingMessages);
                        continue;
                    }
                }
                List<MapRecord<String, Object, Object>> records = stringRedisTemplate.opsForStream().read(
                        Consumer.from(subscription.getConsumerGroup(), consumerName),
                        StreamReadOptions.empty()
                                .count(properties.getRedis().getConsumerBatchSize())
                                .block(properties.getRedis().getConsumerBlockTimeout()),
                        StreamOffset.create(streamKey, org.springframework.data.redis.connection.stream.ReadOffset.lastConsumed())
                );
                if (records == null || records.isEmpty()) {
                    continue;
                }
                List<MQMessage> messages = toMessages(streamKey, records);
                if (messages.isEmpty()) {
                    continue;
                }
                LOG.info("MQ 收到消息批次: topic={}, group={}, shard={}, consumer={}, streamKey={}, count={}",
                    subscription.getTopic(), subscription.getConsumerGroup(), shard, consumerName, streamKey, messages.size());
                handleMessages(handler, subscription, streamKey, subscription.getConsumerGroup(), messages);
            } catch (Exception exception) {
                LOG.error("MQ 消费循环异常: topic={}, group={}, shard={}, consumer={}, streamKey={}, handler={}",
                    subscription.getTopic(),
                    subscription.getConsumerGroup(),
                    shard,
                    consumerName,
                    streamKey,
                    handler.getClass().getSimpleName(),
                    exception);
                sleepSilently(1000L);
            }
        }
    }

    /**
     * 构建 Redis Stream consumer name。
     *
     * @param consumerGroup 消费组
     * @param shard 分片序号
     * @return consumer name
     */
    String buildConsumerName(String consumerGroup, int shard) {
        String instanceId = properties.getRedis().getConsumerInstanceId();
        String safeInstanceId = UNSAFE_CONSUMER_NAME_CHARS.matcher(instanceId).replaceAll("-");
        return consumerGroup + "-" + shard + "-" + safeInstanceId;
    }

    List<MapRecord<String, Object, Object>> reclaimPending(String streamKey, String group, String consumerName) {
        Duration idle = properties.getRedis().getPendingClaimIdle();
        try (RedisConnection connection = redisConnectionFactory.getConnection()) {
            RedisStreamCommands.XPendingOptions options = RedisStreamCommands.XPendingOptions
                    .range(Range.unbounded(), (long) properties.getRedis().getConsumerBatchSize());
            var pendingMessages = connection.streamCommands()
                    .xPending(streamKey.getBytes(StandardCharsets.UTF_8), group, options);
            if (pendingMessages == null || pendingMessages.isEmpty()) {
                return List.of();
            }
            List<RecordId> ids = new ArrayList<>();
            for (PendingMessage pendingMessage : pendingMessages) {
                if (pendingMessage.getElapsedTimeSinceLastDelivery().compareTo(idle) >= 0) {
                    ids.add(pendingMessage.getId());
                }
            }
            if (ids.isEmpty()) {
                return List.of();
            }
            List<MapRecord<String, Object, Object>> records = stringRedisTemplate.opsForStream().claim(
                    streamKey,
                    group,
                    consumerName,
                    idle,
                    ids.toArray(new RecordId[0])
            );
            return records == null ? List.of() : records;
        }
    }

    private void ensureConsumerGroup(String streamKey, String group) {
        try (RedisConnection connection = redisConnectionFactory.getConnection()) {
            connection.streamCommands().xGroupCreate(
                    streamKey.getBytes(StandardCharsets.UTF_8),
                    group,
                    org.springframework.data.redis.connection.stream.ReadOffset.latest(),
                    true
            );
        } catch (Exception ignored) {
        }
    }

    List<MQMessage> toMessages(String streamKey, List<MapRecord<String, Object, Object>> records) {
        List<MQMessage> messages = new ArrayList<>(records.size());
        for (MapRecord<String, Object, Object> record : records) {
            Object payload = record.getValue().get("payload");
            if (payload == null) {
                continue;
            }
            MQMessage message = messageConverter.deserialize(String.valueOf(payload));
            message.setStreamKey(streamKey);
            message.setStreamMessageId(record.getId().getValue());
            messages.add(message);
        }
        return messages;
    }

    private void retryOrDeadLetter(List<MQMessage> messages, String reason) {
        for (MQMessage message : messages) {
            int retryCount = message.getRetryCount() + 1;
            message.setRetryCount(retryCount);
            message.getHeaders().put("lastError", reason == null ? "" : reason);
            if (retryCount > properties.getRedis().getMaxRetry()) {
                LOG.error("MQ 消息超过最大重试次数，进入死信队列: topic={}, messageId={}, streamKey={}, retryCount={}, reason={}",
                    message.getTopic(), message.getMessageId(), message.getStreamKey(), retryCount, reason);
                deadLetterService.deadLetter(message, reason);
            } else {
                LOG.warn("MQ 消息消费失败，准备重试: topic={}, messageId={}, streamKey={}, retryCount={}, reason={}",
                    message.getTopic(), message.getMessageId(), message.getStreamKey(), retryCount, reason);
                stringRedisTemplate.opsForStream().add(
                        org.springframework.data.redis.connection.stream.StreamRecords.string(
                                Map.of("payload", messageConverter.serialize(message))
                        ).withStreamKey(message.getStreamKey())
                );
            }
        }
    }

    void handleMessages(MQConsumerHandler handler,
                        MQSubscription subscription,
                        String streamKey,
                        String group,
                        List<MQMessage> messages) throws Exception {
        if (!subscription.isBatchEnabled()) {
            handleMessagesIndividually(handler, streamKey, group, messages);
            return;
        }
        try {
            LOG.info("MQ 开始批量处理消息: group={}, streamKey={}, count={}, handler={}",
                group, streamKey, messages.size(), handler.getClass().getSimpleName());
            handler.handle(messages);
            acknowledge(streamKey, group, messages);
            LOG.info("MQ 批量处理消息完成: group={}, streamKey={}, count={}, handler={}",
                group, streamKey, messages.size(), handler.getClass().getSimpleName());
            return;
        } catch (Exception batchEx) {
            LOG.error("MQ 批量处理消息失败，准备降级单条处理: group={}, streamKey={}, count={}, handler={}",
                group, streamKey, messages.size(), handler.getClass().getSimpleName(), batchEx);
            if (messages.size() == 1) {
                retryOrDeadLetter(messages, resolveReason(batchEx));
                acknowledge(streamKey, group, messages);
                return;
            }
        }

        handleMessagesIndividually(handler, streamKey, group, messages);
    }

    private void handleMessagesIndividually(MQConsumerHandler handler,
                                            String streamKey,
                                            String group,
                                            List<MQMessage> messages) {
        for (MQMessage message : messages) {
            try {
                LOG.info("MQ 开始单条处理消息: group={}, streamKey={}, messageId={}, handler={}",
                    group, streamKey, message.getMessageId(), handler.getClass().getSimpleName());
                handler.handle(List.of(message));
                LOG.info("MQ 单条处理消息完成: group={}, streamKey={}, messageId={}, handler={}",
                    group, streamKey, message.getMessageId(), handler.getClass().getSimpleName());
            } catch (Exception singleEx) {
                retryOrDeadLetter(List.of(message), resolveReason(singleEx));
            } finally {
                acknowledge(streamKey, group, List.of(message));
            }
        }
    }

    private void acknowledge(String streamKey, String group, List<MQMessage> messages) {
        if (messages == null || messages.isEmpty()) {
            return;
        }
        String[] ids = messages.stream()
                .map(MQMessage::getStreamMessageId)
                .filter(Objects::nonNull)
                .toArray(String[]::new);
        if (ids.length == 0) {
            return;
        }
        MQAcknowledger acknowledger = records -> stringRedisTemplate.opsForStream().acknowledge(streamKey, group, ids);
        acknowledger.acknowledge(messages);
        LOG.info("MQ 消息确认完成: group={}, streamKey={}, count={}", group, streamKey, ids.length);
    }

    private String resolveReason(Exception exception) {
        if (exception == null || exception.getMessage() == null || exception.getMessage().isBlank()) {
            return "消费失败";
        }
        return exception.getMessage();
    }

    private void sleepSilently(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException ignored) {
            Thread.currentThread().interrupt();
        }
    }
}
