package com.lanyuanxiaoyao.service.pulsar;

import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.lanyuanxiaoyao.service.common.exception.PulsarInfoNotFoundException;
import com.lanyuanxiaoyao.service.configuration.entity.pulsar.PulsarInfo;
import com.lanyuanxiaoyao.service.configuration.entity.pulsar.PulsarNamespace;
import com.lanyuanxiaoyao.service.configuration.entity.pulsar.PulsarTenant;
import com.lanyuanxiaoyao.service.configuration.entity.pulsar.PulsarTopic;
import com.ulisesbocchio.jasyptspringboot.annotation.EnableEncryptableProperties;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.pulsar.client.admin.PulsarAdmin;
import org.apache.pulsar.client.admin.PulsarAdminException;
import org.apache.pulsar.client.api.*;
import org.apache.pulsar.client.impl.schema.StringSchema;
import org.apache.pulsar.common.policies.data.ConsumerStats;
import org.apache.pulsar.common.policies.data.*;
import org.eclipse.collections.api.factory.Lists;
import org.eclipse.collections.api.list.ImmutableList;
import org.eclipse.collections.api.list.MutableList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author lanyuanxiaoyao
 * @date 2023-04-27
 */
@EnableDiscoveryClient
@SpringBootApplication(
        scanBasePackages = {"com.lanyuanxiaoyao.service"},
        exclude = {
                GsonAutoConfiguration.class,
                DataSourceAutoConfiguration.class
        }
)
@EnableConfigurationProperties
@EnableEncryptableProperties
@EnableRetry
@RestController
@RequestMapping("pulsar")
public class PulsarQueryApplication {
    private static final Logger logger = LoggerFactory.getLogger(PulsarQueryApplication.class);

    public static void main(String[] args) {
        SpringApplication.run(PulsarQueryApplication.class, args);
    }

    private static PulsarTopic convert(PulsarAdmin admin, String topic) throws PulsarAdminException {
        TopicStats stats = admin.topics().getStats(topic);
        MessageId lastMessageId = admin.topics().getLastMessageId(topic);
        List<String> subscriptions = admin.topics().getSubscriptions(topic);
        return new PulsarTopic(
                topic,
                ObjectUtil.isNull(stats) ? null : new PulsarTopic.State(
                        stats.getMsgRateIn(),
                        stats.getMsgRateOut(),
                        stats.getMsgThroughputIn(),
                        stats.getMsgThroughputOut(),
                        stats.getBytesInCounter(),
                        stats.getBytesOutCounter(),
                        stats.getMsgInCounter(),
                        stats.getMsgOutCounter(),
                        stats.getAverageMsgSize(),
                        stats.isMsgChunkPublished(),
                        stats.getStorageSize(),
                        stats.getBacklogSize(),
                        stats.getOffloadedStorageSize(),
                        ObjectUtil.isNull(stats.getPublishers()) ? null : new ArrayList<PulsarTopic.State.PublisherState>() {{
                            for (PublisherStats publisher : stats.getPublishers()) {
                                add(new PulsarTopic.State.PublisherState(
                                        publisher.getAccessMode().name(),
                                        publisher.getMsgRateIn(),
                                        publisher.getMsgThroughputIn(),
                                        publisher.getAverageMsgSize(),
                                        publisher.getChunkedMessageRate(),
                                        publisher.getProducerId(),
                                        publisher.getProducerName(),
                                        publisher.getAddress(),
                                        publisher.getConnectedSince(),
                                        publisher.getClientVersion(),
                                        publisher.getMetadata()
                                ));
                            }
                        }},
                        stats.getWaitingPublishers(),
                        ObjectUtil.isNull(stats.getSubscriptions()) ? null : new HashMap<String, PulsarTopic.State.SubscriptionState>() {{
                            for (Entry<String, ? extends SubscriptionStats> entry : stats.getSubscriptions().entrySet()) {
                                SubscriptionStats state = entry.getValue();
                                put(entry.getKey(), new PulsarTopic.State.SubscriptionState(
                                        state.getMsgRateOut(),
                                        state.getMsgThroughputOut(),
                                        state.getBytesOutCounter(),
                                        state.getMsgOutCounter(),
                                        state.getMsgRateRedeliver(),
                                        state.getChunkedMessageRate(),
                                        state.getMsgBacklog(),
                                        state.getBacklogSize(),
                                        state.getMsgBacklogNoDelayed(),
                                        state.isBlockedSubscriptionOnUnackedMsgs(),
                                        state.getMsgDelayed(),
                                        state.getUnackedMessages(),
                                        state.getType(),
                                        state.getActiveConsumerName(),
                                        state.getMsgRateExpired(),
                                        state.getTotalMsgExpired(),
                                        state.getLastExpireTimestamp(),
                                        state.getLastConsumedFlowTimestamp(),
                                        state.getLastConsumedTimestamp(),
                                        state.getLastAckedTimestamp(),
                                        state.getLastMarkDeleteAdvancedTimestamp(),
                                        new ArrayList<PulsarTopic.State.ConsumerState>() {{
                                            for (ConsumerStats consumer : state.getConsumers()) {
                                                add(new PulsarTopic.State.ConsumerState(
                                                        consumer.getMsgRateOut(),
                                                        consumer.getMsgThroughputOut(),
                                                        consumer.getBytesOutCounter(),
                                                        consumer.getMsgOutCounter(),
                                                        consumer.getMsgRateRedeliver(),
                                                        consumer.getChunkedMessageRate(),
                                                        consumer.getConsumerName(),
                                                        consumer.getAvailablePermits(),
                                                        consumer.getUnackedMessages(),
                                                        consumer.getAvgMessagesPerEntry(),
                                                        consumer.isBlockedConsumerOnUnackedMsgs(),
                                                        consumer.getReadPositionWhenJoining(),
                                                        consumer.getAddress(),
                                                        consumer.getConnectedSince(),
                                                        consumer.getClientVersion(),
                                                        consumer.getLastAckedTimestamp(),
                                                        consumer.getLastConsumedTimestamp(),
                                                        consumer.getKeyHashRanges(),
                                                        consumer.getMetadata()
                                                ));
                                            }
                                        }},
                                        state.isDurable(),
                                        state.isReplicated(),
                                        state.getConsumersAfterMarkDeletePosition()
                                ));
                            }
                        }},
                        ObjectUtil.isNull(stats.getReplication()) ? null : new HashMap<String, PulsarTopic.State.ReplicateState>() {{
                            for (Entry<String, ? extends ReplicatorStats> entry : stats.getReplication().entrySet()) {
                                ReplicatorStats state = entry.getValue();
                                put(entry.getKey(), new PulsarTopic.State.ReplicateState(
                                        state.getMsgRateIn(),
                                        state.getMsgRateOut(),
                                        state.getMsgThroughputIn(),
                                        state.getMsgThroughputOut(),
                                        state.getMsgRateExpired(),
                                        state.getReplicationBacklog(),
                                        state.isConnected(),
                                        state.getReplicationDelayInSeconds(),
                                        state.getInboundConnection(),
                                        state.getInboundConnectedSince(),
                                        state.getOutboundConnection(),
                                        state.getOutboundConnectedSince()
                                ));
                            }
                        }},
                        stats.getDeduplicationStatus(),
                        stats.getTopicEpoch()
                ),
                lastMessageId.toString(),
                subscriptions
        );
    }

    private PulsarInfo getInfo(String name) {
        if (PulsarInfo.PULSAR_NAME_INFO_MAPPING.containsKey(name)) {
            return PulsarInfo.PULSAR_NAME_INFO_MAPPING.get(name);
        }
        throw new PulsarInfoNotFoundException("Pulsar info not found for " + name);
    }

    private String adminUrl(PulsarInfo info) {
        return StrUtil.format("http://{}/admin/v2", info.getAdmin());
    }

    @Cacheable(value = "name", sync = true)
    @GetMapping("names")
    public ImmutableList<String> names() {
        return Lists.immutable.ofAll(PulsarInfo.DEFAULT_INFOS).collect(PulsarInfo::getName);
    }

    @Cacheable(value = "name", sync = true)
    @GetMapping("name")
    public String name(@RequestParam("pulsar_url") String pulsarUrl) {
        String[] urls = pulsarUrl.replaceAll("pulsar://", "").split(",");
        if (ObjectUtil.isNotEmpty(urls)) {
            return PulsarInfo.PULSAR_CLIENT_NAME_MAPPING.get(urls[0]);
        }
        throw new PulsarInfoNotFoundException("Pulsar name not found for " + pulsarUrl);
    }

    @Cacheable(value = "tenants", sync = true)
    @GetMapping("tenants")
    public ImmutableList<PulsarTenant> tenants(@RequestParam("name") String name) throws PulsarClientException, PulsarAdminException {
        PulsarInfo info = getInfo(name);
        try (PulsarAdmin admin = PulsarAdmin.builder()
                .serviceHttpUrl(adminUrl(info))
                .build()) {
            MutableList<PulsarTenant> result = Lists.mutable.empty();
            for (String tenant : admin.tenants().getTenants()) {
                result.add(new PulsarTenant(tenant));
            }
            return result.toImmutable();
        }
    }

    @Cacheable(value = "namespaces", sync = true)
    @GetMapping("namespaces")
    public ImmutableList<PulsarNamespace> namespaces(@RequestParam("name") String name, @RequestParam("tenant") String tenant) throws PulsarClientException, PulsarAdminException {
        PulsarInfo info = getInfo(name);
        try (PulsarAdmin admin = PulsarAdmin.builder()
                .serviceHttpUrl(adminUrl(info))
                .build()) {
            MutableList<PulsarNamespace> result = Lists.mutable.empty();
            for (String namespace : admin.namespaces().getNamespaces(tenant)) {
                result.add(new PulsarNamespace(tenant, namespace));
            }
            return result.toImmutable();
        }
    }

    @GetMapping("topics")
    public ImmutableList<PulsarTopic> topics(@RequestParam("name") String name, @RequestParam("namespace") String namespace) throws PulsarClientException, PulsarAdminException {
        PulsarInfo info = getInfo(name);
        try (PulsarAdmin admin = PulsarAdmin.builder()
                .serviceHttpUrl(adminUrl(info))
                .build()) {
            MutableList<PulsarTopic> result = Lists.mutable.empty();
            for (String topic : admin.topics().getList(namespace)) {
                result.add(convert(admin, topic));
            }
            return result.toImmutable();
        }
    }

    @Cacheable(value = "topic", sync = true)
    @GetMapping("topic")
    public PulsarTopic topic(@RequestParam("name") String name, @RequestParam("topic") String topic) throws PulsarClientException, PulsarAdminException {
        PulsarInfo info = getInfo(name);
        try (PulsarAdmin admin = PulsarAdmin.builder()
                .serviceHttpUrl(adminUrl(info))
                .build()) {
            return convert(admin, topic);
        }
    }

    @Cacheable(value = "backlog", sync = true)
    @GetMapping("backlog")
    public Long backlog(@RequestParam("name") String name, @RequestParam("topic") String topic, @RequestParam("subscription") String subscription) throws PulsarClientException, PulsarAdminException {
        PulsarInfo info = getInfo(name);
        try (PulsarAdmin admin = PulsarAdmin.builder()
                .serviceHttpUrl(adminUrl(info))
                .build()) {
            Map<String, ? extends SubscriptionStats> subscriptions = admin.topics().getStats(topic).getSubscriptions();
            if (ObjectUtil.isNotNull(subscriptions) && subscriptions.containsKey(subscription)) {
                return subscriptions.get(subscription).getBacklogSize();
            }
            return -1L;
        }
    }

    @Cacheable(value = "exists-topic", sync = true)
    @GetMapping("exists_topic")
    public Boolean existsTopic(@RequestParam("url") String url, @RequestParam("topic") String topic) {
        try (PulsarClient client = PulsarClient.builder()
                .serviceUrl(url)
                .build()) {
            try (Consumer<String> consumer = client.newConsumer(new StringSchema())
                    .topic(topic)
                    .subscriptionMode(SubscriptionMode.NonDurable)
                    .subscriptionType(SubscriptionType.Exclusive)
                    .subscriptionName("Pulsar_Exists_Topic_Detect_" + IdUtil.nanoId(8))
                    .subscribe()) {
                return consumer.isConnected();
            }
        } catch (PulsarClientException e) {
            logger.warn("Pulsar {} not contain topic {}", url, topic);
        }
        return false;
    }
}
