package com.simplydistributed.wordpress.kafkastreams.rest;

import com.simplydistributed.wordpress.kafkastreams.CPUMetricStreamHandler;
import com.simplydistributed.wordpress.kafkastreams.GlobalAppState;
import com.simplydistributed.wordpress.kafkastreams.Utils;
import com.simplydistributed.wordpress.kafkastreams.domain.Metrics;
import java.util.concurrent.TimeUnit;

import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.apache.kafka.common.serialization.StringSerializer;

import org.apache.kafka.streams.KafkaStreams;
import org.apache.kafka.streams.KeyValue;
import org.apache.kafka.streams.state.HostInfo;
import org.apache.kafka.streams.state.KeyValueIterator;
import org.apache.kafka.streams.state.QueryableStoreTypes;
import org.apache.kafka.streams.state.ReadOnlyKeyValueStore;
import org.apache.kafka.streams.state.StreamsMetadata;

/**
 * REST interface for fetching Kafka Stream processing state. Exposes endpoints
 * for local as well as remote client
 */
@Path("metrics")
public final class MetricsResource {

    private final String storeName;

    public MetricsResource() {
        storeName = CPUMetricStreamHandler.AVG_STORE_NAME;
    }

    private static final Logger LOGGER = Logger.getLogger(MetricsResource.class.getName());

    /**
     * Local interface for fetching metrics
     *
     * @return Metrics from local and other remote stores (if needed)
     * @throws Exception
     */
    @GET
    @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
    public Response all_metrics() throws Exception {
        Response response = null;
        try {
            KafkaStreams ks = GlobalAppState.getInstance().getKafkaStreams();
            HostInfo thisInstance = GlobalAppState.getInstance().getHostPortInfo();

            LOGGER.info("Querying local store");
            Metrics metrics = getLocalMetrics();

            // LOGGER.info("Querying remote stores....");
            ks.allMetadataForStore(storeName)
                    .stream()
                    .filter(sm -> !(sm.host().equals(thisInstance.host()) && sm.port() == thisInstance.port())) //only query remote node stores
                    .forEach(new Consumer<StreamsMetadata>() {
                        @Override
                        public void accept(StreamsMetadata t) {
                            String url = "http://" + t.host() + ":" + t.port() + "/metrics/remote";
                            //LOGGER.log(Level.INFO, "Fetching remote store at {0}", url);
                            Metrics remoteMetrics = Utils.getRemoteStoreState(url, 2, TimeUnit.SECONDS);
                            metrics.add(remoteMetrics);
                            LOGGER.log(Level.INFO, "Metric from remote store at {0} == {1}", new Object[]{url, remoteMetrics});
                        }

                    });

            LOGGER.log(Level.INFO, "Complete store state {0}", metrics);
            response = Response.ok(metrics).build();
        } catch (Exception e) {
            LOGGER.log(Level.SEVERE, "Error - {0}", e.getMessage());
            e.printStackTrace();
        }

        return response;
    }

    /**
     * Remote interface for fetching metrics
     *
     * @return Metrics
     */
    @GET
    @Path("remote")
    @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
    public Metrics remote() {
        LOGGER.info("Remote interface invoked");
        Metrics metrics = null;
        try {
            metrics = getLocalMetrics();
        } catch (Exception e) {
            LOGGER.log(Level.SEVERE, "Error - {0}", e.getMessage());
            e.printStackTrace();
        }

        return metrics;

    }

    /**
     * Query local state store to extract metrics
     *
     * @return local Metrics
     */
    private Metrics getLocalMetrics() {
        HostInfo thisInstance = GlobalAppState.getInstance().getHostPortInfo();
        KafkaStreams ks = GlobalAppState.getInstance().getKafkaStreams();

        String source = thisInstance.host() + ":" + thisInstance.port();
        Metrics localMetrics = new Metrics();

        ReadOnlyKeyValueStore<String, Double> averageStore = ks
                .store(storeName,
                        QueryableStoreTypes.<String, Double>keyValueStore());

        LOGGER.log(Level.INFO, "Entries in store {0}", averageStore.approximateNumEntries());
        KeyValueIterator<String, Double> storeIterator = averageStore.all();

        while (storeIterator.hasNext()) {
            KeyValue<String, Double> kv = storeIterator.next();
            localMetrics.add(source, kv.key, String.valueOf(kv.value));

        }
        LOGGER.log(Level.INFO, "Local store state {0}", localMetrics);
        return localMetrics;
    }

    /**
     * Metrics for a machine
     *
     * @param machine
     * @return the metric
     */
    @GET
    @Path("{machine}")
    @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
    public Response getMachineMetric(@PathParam("machine") String machine) {
        LOGGER.log(Level.INFO, "Fetching metrics for machine {0}", machine);

        KafkaStreams ks = GlobalAppState.getInstance().getKafkaStreams();
        HostInfo thisInstance = GlobalAppState.getInstance().getHostPortInfo();

        Metrics metrics = null;

        StreamsMetadata metadataForMachine = ks.metadataForKey(storeName, machine, new StringSerializer());

        if (metadataForMachine.host().equals(thisInstance.host()) && metadataForMachine.port() == thisInstance.port()) {
            LOGGER.log(Level.INFO, "Querying local store for machine {0}", machine);
            metrics = getLocalMetrics(machine);
        } else {
            //LOGGER.log(Level.INFO, "Querying remote store for machine {0}", machine);
            String url = "http://" + metadataForMachine.host() + ":" + metadataForMachine.port() + "/metrics/remote/" + machine;
            metrics = Utils.getRemoteStoreState(url, 2, TimeUnit.SECONDS);
            LOGGER.log(Level.INFO, "Metric from remote store at {0} == {1}", new Object[]{url, metrics});
        }

        return Response.ok(metrics).build();
    }

    /**
     * Remote interface for fetching a specific machine's metric
     * @param machine
     * @return Metrics
     */
    @GET
    @Path("remote/{machine}")
    @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
    public Metrics remote(@PathParam("machine") String machine) {
        LOGGER.log(Level.INFO, "Remote interface invoked for machine {0}", machine);
        Metrics metrics = null;
        try {
            metrics = getLocalMetrics(machine);
        } catch (Exception e) {
            LOGGER.log(Level.SEVERE, "Error - {0}", e.getMessage());
            e.printStackTrace();
        }

        return metrics;

    }

    /**
     * get Metrics for a machine
     * @param machine
     * @return 
     */
    private Metrics getLocalMetrics(String machine) {
        LOGGER.log(Level.INFO, "Getting Metrics for machine {0}", machine);
        
        HostInfo thisInstance = GlobalAppState.getInstance().getHostPortInfo();
        KafkaStreams ks = GlobalAppState.getInstance().getKafkaStreams();

        String source = thisInstance.host() + ":" + thisInstance.port();
        Metrics localMetrics = new Metrics();

        ReadOnlyKeyValueStore<String, Double> averageStore = ks
                .store(storeName,
                        QueryableStoreTypes.<String, Double>keyValueStore());

        LOGGER.log(Level.INFO, "Entries in store {0}", averageStore.approximateNumEntries());

        localMetrics.add(source, machine, String.valueOf(averageStore.get(machine)));

        LOGGER.log(Level.INFO, "Metrics for machine {0} - {1}", new Object[]{machine, localMetrics});
        return localMetrics;
    }

}